diff --git a/scripts/e2e/kitchen-sink-rpc-walk.mjs b/scripts/e2e/kitchen-sink-rpc-walk.mjs index 5428aa392d0..0e8982b9a12 100644 --- a/scripts/e2e/kitchen-sink-rpc-walk.mjs +++ b/scripts/e2e/kitchen-sink-rpc-walk.mjs @@ -227,36 +227,33 @@ function unwrapRpcPayload(raw) { } async function rpcCall(method, params, options) { - const { callGateway } = await loadCallGatewayModule(options.runner); - const payload = await callGateway({ - config: readJson(options.env.OPENCLAW_CONFIG_PATH), - configPath: options.env.OPENCLAW_CONFIG_PATH, - url: `ws://127.0.0.1:${options.port}`, - token: TOKEN, - method, - params: params ?? {}, - timeoutMs: RPC_TIMEOUT_MS, - requiredMethods: [method], - }); + const module = await loadCallGatewayModule(options.runner); + const payload = module + ? await module.callGateway({ + config: readJson(options.env.OPENCLAW_CONFIG_PATH), + configPath: options.env.OPENCLAW_CONFIG_PATH, + url: `ws://127.0.0.1:${options.port}`, + token: TOKEN, + method, + params: params ?? {}, + timeoutMs: RPC_TIMEOUT_MS, + requiredMethods: [method], + }) + : await rpcCallViaCli(method, params, options); return unwrapRpcPayload(payload); } async function loadCallGatewayModule(runner) { - callGatewayModulePromise ??= importCallGatewayModule(runner); + if (!usesBuiltOpenClawEntry(runner)) { + return null; + } + callGatewayModulePromise ??= importCallGatewayModule(); return callGatewayModulePromise; } -async function importCallGatewayModule(runner) { - if (!usesPackagedOpenClawEntry(runner)) { - return import(pathToFileURL(path.join(process.cwd(), "src/gateway/call.ts")).href); - } +async function importCallGatewayModule() { const distDir = path.join(process.cwd(), "dist"); - const candidates = fs.existsSync(distDir) - ? fs - .readdirSync(distDir) - .filter((name) => /^call(?:\.runtime)?-[A-Za-z0-9_-]+\.js$/u.test(name)) - .toSorted((left, right) => left.localeCompare(right)) - : []; + const candidates = findDistCallGatewayModuleFiles(); for (const name of candidates) { const module = await import(pathToFileURL(path.join(distDir, name)).href); if (typeof module.callGateway === "function") { @@ -266,10 +263,49 @@ async function importCallGatewayModule(runner) { throw new Error(`unable to find callGateway export in dist (${candidates.join(", ")})`); } -function usesPackagedOpenClawEntry(runner) { - return Boolean( - process.env.OPENCLAW_ENTRY && runner?.baseArgs?.[0] === process.env.OPENCLAW_ENTRY, +async function rpcCallViaCli(method, params, options) { + const { stdout } = await runOpenClaw( + options.runner, + [ + "gateway", + "call", + method, + "--url", + `ws://127.0.0.1:${options.port}`, + "--token", + TOKEN, + "--timeout", + String(RPC_TIMEOUT_MS), + "--json", + "--params", + JSON.stringify(params ?? {}), + ], + options.env, + { timeoutMs: RPC_TIMEOUT_MS + 30000 }, ); + return parseJsonOutput(stdout); +} + +export function findDistCallGatewayModuleFiles(cwd = process.cwd()) { + const distDir = path.join(cwd, "dist"); + return fs.existsSync(distDir) + ? fs + .readdirSync(distDir) + .filter((name) => /^call(?:\.runtime)?-[A-Za-z0-9_-]+\.js$/u.test(name)) + .toSorted((left, right) => left.localeCompare(right)) + : []; +} + +export function usesBuiltOpenClawEntry(runner, cwd = process.cwd(), env = process.env) { + if (runner?.pnpm || !runner?.baseArgs?.[0]) { + return false; + } + const entry = runner.baseArgs[0]; + if (env.OPENCLAW_ENTRY && entry === env.OPENCLAW_ENTRY) { + return true; + } + const relative = path.relative(path.resolve(cwd, "dist"), path.resolve(cwd, entry)); + return relative.length > 0 && !relative.startsWith("..") && !path.isAbsolute(relative); } async function retryRpcCall(method, params, options) { diff --git a/test/scripts/kitchen-sink-rpc-walk.test.ts b/test/scripts/kitchen-sink-rpc-walk.test.ts index 419672d0d9b..7881365fdd2 100644 --- a/test/scripts/kitchen-sink-rpc-walk.test.ts +++ b/test/scripts/kitchen-sink-rpc-walk.test.ts @@ -1,12 +1,52 @@ +import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs"; +import { tmpdir } from "node:os"; +import path from "node:path"; import { describe, expect, it, vi } from "vitest"; import { assertResourceCeiling, fetchJson, + findDistCallGatewayModuleFiles, sampleProcess, sampleWindowsProcessByPort, summarizeProcessSamples, + usesBuiltOpenClawEntry, } from "../../scripts/e2e/kitchen-sink-rpc-walk.mjs"; +describe("kitchen-sink RPC caller loading", () => { + it("uses built callGateway chunks for dist and packaged entries", () => { + expect(usesBuiltOpenClawEntry({ command: "node", baseArgs: ["dist/index.js"] })).toBe(true); + expect( + usesBuiltOpenClawEntry( + { command: "node", baseArgs: ["/app/openclaw.mjs"] }, + "/repo", + { OPENCLAW_ENTRY: "/app/openclaw.mjs" }, + ), + ).toBe(true); + }); + + it("does not deep-import gateway TypeScript for source pnpm runners", () => { + expect(usesBuiltOpenClawEntry({ pnpm: true, baseArgs: ["openclaw"] })).toBe(false); + expect(usesBuiltOpenClawEntry({ command: "node", baseArgs: ["scripts/dev.mjs"] })).toBe(false); + }); + + it("finds only built callGateway chunks", () => { + const root = mkdtempSync(path.join(tmpdir(), "openclaw-rpc-call-chunks-")); + try { + mkdirSync(path.join(root, "dist")); + writeFileSync(path.join(root, "dist", "call-Abc123.js"), ""); + writeFileSync(path.join(root, "dist", "call.runtime-Def456.js"), ""); + writeFileSync(path.join(root, "dist", "index.js"), ""); + + expect(findDistCallGatewayModuleFiles(root)).toEqual([ + "call-Abc123.js", + "call.runtime-Def456.js", + ]); + } finally { + rmSync(root, { recursive: true, force: true }); + } + }); +}); + describe("kitchen-sink RPC process sampling", () => { it("samples RSS on Windows instead of silently disabling the resource guard", async () => { const calls: Array<{ command: string; args: string[] }> = []; diff --git a/test/scripts/plugin-prerelease-test-plan.test.ts b/test/scripts/plugin-prerelease-test-plan.test.ts index 5d9acbbd470..e10671ce5b7 100644 --- a/test/scripts/plugin-prerelease-test-plan.test.ts +++ b/test/scripts/plugin-prerelease-test-plan.test.ts @@ -189,8 +189,10 @@ describe("scripts/lib/plugin-prerelease-test-plan.mjs", () => { expect(walkScript).toContain("tts.providers"); expect(walkScript).toContain("plugins.uiDescriptors"); expect(walkScript).toContain("loadCallGatewayModule(options.runner)"); - expect(walkScript).toContain("usesPackagedOpenClawEntry(runner)"); - expect(walkScript).toContain("src/gateway/call.ts"); + expect(walkScript).toContain("usesBuiltOpenClawEntry(runner)"); + expect(walkScript).toContain('"gateway"'); + expect(walkScript).toContain('"call"'); + expect(walkScript).not.toContain("src/gateway/call.ts"); expect(walkScript).toContain("^call(?:\\.runtime)?"); });