openclaw/test/scripts/agent-plugin-gateway-e2e.test.ts
Peter Steinberger ee51203de7
fix(test): keep subprocess fixtures consistent across Node and Bun (#163805)
Couple test-worker arguments to the executable selected by the shared subprocess helper and migrate its runtime-dependent callers. Repair command parsing, runtime pinning, worker observation, storage settlement, and cleanup-clock fixtures while preserving their contracts.

Fourteen scoped suites pass on Node 24 and Bun; the Bun readSync gap remains an unchanged stop. Refreshed proof covers 638 passed/1 existing skip on Node and 636 passed/3 existing skips on Bun. P2 review and exact-head ClawSweeper found no actionable defects. Production LOC delta is zero.

The first CI head exposed an inherited UI-validator fixture omission. After incorporating its existing main fix, exact-head CI passed, including the published-driver update job. No same-head retry or admin exception was used.
2026-10-02 15:46:52 -07:00

117 lines
4.5 KiB
TypeScript

import type { ChildProcess } from "node:child_process";
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
import path from "node:path";
import { afterEach, expect, it } from "vitest";
import { scriptProcessEntrypoints } from "../../scripts/script-process-runtime.test-support.js";
import { resolveRuntimeWorkerUrl } from "../../src/infra/runtime-worker-url.js";
import { createFixtureLifetime } from "../helpers/fixture-lifetime.js";
import { createDeferred } from "../helpers/promise.js";
import { runNodeScript } from "../helpers/run-node-script.js";
const scriptUrl = resolveRuntimeWorkerUrl(scriptProcessEntrypoints.agentPluginGatewayE2e);
const fixtures = createFixtureLifetime();
afterEach(() => fixtures.cleanup());
const cases = [
...(["fixture", "config"] as const).flatMap((phase) =>
([null, "SIGINT", "SIGTERM"] as const).map((signal) => ({
phase,
signal,
boundary: `${phase} write`,
})),
),
{ phase: "install", signal: "SIGTERM", boundary: "running installer" } as const,
];
it.skipIf(process.platform === "win32").for(cases)(
"respects Agent Plugin E2E cancellation across the $boundary ($signal)",
{ timeout: 30_000 },
async ({ phase, signal }, context) => {
await fixtures.run(async () => {
const controls = fixtures.createTempDir("agent-plugin-cancellation-");
const home = path.join(controls, "home");
const tmp = path.join(controls, "tmp");
mkdirSync(home);
mkdirSync(tmp);
const release = path.join(controls, "release");
const ready = createDeferred();
const receivedSignal = createDeferred();
let child: ChildProcess | undefined;
const command = fixtures.track(
runNodeScript(
(workerArgv) => {
const scriptArgs = workerArgv(scriptUrl);
return [
...scriptArgs.slice(0, -1),
"--import",
new URL("./fixtures/agent-plugin-gateway-cancellation.mjs", import.meta.url).href,
...scriptArgs.slice(-1),
];
},
{
PATH: process.env.PATH,
HOME: home,
TMPDIR: tmp,
TMP: tmp,
TEMP: tmp,
AGENT_PLUGIN_E2E_FIXTURE_DIR: controls,
AGENT_PLUGIN_E2E_WRITE_PHASE: phase,
...(signal ? { AGENT_PLUGIN_E2E_SIGNAL: signal } : {}),
},
10_000,
{
cwd: process.cwd(),
signal: context.signal,
requireProcessTreeExit: true,
maxBuffer: 128 * 1024,
onReady(owned, output) {
child = owned;
owned.stdout!.on("data", () => {
const text = output().stdout;
if (text.includes("fixture-write-ready\n")) {
ready.resolve();
}
if (signal && text.includes(`fixture-signal-${signal}\n`)) {
receivedSignal.resolve();
}
});
},
},
),
);
const closedBeforeGate = command.then((result) => {
throw new Error(`Entry point exited before the fixture gate: ${JSON.stringify(result)}`);
});
void closedBeforeGate.catch(() => {});
try {
await Promise.race([ready.promise, closedBeforeGate]);
if (signal) {
expect(child?.kill(signal)).toBe(true);
await Promise.race([receivedSignal.promise, closedBeforeGate]);
}
writeFileSync(release, "release");
const result = await command;
expect(result.error, result.stderr).toBeUndefined();
expect(result.status).toBeGreaterThan(0);
const launchesFile = path.join(controls, "launches");
const launches = existsSync(launchesFile)
? readFileSync(launchesFile, "utf8").trim().split("\n")
: [];
expect(launches).toEqual(
signal ? (phase === "fixture" ? [] : ["install"]) : ["install", "mock"],
);
if (!signal) {
expect(result.stderr).toContain("Synthetic service launch stopped at mock");
}
if (phase === "install") {
expect(result.stderr).toContain("agent-plugin-gateway-e2e interrupted");
expect(result.stderr).toContain("Managed command aborted");
}
const fixtureRoot = readFileSync(path.join(controls, "fixture-root"), "utf8");
expect(existsSync(fixtureRoot)).toBe(false);
} finally {
writeFileSync(release, "release");
await command;
}
});
},
);