fix(qa): repair package Telegram harness boundaries (#108499)

* fix(qa): keep packaged entry off private transport

* fix(qa): lazy-load private transport runtime

* fix(qa): pass relative package artifact path

* fix(qa): isolate packaged bus protocol

* fix(qa): mount package scenario catalog
This commit is contained in:
Peter Steinberger
2026-07-15 19:24:55 -07:00
committed by GitHub
parent 08ecf63bb6
commit 154d1277e1
14 changed files with 245 additions and 17 deletions
+32
View File
@@ -0,0 +1,32 @@
import { describe, expect, it, vi } from "vitest";
const qaChannelLoads = vi.hoisted(() => vi.fn());
const qaChannelProtocolLoads = vi.hoisted(() => vi.fn());
vi.mock("openclaw/plugin-sdk/qa-channel", () => {
qaChannelLoads();
throw new Error("QA Lab entrypoint loaded the private QA channel");
});
vi.mock("openclaw/plugin-sdk/qa-channel-protocol", () => {
qaChannelProtocolLoads();
throw new Error("QA Lab entrypoint loaded the private QA channel protocol");
});
describe("QA Lab plugin entrypoint", () => {
it("loads without the private QA transport runtime", async () => {
const { default: plugin } = await import("./index.js");
expect(plugin.id).toBe("qa-lab");
expect(qaChannelLoads).not.toHaveBeenCalled();
expect(qaChannelProtocolLoads).not.toHaveBeenCalled();
});
it("loads the package Telegram harness without the private QA transport runtime", async () => {
const { runQaTelegramSuite } = await import("./src/live-transports/telegram/cli.runtime.js");
expect(runQaTelegramSuite).toBeTypeOf("function");
expect(qaChannelLoads).not.toHaveBeenCalled();
expect(qaChannelProtocolLoads).not.toHaveBeenCalled();
});
});
+2 -1
View File
@@ -1,7 +1,8 @@
// Qa Lab plugin entrypoint registers its OpenClaw integration.
import { setTimeout as sleep } from "node:timers/promises";
// Keep plugin registration independent of private QA transports, which packaged runtimes omit.
import { definePluginEntry } from "openclaw/plugin-sdk/plugin-entry";
import { jsonResult } from "openclaw/plugin-sdk/tool-results";
import { definePluginEntry } from "./runtime-api.js";
import { registerQaLabCli } from "./src/cli.js";
import { createQaLabWebSearchProvider } from "./src/qa-web-search-provider.js";
import { createStaticSshWorkerProvider } from "./src/static-ssh-worker-provider.js";
+1 -1
View File
@@ -1,6 +1,6 @@
// Qa Lab plugin module implements bus queries behavior.
import { parseQaTarget } from "openclaw/plugin-sdk/qa-channel-protocol";
import { normalizeOptionalLowercaseString } from "openclaw/plugin-sdk/string-coerce-runtime";
import { parseQaTarget } from "./qa-bus-protocol.js";
import type {
QaBusAttachment,
QaBusConversation,
+1 -1
View File
@@ -1,6 +1,5 @@
// Qa Lab plugin module implements bus state behavior.
import { randomUUID } from "node:crypto";
import { sanitizeQaBusToolCalls } from "openclaw/plugin-sdk/qa-channel-protocol";
import {
buildQaBusSnapshot,
cloneMessage,
@@ -12,6 +11,7 @@ import {
searchQaBusMessages,
} from "./bus-queries.js";
import { createQaBusWaiterStore } from "./bus-waiters.js";
import { sanitizeQaBusToolCalls } from "./qa-bus-protocol.js";
import type {
QaBusAttachment,
QaBusConversation,
+1 -1
View File
@@ -14,7 +14,7 @@ const qaChannelMock = vi.hoisted(() => ({
startAccount: vi.fn(),
}));
vi.mock("./runtime-api.js", () => ({
vi.mock("openclaw/plugin-sdk/qa-channel", () => ({
qaChannelPlugin: {
config: {
resolveAccount: qaChannelMock.resolveAccount,
+2 -1
View File
@@ -2,6 +2,7 @@
import fs from "node:fs";
import { createServer } from "node:http";
import path from "node:path";
import type { OpenClawConfig } from "openclaw/plugin-sdk/config-contracts";
import { formatErrorMessage } from "openclaw/plugin-sdk/error-runtime";
import {
acquireDebugProxyCaptureStore,
@@ -54,7 +55,6 @@ import {
createQaRunOutputDir,
normalizeQaRunSelection,
} from "./run-config.js";
import { qaChannelPlugin, setQaChannelRuntime, type OpenClawConfig } from "./runtime-api.js";
import { readQaBootstrapScenarioCatalog } from "./scenario-catalog.js";
import { runQaSelfCheckAgainstState, type QaSelfCheckResult } from "./self-check.js";
@@ -247,6 +247,7 @@ function detectQaEvidenceArtifactContentType(filePath: string): string {
}
async function startQaGatewayLoop(params: { state: QaBusState; baseUrl: string }) {
const { qaChannelPlugin, setQaChannelRuntime } = await import("openclaw/plugin-sdk/qa-channel");
const runtime = createQaRunnerRuntime();
setQaChannelRuntime(runtime);
const cfg = createQaLabConfig(params.baseUrl);
@@ -0,0 +1,60 @@
import {
parseQaTarget as parseCanonicalQaTarget,
sanitizeQaBusToolCalls as sanitizeCanonicalQaBusToolCalls,
} from "openclaw/plugin-sdk/qa-channel-protocol";
import { describe, expect, it } from "vitest";
import { parseQaTarget, sanitizeQaBusToolCalls } from "./qa-bus-protocol.js";
describe("QA Lab package bus protocol", () => {
it.each([
"bare-id",
"channel:CaseSensitive",
"group:team-room",
"dm:user-1",
"thread:Room/Topic",
])("matches the canonical target parser for %s", (target) => {
expect(parseQaTarget(target)).toEqual(parseCanonicalQaTarget(target));
});
it.each(["", "CHANNEL:CaseSensitive", "thread:Room/", "dm:"])(
"matches canonical target errors for %j",
(target) => {
expect(() => parseQaTarget(target)).toThrow();
expect(() => parseCanonicalQaTarget(target)).toThrow();
},
);
it("matches canonical bounded redaction", () => {
const toolCalls = [
null,
{ name: 123 },
{
name: " exec ",
arguments: {
command: "cat README.md",
apiToken: "secret-token",
headers: { Authorization: "Bearer secret" },
values: ["ok", { password: "hunter2" }],
nested: { one: { two: { three: { four: "truncated" } } } },
finite: 42,
infinite: Number.POSITIVE_INFINITY,
bigint: 123n,
omitted: undefined,
},
},
];
expect(sanitizeQaBusToolCalls(toolCalls)).toEqual(sanitizeCanonicalQaBusToolCalls(toolCalls));
});
it("matches the canonical count limit without processing the tail", () => {
const toolCalls = Array.from({ length: 50 }, (_, index) => ({ name: `tool-${index}` }));
toolCalls.push({
get name(): string {
throw new Error("tail should not be sanitized");
},
});
expect(sanitizeQaBusToolCalls(toolCalls)).toEqual(sanitizeCanonicalQaBusToolCalls(toolCalls));
});
});
+131
View File
@@ -0,0 +1,131 @@
// QA Lab source is mounted into package acceptance without the local-only QA Channel SDK.
// Keep its in-memory bus protocol self-contained; parity tests guard the shared semantics.
import { isRecord } from "openclaw/plugin-sdk/string-coerce-runtime";
import type { QaBusConversation, QaBusToolCall } from "./runtime-api.js";
type QaTargetParts = {
chatType: QaBusConversation["kind"];
conversationId: string;
threadId?: string;
};
export function parseQaTarget(raw: string): QaTargetParts {
const normalized = raw.trim();
if (!normalized) {
throw new Error("qa-channel target is required");
}
const prefixed = /^(thread|channel|group|dm):(.*)$/u.exec(normalized);
if (!prefixed && /^(thread|channel|group|dm):/iu.test(normalized)) {
throw new Error(`qa-channel target prefixes must be lowercase: ${normalized}`);
}
const prefix = prefixed?.[1];
const rest = prefixed?.[2]?.trim();
if (prefix === "thread") {
if (!rest) {
throw new Error(`invalid qa-channel thread target: ${normalized}`);
}
const slashIndex = rest.indexOf("/");
if (slashIndex <= 0 || slashIndex === rest.length - 1) {
throw new Error(`invalid qa-channel thread target: ${normalized}`);
}
const conversationId = rest.slice(0, slashIndex).trim();
const threadId = rest.slice(slashIndex + 1).trim();
if (!conversationId || !threadId) {
throw new Error(`invalid qa-channel thread target: ${normalized}`);
}
return {
chatType: "channel",
conversationId,
threadId,
};
}
if (prefix) {
if (!rest) {
throw new Error(`invalid qa-channel ${prefix} target: ${normalized}`);
}
return {
chatType: prefix === "dm" ? "direct" : prefix === "group" ? "group" : "channel",
conversationId: rest,
};
}
return {
chatType: "direct",
conversationId: normalized,
};
}
const TOOL_CALL_MAX_COUNT = 50;
const TOOL_CALL_MAX_DEPTH = 4;
const TOOL_CALL_MAX_ARRAY_LENGTH = 20;
const TOOL_CALL_MAX_OBJECT_KEYS = 40;
const TOOL_CALL_REDACTED = "[redacted]";
const TOOL_CALL_SENSITIVE_KEY_RE =
/authorization|cookie|credential|password|secret|token|api[-_]?key|access[-_]?key|private[-_]?key/iu;
function sanitizeToolCallValue(value: unknown, depth: number, key?: string): unknown {
if (key && TOOL_CALL_SENSITIVE_KEY_RE.test(key)) {
return TOOL_CALL_REDACTED;
}
if (value === null || typeof value === "boolean" || typeof value === "number") {
return Number.isFinite(value as number) || typeof value !== "number" ? value : String(value);
}
if (typeof value === "string") {
return TOOL_CALL_REDACTED;
}
if (typeof value === "bigint") {
return value.toString();
}
if (value === undefined || typeof value === "function" || typeof value === "symbol") {
return undefined;
}
if (depth >= TOOL_CALL_MAX_DEPTH) {
return "[truncated]";
}
if (Array.isArray(value)) {
return value.slice(0, TOOL_CALL_MAX_ARRAY_LENGTH).map((entry) => {
return sanitizeToolCallValue(entry, depth + 1);
});
}
if (isRecord(value)) {
return Object.fromEntries(
Object.entries(value)
.slice(0, TOOL_CALL_MAX_OBJECT_KEYS)
.flatMap(([entryKey, entryValue]) => {
const sanitized = sanitizeToolCallValue(entryValue, depth + 1, entryKey);
return sanitized === undefined ? [] : [[entryKey, sanitized]];
}),
);
}
return undefined;
}
function sanitizeToolCallArguments(value: unknown): Record<string, unknown> | undefined {
if (!isRecord(value)) {
return undefined;
}
const sanitized = sanitizeToolCallValue(value, 0);
return isRecord(sanitized) ? sanitized : undefined;
}
export function sanitizeQaBusToolCalls(value: unknown): QaBusToolCall[] | undefined {
if (!Array.isArray(value)) {
return undefined;
}
const sanitized = value.slice(0, TOOL_CALL_MAX_COUNT).flatMap((toolCall) => {
if (!isRecord(toolCall)) {
return [];
}
const name = typeof toolCall.name === "string" ? toolCall.name.trim() : "";
if (!name) {
return [];
}
const args = sanitizeToolCallArguments(toolCall.arguments);
return [
{
name,
...(args && Object.keys(args).length > 0 ? { arguments: args } : {}),
},
];
});
return sanitized.length > 0 ? sanitized : undefined;
}
@@ -18,7 +18,6 @@ import type {
QaTransportPolicy,
QaTransportReportParams,
} from "./qa-transport.js";
import { qaChannelPlugin } from "./runtime-api.js";
const QA_CHANNEL_ID = "qa-channel";
const QA_CHANNEL_ACCOUNT_ID = "default";
@@ -142,6 +141,7 @@ async function handleQaChannelAction(params: {
cfg: OpenClawConfig;
accountId?: string | null;
}) {
const { qaChannelPlugin } = await import("openclaw/plugin-sdk/qa-channel");
return await qaChannelPlugin.actions?.handleAction?.({
channel: QA_CHANNEL_ID,
action: params.action,
@@ -86,7 +86,6 @@ function createDeps(overrides?: Partial<QaScenarioRuntimeDeps>): QaScenarioRunti
liveTurnTimeoutMs: fn,
resolveQaLiveTurnTimeoutMs: fn,
splitModelRef: fn,
qaChannelPlugin: { id: "qa-channel" },
hasDiscoveryLabels: fn,
reportsDiscoveryScopeLeak: fn,
reportsMissingDiscoveryFiles: fn,
@@ -98,7 +98,6 @@ type QaScenarioRuntimeDeps = {
liveTurnTimeoutMs: QaScenarioRuntimeFunction;
resolveQaLiveTurnTimeoutMs: QaScenarioRuntimeFunction;
splitModelRef: QaScenarioRuntimeFunction;
qaChannelPlugin: unknown;
hasDiscoveryLabels: QaScenarioRuntimeFunction;
reportsDiscoveryScopeLeak: QaScenarioRuntimeFunction;
reportsMissingDiscoveryFiles: QaScenarioRuntimeFunction;
@@ -195,7 +194,6 @@ type QaScenarioRuntimeApi<
liveTurnTimeoutMs: TDeps["liveTurnTimeoutMs"];
resolveQaLiveTurnTimeoutMs: TDeps["resolveQaLiveTurnTimeoutMs"];
splitModelRef: TDeps["splitModelRef"];
qaChannelPlugin: unknown;
hasDiscoveryLabels: TDeps["hasDiscoveryLabels"];
reportsDiscoveryScopeLeak: TDeps["reportsDiscoveryScopeLeak"];
reportsMissingDiscoveryFiles: TDeps["reportsMissingDiscoveryFiles"];
@@ -309,7 +307,6 @@ export function createQaScenarioRuntimeApi<
liveTurnTimeoutMs: params.deps.liveTurnTimeoutMs,
resolveQaLiveTurnTimeoutMs: params.deps.resolveQaLiveTurnTimeoutMs,
splitModelRef: params.deps.splitModelRef,
qaChannelPlugin: params.deps.qaChannelPlugin,
hasDiscoveryLabels: params.deps.hasDiscoveryLabels,
reportsDiscoveryScopeLeak: params.deps.reportsDiscoveryScopeLeak,
reportsMissingDiscoveryFiles: params.deps.reportsMissingDiscoveryFiles,
@@ -27,7 +27,6 @@ import { extractQaToolPayload } from "./extract-tool-payload.js";
import { assertNoGatewayLogSentinels, scanGatewayLogSentinels } from "./gateway-log-sentinel.js";
import { resolveQaLiveTurnTimeoutMs } from "./live-timeout.js";
import { hasModelSwitchContinuitySignal } from "./model-switch-eval.js";
import { qaChannelPlugin } from "./runtime-api.js";
import { runRuntimeToolFixture } from "./runtime-tool-fixture.js";
import type { QaSeedScenarioWithSource } from "./scenario-catalog.js";
import { runScenarioFlow } from "./scenario-flow-runner.js";
@@ -291,7 +290,6 @@ function createQaSuiteScenarioDeps(params: QaSuiteScenarioDepsParams) {
liveTurnTimeoutMs: params.liveTurnTimeoutMs,
resolveQaLiveTurnTimeoutMs: params.resolveQaLiveTurnTimeoutMs,
splitModelRef: params.splitModelRef,
qaChannelPlugin,
hasDiscoveryLabels,
reportsDiscoveryScopeLeak,
reportsMissingDiscoveryFiles,
+4 -2
View File
@@ -18,7 +18,8 @@ case "$OUTPUT_DIR" in
/*) OUTPUT_DIR_HOST="$OUTPUT_DIR" ;;
*) OUTPUT_DIR_HOST="$ROOT_DIR/$OUTPUT_DIR" ;;
esac
OUTPUT_DIR_CONTAINER="/app/.artifacts/qa-e2e/npm-telegram-live-output"
OUTPUT_DIR_CONTAINER_RELATIVE=".artifacts/qa-e2e/npm-telegram-live-output"
OUTPUT_DIR_CONTAINER="/app/$OUTPUT_DIR_CONTAINER_RELATIVE"
resolve_credential_source() {
if [ -n "${OPENCLAW_NPM_TELEGRAM_CREDENTIAL_SOURCE:-}" ]; then
@@ -220,7 +221,7 @@ docker_env=(
-e TMPDIR=/tmp
-e OPENCLAW_NPM_TELEGRAM_PACKAGE_SPEC="$PACKAGE_SPEC"
-e OPENCLAW_NPM_TELEGRAM_PACKAGE_LABEL="$PACKAGE_LABEL"
-e OPENCLAW_NPM_TELEGRAM_OUTPUT_DIR="$OUTPUT_DIR_CONTAINER"
-e OPENCLAW_NPM_TELEGRAM_OUTPUT_DIR="$OUTPUT_DIR_CONTAINER_RELATIVE"
-e OPENCLAW_QA_PACKAGE_SOURCE="$package_install_source"
-e OPENCLAW_QA_PACKAGE_SOURCE_KIND="$package_source_kind"
-e OPENCLAW_QA_RUNNER="${OPENCLAW_QA_RUNNER:-docker}"
@@ -416,6 +417,7 @@ run_logged docker_e2e_run_with_harness \
-v "$ROOT_DIR/.artifacts:/app/.artifacts" \
-v "$OUTPUT_DIR_HOST:$OUTPUT_DIR_CONTAINER" \
-v "$ROOT_DIR/extensions/qa-lab:/app/extensions/qa-lab:ro" \
-v "$ROOT_DIR/qa/scenarios:/app/qa/scenarios:ro" \
-v "$npm_prefix_host:/npm-global" \
-i "$IMAGE_NAME" bash -s <<'EOF'
set -euo pipefail
+10 -3
View File
@@ -156,9 +156,12 @@ describe("package Telegram live Docker E2E", () => {
'OUTPUT_DIR="${OPENCLAW_NPM_TELEGRAM_OUTPUT_DIR:-.artifacts/qa-e2e/npm-telegram-live/$RUN_ID}"',
);
expect(script).toContain(
'OUTPUT_DIR_CONTAINER="/app/.artifacts/qa-e2e/npm-telegram-live-output"',
'OUTPUT_DIR_CONTAINER_RELATIVE=".artifacts/qa-e2e/npm-telegram-live-output"',
);
expect(script).toContain('OUTPUT_DIR_CONTAINER="/app/$OUTPUT_DIR_CONTAINER_RELATIVE"');
expect(script).toContain(
'-e OPENCLAW_NPM_TELEGRAM_OUTPUT_DIR="$OUTPUT_DIR_CONTAINER_RELATIVE"',
);
expect(script).toContain('-e OPENCLAW_NPM_TELEGRAM_OUTPUT_DIR="$OUTPUT_DIR_CONTAINER"');
expect(script).not.toContain(
'OUTPUT_DIR="${OPENCLAW_NPM_TELEGRAM_OUTPUT_DIR:-.artifacts/qa-e2e/npm-telegram-live}"',
);
@@ -204,7 +207,10 @@ describe("package Telegram live Docker E2E", () => {
expect(script).toContain('*) OUTPUT_DIR_HOST="$ROOT_DIR/$OUTPUT_DIR" ;;');
expect(script).toContain('mkdir -p "$OUTPUT_DIR_HOST"');
expect(dockerEnv).toContain('-e OPENCLAW_NPM_TELEGRAM_OUTPUT_DIR="$OUTPUT_DIR_CONTAINER"');
expect(dockerEnv).toContain(
'-e OPENCLAW_NPM_TELEGRAM_OUTPUT_DIR="$OUTPUT_DIR_CONTAINER_RELATIVE"',
);
expect(dockerEnv).not.toContain('-e OPENCLAW_NPM_TELEGRAM_OUTPUT_DIR="$OUTPUT_DIR_CONTAINER"');
expect(dockerEnv).not.toContain('-e OPENCLAW_NPM_TELEGRAM_OUTPUT_DIR="$OUTPUT_DIR"');
expect(script).toContain('-v "$OUTPUT_DIR_HOST:$OUTPUT_DIR_CONTAINER"');
});
@@ -251,6 +257,7 @@ describe("package Telegram live Docker E2E", () => {
expect(script).toContain('ln -sfnT "$openclaw_package_dir/dist" /app/dist');
expect(script).toContain('cp "$openclaw_package_dir/package.json" /app/package.json');
expect(script).toContain('-v "$ROOT_DIR/extensions/qa-lab:/app/extensions/qa-lab:ro"');
expect(script).toContain('-v "$ROOT_DIR/qa/scenarios:/app/qa/scenarios:ro"');
expect(script).not.toContain('ln -sfnT /app/extensions "$openclaw_package_dir/extensions"');
expect(script).toContain("node scripts/e2e/lib/npm-telegram-live/prepare-package.mjs");
expect(script).toContain("/app/node_modules/openclaw/package.json");