diff --git a/CHANGELOG.md b/CHANGELOG.md index c58b9d4160e..eef46588432 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,6 +21,7 @@ Docs: https://docs.openclaw.ai - Gateway/config: stop Gateway startup and hot reload from auto-restoring invalid config; invalid config now fails closed and `openclaw doctor --fix` owns last-known-good repair. - Gateway/performance: lazy-load early runtime discovery and shutdown-hook helpers, defer maintenance timers until after readiness, and trim duplicate plugin auto-enable work during Gateway startup. - QA/Mantis: add a `pnpm openclaw qa mantis discord-smoke` runner and manual GitHub workflow that verify the Mantis Discord bot can see the configured guild/channel, post a smoke message, add a reaction, and upload artifacts. +- QA/Mantis: pass the runtime env through desktop-browser Crabbox and artifact-copy child commands, so embedded Mantis callers can provide Crabbox credentials without mutating the parent process. Thanks @vincentkoc. - QA/Slack: add a Slack live transport QA runner with canary and mention-gating coverage for the private bot-to-bot harness. Thanks @vincentkoc. - Gateway/performance: lazy-load the heavy cron runtime after the rest of Gateway startup, defer restart-sentinel refresh after readiness, and let the Gateway startup benchmark write per-run V8 CPU profiles with `--cpu-prof-dir`. - Gateway/performance: keep raw channel-config schema parsing from discovering bundled plugin runtime metadata, and add `pnpm gateway:watch --benchmark-no-force` for profiling startup without the default port cleanup. diff --git a/extensions/qa-lab/src/mantis/desktop-browser-smoke.runtime.test.ts b/extensions/qa-lab/src/mantis/desktop-browser-smoke.runtime.test.ts index 4c4bf9fd253..d2c6623555e 100644 --- a/extensions/qa-lab/src/mantis/desktop-browser-smoke.runtime.test.ts +++ b/extensions/qa-lab/src/mantis/desktop-browser-smoke.runtime.test.ts @@ -18,43 +18,51 @@ describe("mantis desktop browser smoke runtime", () => { it("leases a desktop box, runs a visible browser, copies artifacts, and stops on pass", async () => { await fs.mkdir(path.join(repoRoot, "qa-artifacts"), { recursive: true }); await fs.writeFile(path.join(repoRoot, "qa-artifacts", "timeline.html"), "

Mantis

"); - const commands: { args: readonly string[]; command: string }[] = []; - const runner = vi.fn(async (command: string, args: readonly string[]) => { - commands.push({ command, args }); - if (command === "/tmp/crabbox" && args[0] === "warmup") { - return { stdout: "ready lease cbx_abc123\n", stderr: "" }; - } - if (command === "/tmp/crabbox" && args[0] === "inspect") { - return { - stdout: `${JSON.stringify({ - host: "203.0.113.10", - id: "cbx_abc123", - provider: "hetzner", - slug: "brisk-mantis", - sshKey: "/tmp/key", - sshPort: "2222", - sshUser: "crabbox", - state: "active", - })}\n`, - stderr: "", - }; - } - if (command === "rsync") { - const outputDir = args.at(-1); - expect(outputDir).toBeTypeOf("string"); - await fs.mkdir(outputDir as string, { recursive: true }); - await fs.writeFile(path.join(outputDir as string, "desktop-browser-smoke.png"), "png"); - await fs.writeFile(path.join(outputDir as string, "remote-metadata.json"), "{}\n"); - await fs.writeFile(path.join(outputDir as string, "chrome.log"), "chrome\n"); + const commands: { args: readonly string[]; command: string; env?: NodeJS.ProcessEnv }[] = []; + const runtimeEnv = { + PATH: process.env.PATH, + CRABBOX_COORDINATOR_TOKEN: "runtime-token", + OPENCLAW_MANTIS_CRABBOX_PROVIDER: "hetzner", + }; + const runner = vi.fn( + async (command: string, args: readonly string[], options: { env?: NodeJS.ProcessEnv }) => { + commands.push({ command, args, env: options.env }); + if (command === "/tmp/crabbox" && args[0] === "warmup") { + return { stdout: "ready lease cbx_abc123\n", stderr: "" }; + } + if (command === "/tmp/crabbox" && args[0] === "inspect") { + return { + stdout: `${JSON.stringify({ + host: "203.0.113.10", + id: "cbx_abc123", + provider: "hetzner", + slug: "brisk-mantis", + sshKey: "/tmp/key", + sshPort: "2222", + sshUser: "crabbox", + state: "active", + })}\n`, + stderr: "", + }; + } + if (command === "rsync") { + const outputDir = args.at(-1); + expect(outputDir).toBeTypeOf("string"); + await fs.mkdir(outputDir as string, { recursive: true }); + await fs.writeFile(path.join(outputDir as string, "desktop-browser-smoke.png"), "png"); + await fs.writeFile(path.join(outputDir as string, "remote-metadata.json"), "{}\n"); + await fs.writeFile(path.join(outputDir as string, "chrome.log"), "chrome\n"); + return { stdout: "", stderr: "" }; + } return { stdout: "", stderr: "" }; - } - return { stdout: "", stderr: "" }; - }); + }, + ); const result = await runMantisDesktopBrowserSmoke({ browserUrl: "https://openclaw.ai/docs", commandRunner: runner, crabboxBin: "/tmp/crabbox", + env: runtimeEnv, htmlFile: "qa-artifacts/timeline.html", now: () => new Date("2026-05-04T12:00:00.000Z"), outputDir: ".artifacts/qa-e2e/mantis/desktop-browser-test", @@ -69,6 +77,7 @@ describe("mantis desktop browser smoke runtime", () => { ["rsync", "-az"], ["/tmp/crabbox", "stop"], ]); + expect(commands.every((entry) => entry.env === runtimeEnv)).toBe(true); const rsyncArgs = commands.find((entry) => entry.command === "rsync")?.args ?? []; expect(rsyncArgs).not.toContain("--delete"); expect(rsyncArgs).toEqual( diff --git a/extensions/qa-lab/src/mantis/desktop-browser-smoke.runtime.ts b/extensions/qa-lab/src/mantis/desktop-browser-smoke.runtime.ts index 6c4da15ed38..81c531a3c4d 100644 --- a/extensions/qa-lab/src/mantis/desktop-browser-smoke.runtime.ts +++ b/extensions/qa-lab/src/mantis/desktop-browser-smoke.runtime.ts @@ -303,12 +303,13 @@ async function runCommand(params: { args: readonly string[]; command: string; cwd: string; + env: NodeJS.ProcessEnv; runner: CommandRunner; stdio?: "inherit" | "pipe"; }) { return params.runner(params.command, params.args, { cwd: params.cwd, - env: process.env, + env: params.env, stdio: params.stdio ?? "pipe", }); } @@ -316,6 +317,7 @@ async function runCommand(params: { async function warmupCrabbox(params: { crabboxBin: string; cwd: string; + env: NodeJS.ProcessEnv; idleTimeout: string; machineClass: string; provider: string; @@ -338,6 +340,7 @@ async function warmupCrabbox(params: { params.ttl, ], cwd: params.cwd, + env: params.env, runner: params.runner, stdio: "inherit", }); @@ -351,6 +354,7 @@ async function warmupCrabbox(params: { async function inspectCrabbox(params: { crabboxBin: string; cwd: string; + env: NodeJS.ProcessEnv; leaseId: string; provider: string; runner: CommandRunner; @@ -359,6 +363,7 @@ async function inspectCrabbox(params: { command: params.crabboxBin, args: ["inspect", "--provider", params.provider, "--id", params.leaseId, "--json"], cwd: params.cwd, + env: params.env, runner: params.runner, }); return JSON.parse(result.stdout) as CrabboxInspect; @@ -366,6 +371,7 @@ async function inspectCrabbox(params: { async function copyRemoteArtifacts(params: { cwd: string; + env: NodeJS.ProcessEnv; inspect: CrabboxInspect; outputDir: string; remoteOutputDir: string; @@ -401,6 +407,7 @@ async function copyRemoteArtifacts(params: { `${params.outputDir}/`, ], cwd: params.cwd, + env: params.env, runner: params.runner, }); } @@ -408,6 +415,7 @@ async function copyRemoteArtifacts(params: { async function stopCrabbox(params: { crabboxBin: string; cwd: string; + env: NodeJS.ProcessEnv; leaseId: string; provider: string; runner: CommandRunner; @@ -416,6 +424,7 @@ async function stopCrabbox(params: { command: params.crabboxBin, args: ["stop", "--provider", params.provider, params.leaseId], cwd: params.cwd, + env: params.env, runner: params.runner, stdio: "inherit", }); @@ -471,6 +480,7 @@ export async function runMantisDesktopBrowserSmoke( (await warmupCrabbox({ crabboxBin, cwd: repoRoot, + env, idleTimeout, machineClass, provider, @@ -480,6 +490,7 @@ export async function runMantisDesktopBrowserSmoke( const inspected = await inspectCrabbox({ crabboxBin, cwd: repoRoot, + env, leaseId, provider, runner, @@ -500,11 +511,13 @@ export async function runMantisDesktopBrowserSmoke( renderRemoteScript({ browserUrl, htmlBase64, remoteOutputDir }), ], cwd: repoRoot, + env, runner, stdio: "inherit", }); await copyRemoteArtifacts({ cwd: repoRoot, + env, inspect: inspected, outputDir, remoteOutputDir, @@ -582,7 +595,7 @@ export async function runMantisDesktopBrowserSmoke( await fs.writeFile(reportPath, renderReport(summary), "utf8"); } if (summary?.status === "pass" && createdLease && leaseId && !keepLease) { - await stopCrabbox({ crabboxBin, cwd: repoRoot, leaseId, provider, runner }); + await stopCrabbox({ crabboxBin, cwd: repoRoot, env, leaseId, provider, runner }); } } }