diff --git a/scripts/e2e/npm-telegram-live-docker.sh b/scripts/e2e/npm-telegram-live-docker.sh index f121dcc325d..5bfac93c3b2 100755 --- a/scripts/e2e/npm-telegram-live-docker.sh +++ b/scripts/e2e/npm-telegram-live-docker.sh @@ -227,6 +227,25 @@ openclaw_package_dir="/npm-global/lib/node_modules/openclaw" # point those imports at the installed package without copying source into the test image. rm -rf /app/node_modules/openclaw ln -sfnT "$openclaw_package_dir" /app/node_modules/openclaw +rm -rf /app/dist +ln -sfnT "$openclaw_package_dir/dist" /app/dist +cp "$openclaw_package_dir/package.json" /app/package.json +node --input-type=module <<'NODE' +import fs from "node:fs"; + +const packageJsonPath = "/app/package.json"; +const pkg = JSON.parse(fs.readFileSync(packageJsonPath, "utf8")); +pkg.exports = pkg.exports && typeof pkg.exports === "object" ? pkg.exports : {}; +pkg.exports["./plugin-sdk/qa-channel"] = { + types: "./extensions/qa-channel/api.ts", + default: "./extensions/qa-channel/api.ts", +}; +pkg.exports["./plugin-sdk/qa-channel-protocol"] = { + types: "./extensions/qa-channel/src/protocol.ts", + default: "./extensions/qa-channel/src/protocol.ts", +}; +fs.writeFileSync(packageJsonPath, `${JSON.stringify(pkg, null, 2)}\n`); +NODE for deps_dir in "$openclaw_package_dir/node_modules" /npm-global/lib/node_modules; do [ -d "$deps_dir" ] || continue for dependency_dir in "$deps_dir"/*; do diff --git a/scripts/openclaw-cross-os-release-checks.ts b/scripts/openclaw-cross-os-release-checks.ts index ab98fc71bbe..92cb785cabc 100644 --- a/scripts/openclaw-cross-os-release-checks.ts +++ b/scripts/openclaw-cross-os-release-checks.ts @@ -60,6 +60,9 @@ const OMITTED_QA_EXTENSION_PREFIXES = [ ]; export const CROSS_OS_DASHBOARD_SMOKE_TIMEOUT_MS = 120_000; export const CROSS_OS_DASHBOARD_FETCH_TIMEOUT_MS = 10_000; +export const CROSS_OS_GATEWAY_STATUS_RPC_TIMEOUT_MS = 30_000; +export const CROSS_OS_GATEWAY_READY_TIMEOUT_MS = 3 * 60_000; +export const CROSS_OS_WINDOWS_GATEWAY_READY_TIMEOUT_MS = 5 * 60_000; if (isMainModule()) { try { @@ -1629,7 +1632,14 @@ async function resolveInstalledGatewayStatusArgs(params) { requireRpc && (help.stdout.includes("--require-rpc") || help.stderr.includes("--require-rpc")) ) { - return ["gateway", "status", "--deep", "--require-rpc", "--timeout", "5000"]; + return [ + "gateway", + "status", + "--deep", + "--require-rpc", + "--timeout", + String(CROSS_OS_GATEWAY_STATUS_RPC_TIMEOUT_MS), + ]; } return ["gateway", "status", "--deep"]; } @@ -2370,7 +2380,9 @@ async function waitForGateway(params) { } function gatewayReadyDeadlineMs() { - return process.platform === "win32" ? 5 * 60 * 1000 : 90_000; + return process.platform === "win32" + ? CROSS_OS_WINDOWS_GATEWAY_READY_TIMEOUT_MS + : CROSS_OS_GATEWAY_READY_TIMEOUT_MS; } async function resolveGatewayStatusArgs(lane, env, logPath) { @@ -2383,7 +2395,14 @@ async function resolveGatewayStatusArgs(lane, env, logPath) { check: false, }); if (help.stdout.includes("--require-rpc") || help.stderr.includes("--require-rpc")) { - return ["gateway", "status", "--deep", "--require-rpc", "--timeout", "5000"]; + return [ + "gateway", + "status", + "--deep", + "--require-rpc", + "--timeout", + String(CROSS_OS_GATEWAY_STATUS_RPC_TIMEOUT_MS), + ]; } return ["gateway", "status", "--deep"]; } diff --git a/src/plugins/bundled-runtime-root.test.ts b/src/plugins/bundled-runtime-root.test.ts index 3c4f56f89f3..b045f20b756 100644 --- a/src/plugins/bundled-runtime-root.test.ts +++ b/src/plugins/bundled-runtime-root.test.ts @@ -107,4 +107,56 @@ describe("prepareBundledPluginRuntimeRoot", () => { expect(fs.lstatSync(staleMirrorChunk).isSymbolicLink()).toBe(false); expect(fs.readFileSync(staleMirrorChunk, "utf8")).toContain("playwright-core"); }); + + it("does not copy staged runtime mirror dist files onto themselves", () => { + const stageDir = makeTempRoot(); + const installRoot = path.join(stageDir, "openclaw-2026.4.26-alpha"); + const pluginRoot = path.join(installRoot, "dist", "extensions", "qqbot"); + const distChunk = path.join(installRoot, "dist", "accounts-abc123.js"); + const env = { ...process.env, OPENCLAW_PLUGIN_STAGE_DIR: stageDir }; + fs.mkdirSync(pluginRoot, { recursive: true }); + fs.writeFileSync( + path.join(installRoot, "package.json"), + JSON.stringify({ name: "openclaw", version: "2026.4.26", type: "module" }), + "utf8", + ); + fs.writeFileSync(distChunk, "export const marker = 'same-root';\n", "utf8"); + fs.writeFileSync( + path.join(pluginRoot, "index.js"), + `import { marker } from "../../accounts-abc123.js"; export default { id: "qqbot", marker };\n`, + "utf8", + ); + fs.writeFileSync( + path.join(pluginRoot, "package.json"), + JSON.stringify( + { + name: "@openclaw/qqbot", + version: "1.0.0", + type: "module", + dependencies: { "qqbot-runtime": "1.0.0" }, + openclaw: { extensions: ["./index.js"] }, + }, + null, + 2, + ), + "utf8", + ); + fs.mkdirSync(path.join(installRoot, "node_modules", "qqbot-runtime"), { recursive: true }); + fs.writeFileSync( + path.join(installRoot, "node_modules", "qqbot-runtime", "package.json"), + JSON.stringify({ name: "qqbot-runtime", version: "1.0.0", type: "module" }), + "utf8", + ); + + const prepared = prepareBundledPluginRuntimeRoot({ + pluginId: "qqbot", + pluginRoot, + modulePath: path.join(pluginRoot, "index.js"), + env, + }); + + expect(prepared.pluginRoot).toBe(pluginRoot); + expect(prepared.modulePath).toBe(path.join(pluginRoot, "index.js")); + expect(fs.readFileSync(distChunk, "utf8")).toContain("same-root"); + }); }); diff --git a/src/plugins/bundled-runtime-root.ts b/src/plugins/bundled-runtime-root.ts index 0cae8a49c00..8ade46a01f8 100644 --- a/src/plugins/bundled-runtime-root.ts +++ b/src/plugins/bundled-runtime-root.ts @@ -142,6 +142,9 @@ function prepareBundledPluginRuntimeDistMirror(params: { } const sourcePath = path.join(sourceDistRoot, entry.name); const targetPath = path.join(mirrorDistRoot, entry.name); + if (path.resolve(sourcePath) === path.resolve(targetPath)) { + continue; + } if (entry.isFile() && shouldMaterializeBundledRuntimeMirrorDistFile(sourcePath)) { materializeBundledRuntimeMirrorDistFile(sourcePath, targetPath); continue; @@ -175,6 +178,9 @@ function ensureBundledRuntimeDistPackageJson(mirrorDistRoot: string): void { } function copyBundledPluginRuntimeRoot(sourceRoot: string, targetRoot: string): void { + if (path.resolve(sourceRoot) === path.resolve(targetRoot)) { + return; + } fs.mkdirSync(targetRoot, { recursive: true, mode: 0o755 }); for (const entry of fs.readdirSync(sourceRoot, { withFileTypes: true })) { if (entry.name === "node_modules") { diff --git a/src/plugins/loader.ts b/src/plugins/loader.ts index fb52fa311d2..5f9788fc910 100644 --- a/src/plugins/loader.ts +++ b/src/plugins/loader.ts @@ -791,6 +791,9 @@ function mirrorBundledRuntimeDistRootEntries(params: { } const sourcePath = path.join(params.sourceDistRoot, entry.name); const targetPath = path.join(params.mirrorDistRoot, entry.name); + if (path.resolve(sourcePath) === path.resolve(targetPath)) { + continue; + } if (entry.isFile() && shouldMaterializeBundledRuntimeMirrorDistFile(sourcePath)) { materializeBundledRuntimeMirrorDistFile(sourcePath, targetPath); continue; @@ -860,6 +863,9 @@ function ensureBundledRuntimeDistPackageJson(mirrorDistRoot: string): void { } function copyBundledPluginRuntimeRoot(sourceRoot: string, targetRoot: string): void { + if (path.resolve(sourceRoot) === path.resolve(targetRoot)) { + return; + } fs.mkdirSync(targetRoot, { recursive: true, mode: 0o755 }); for (const entry of fs.readdirSync(sourceRoot, { withFileTypes: true })) { if (entry.name === "node_modules") { diff --git a/test/scripts/npm-telegram-live.test.ts b/test/scripts/npm-telegram-live.test.ts index 95704c85681..1a0a56176bf 100644 --- a/test/scripts/npm-telegram-live.test.ts +++ b/test/scripts/npm-telegram-live.test.ts @@ -55,6 +55,17 @@ describe("package Telegram live Docker E2E", () => { ); }); + it("keeps private QA harness imports local while using the installed package dist", () => { + const script = readFileSync(DOCKER_SCRIPT_PATH, "utf8"); + + 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('pkg.exports["./plugin-sdk/qa-channel"]'); + expect(script).toContain('"./extensions/qa-channel/api.ts"'); + expect(script).toContain('pkg.exports["./plugin-sdk/qa-channel-protocol"]'); + expect(script).toContain('"./extensions/qa-channel/src/protocol.ts"'); + }); + it("lets npm-specific credential aliases override shared QA env", () => { expect( __testing.resolveCredentialSource({ diff --git a/test/scripts/openclaw-cross-os-release-checks.test.ts b/test/scripts/openclaw-cross-os-release-checks.test.ts index 49c28eba24f..ce8d802d89a 100644 --- a/test/scripts/openclaw-cross-os-release-checks.test.ts +++ b/test/scripts/openclaw-cross-os-release-checks.test.ts @@ -12,6 +12,9 @@ import { canConnectToLoopbackPort, buildDiscordSmokeGuildsConfig, buildRealUpdateEnv, + CROSS_OS_GATEWAY_READY_TIMEOUT_MS, + CROSS_OS_GATEWAY_STATUS_RPC_TIMEOUT_MS, + CROSS_OS_WINDOWS_GATEWAY_READY_TIMEOUT_MS, CROSS_OS_DASHBOARD_FETCH_TIMEOUT_MS, CROSS_OS_DASHBOARD_SMOKE_TIMEOUT_MS, isImmutableReleaseRef, @@ -46,6 +49,12 @@ describe("scripts/openclaw-cross-os-release-checks", () => { expect(CROSS_OS_DASHBOARD_FETCH_TIMEOUT_MS).toBeGreaterThanOrEqual(10_000); }); + it("keeps gateway RPC status probes patient enough for live release startup", () => { + expect(CROSS_OS_GATEWAY_STATUS_RPC_TIMEOUT_MS).toBeGreaterThanOrEqual(30_000); + expect(CROSS_OS_GATEWAY_READY_TIMEOUT_MS).toBeGreaterThanOrEqual(180_000); + expect(CROSS_OS_WINDOWS_GATEWAY_READY_TIMEOUT_MS).toBeGreaterThanOrEqual(300_000); + }); + it("accepts OK agent output from the captured log when stdout is empty", () => { const dir = mkdtempSync(join(tmpdir(), "openclaw-cross-os-agent-output-")); try {