From 154d1277e12e0b69adbc605a7de10de3a61bc921 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Wed, 15 Jul 2026 19:24:55 -0700 Subject: [PATCH] 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 --- extensions/qa-lab/index.test.ts | 32 +++++ extensions/qa-lab/index.ts | 3 +- extensions/qa-lab/src/bus-queries.ts | 2 +- extensions/qa-lab/src/bus-state.ts | 2 +- extensions/qa-lab/src/lab-server.test.ts | 2 +- extensions/qa-lab/src/lab-server.ts | 3 +- extensions/qa-lab/src/qa-bus-protocol.test.ts | 60 ++++++++ extensions/qa-lab/src/qa-bus-protocol.ts | 131 ++++++++++++++++++ extensions/qa-lab/src/qa-channel-transport.ts | 2 +- .../qa-lab/src/scenario-runtime-api.test.ts | 1 - extensions/qa-lab/src/scenario-runtime-api.ts | 3 - extensions/qa-lab/src/suite-runtime-flow.ts | 2 - scripts/e2e/npm-telegram-live-docker.sh | 6 +- test/scripts/npm-telegram-live.test.ts | 13 +- 14 files changed, 245 insertions(+), 17 deletions(-) create mode 100644 extensions/qa-lab/index.test.ts create mode 100644 extensions/qa-lab/src/qa-bus-protocol.test.ts create mode 100644 extensions/qa-lab/src/qa-bus-protocol.ts diff --git a/extensions/qa-lab/index.test.ts b/extensions/qa-lab/index.test.ts new file mode 100644 index 000000000000..db29fde22bf7 --- /dev/null +++ b/extensions/qa-lab/index.test.ts @@ -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(); + }); +}); diff --git a/extensions/qa-lab/index.ts b/extensions/qa-lab/index.ts index d6f51e540e62..27bf9122d780 100644 --- a/extensions/qa-lab/index.ts +++ b/extensions/qa-lab/index.ts @@ -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"; diff --git a/extensions/qa-lab/src/bus-queries.ts b/extensions/qa-lab/src/bus-queries.ts index b3136dbd7c17..2b4983172b8a 100644 --- a/extensions/qa-lab/src/bus-queries.ts +++ b/extensions/qa-lab/src/bus-queries.ts @@ -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, diff --git a/extensions/qa-lab/src/bus-state.ts b/extensions/qa-lab/src/bus-state.ts index bc75aab67d92..790845d381aa 100644 --- a/extensions/qa-lab/src/bus-state.ts +++ b/extensions/qa-lab/src/bus-state.ts @@ -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, diff --git a/extensions/qa-lab/src/lab-server.test.ts b/extensions/qa-lab/src/lab-server.test.ts index f3a042057d6e..88165fb4e526 100644 --- a/extensions/qa-lab/src/lab-server.test.ts +++ b/extensions/qa-lab/src/lab-server.test.ts @@ -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, diff --git a/extensions/qa-lab/src/lab-server.ts b/extensions/qa-lab/src/lab-server.ts index c2a444136cea..ae24eea4ee23 100644 --- a/extensions/qa-lab/src/lab-server.ts +++ b/extensions/qa-lab/src/lab-server.ts @@ -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); diff --git a/extensions/qa-lab/src/qa-bus-protocol.test.ts b/extensions/qa-lab/src/qa-bus-protocol.test.ts new file mode 100644 index 000000000000..b75aea36ae7f --- /dev/null +++ b/extensions/qa-lab/src/qa-bus-protocol.test.ts @@ -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)); + }); +}); diff --git a/extensions/qa-lab/src/qa-bus-protocol.ts b/extensions/qa-lab/src/qa-bus-protocol.ts new file mode 100644 index 000000000000..58048fb42bfe --- /dev/null +++ b/extensions/qa-lab/src/qa-bus-protocol.ts @@ -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 | 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; +} diff --git a/extensions/qa-lab/src/qa-channel-transport.ts b/extensions/qa-lab/src/qa-channel-transport.ts index b2ceee280aaa..9f1c6f23ae1a 100644 --- a/extensions/qa-lab/src/qa-channel-transport.ts +++ b/extensions/qa-lab/src/qa-channel-transport.ts @@ -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, diff --git a/extensions/qa-lab/src/scenario-runtime-api.test.ts b/extensions/qa-lab/src/scenario-runtime-api.test.ts index 98986637df25..cab38ebac02f 100644 --- a/extensions/qa-lab/src/scenario-runtime-api.test.ts +++ b/extensions/qa-lab/src/scenario-runtime-api.test.ts @@ -86,7 +86,6 @@ function createDeps(overrides?: Partial): QaScenarioRunti liveTurnTimeoutMs: fn, resolveQaLiveTurnTimeoutMs: fn, splitModelRef: fn, - qaChannelPlugin: { id: "qa-channel" }, hasDiscoveryLabels: fn, reportsDiscoveryScopeLeak: fn, reportsMissingDiscoveryFiles: fn, diff --git a/extensions/qa-lab/src/scenario-runtime-api.ts b/extensions/qa-lab/src/scenario-runtime-api.ts index cbfdcfce0539..46e1f84b47b4 100644 --- a/extensions/qa-lab/src/scenario-runtime-api.ts +++ b/extensions/qa-lab/src/scenario-runtime-api.ts @@ -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, diff --git a/extensions/qa-lab/src/suite-runtime-flow.ts b/extensions/qa-lab/src/suite-runtime-flow.ts index 232e300ecb13..20012c8c196e 100644 --- a/extensions/qa-lab/src/suite-runtime-flow.ts +++ b/extensions/qa-lab/src/suite-runtime-flow.ts @@ -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, diff --git a/scripts/e2e/npm-telegram-live-docker.sh b/scripts/e2e/npm-telegram-live-docker.sh index bb529eb5f9a4..b467e2157daa 100755 --- a/scripts/e2e/npm-telegram-live-docker.sh +++ b/scripts/e2e/npm-telegram-live-docker.sh @@ -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 diff --git a/test/scripts/npm-telegram-live.test.ts b/test/scripts/npm-telegram-live.test.ts index 52045a1046de..c471414a179f 100644 --- a/test/scripts/npm-telegram-live.test.ts +++ b/test/scripts/npm-telegram-live.test.ts @@ -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");