Files
reverse-engineering-anything/tests/boundary/process/environmentServeShutdown.test.ts
T
Edgar ArakelyanandClaude Opus 5.5 6676582675 feat(environment)!: serve the environment state by default
environment-serve derives the environment state at startup by running the
environment-state view in-process on the capture with the manifest's
template rules, so an environment comes with its state and a user supplies
nothing extra. The derived Evidence is the same one inspect-analysis-view
produces by hand. --state is removed; --replay-only serves exactly the
stateless replay of main. When the state cannot be derived, or its catalog
does not fit the discovery budget, the server starts in replay mode, logs
the reason and reports it in the stop summary's `state`. Exported bundles
serve the state by default too: export derives it the same way, so the
verifier's tool count and checked answer match what is served, and the
README describes the served mode.

BREAKING CHANGE: environment-serve answers read tools from the derived
state and does not serve write tools unless --replay-only is given; the
stop summary reports `state` instead of `state_evidence_id`.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 23:29:29 -07:00

238 lines
7.9 KiB
TypeScript

import { once } from "node:events";
import { writeFile } from "node:fs/promises";
import { join } from "node:path";
import { describe, expect } from "vitest";
import {
scenarioHeaderOnlyCaptureEvidence,
scenarioServeFixture,
} from "../../fixtures/browserScenarioCapture.js";
import {
environmentServeFixture,
writeEnvironmentServeFiles,
} from "../../fixtures/environmentServe.js";
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
import { rawHttpRequest } from "../../support/http/rawRequest.js";
import { startListeningProcess } from "../../support/mcp/streamableHttpProcess.js";
import { processTest } from "../../support/process/processFixture.js";
const SHUTDOWN_TIMEOUT_MS = 90_000;
/** How each case is served and what its stopped document reports about the state. */
const MODES = [
{
mode: "from the state it derives",
fixture: environmentServeFixture,
flags: [],
environment: "grafana-example",
// The manifest's 19 tools less its 5 write tools.
tools: 14,
state: { served: true, evidence_id: expect.stringMatching(/^ev_/u) },
},
{
mode: "as pure replay with --replay-only",
fixture: environmentServeFixture,
flags: ["--replay-only"],
environment: "grafana-example",
tools: 19,
state: { served: false, reason: "replay-only" },
},
{
mode: "as replay when the capture yields no state",
fixture: () => scenarioServeFixture(scenarioHeaderOnlyCaptureEvidence()),
flags: [],
environment: expect.any(String),
tools: 5,
state: {
served: false,
reason: "unavailable",
code: "unsupported_target",
message: expect.stringContaining("response_body"),
},
},
] as const;
describe("rea environment-serve shutdown", () => {
// On Windows, Node's child.kill() terminates the child outright for SIGTERM and SIGINT alike,
// so no signal handler runs and there is no graceful shutdown to observe.
for (const { mode, fixture, flags, environment, tools, state } of MODES)
processTest.skipIf(process.platform === "win32")(
`closes the listener on SIGTERM, prints one stopped document and exits 143 when serving ${mode}`,
async ({ processes }) => {
const root = await createTestTempDirectory(
"rea-environment-serve-stop-",
);
const files = await writeEnvironmentServeFiles(root, fixture());
const started = await startListeningProcess(processes, [
"environment-serve",
files.manifestPath,
"--evidence",
files.capturePath,
...flags,
"--port",
"0",
"--json",
]);
let stdout = "";
started.child.stdout.on("data", (chunk: Buffer) => {
stdout += chunk.toString("utf8");
});
const card = await rawHttpRequest(
new URL("/.well-known/agent-env.json", started.url.origin),
);
expect(card.status).toBe(200);
started.child.kill("SIGTERM");
const [code, signalCode] = await once(started.child, "close");
expect(code).toBe(143);
expect(signalCode).toBeNull();
expect(JSON.parse(stdout)).toEqual({
status: "stopped",
url: started.url.href,
environment,
tools,
evidence_id: expect.stringMatching(/^ev_/u),
manifest_evidence_id: expect.stringMatching(/^ev_/u),
state,
});
// A state that cannot be derived is logged as a warning naming its code, before the listener binds.
const warning = started
.stderr()
.split("\n")
.find((line) => line.includes("Environment state unavailable"));
if ("code" in state) {
expect(JSON.parse(warning ?? "{}")).toMatchObject({
level: 40,
code: state.code,
error: state.message,
});
expect(started.stderr().indexOf(warning ?? "")).toBeLessThan(
started.stderr().indexOf("MCP transport listening"),
);
} else expect(warning).toBeUndefined();
await expect(rawHttpRequest(started.url)).rejects.toMatchObject({
code: "ECONNREFUSED",
});
},
SHUTDOWN_TIMEOUT_MS,
);
});
describe.skipIf(process.platform === "win32")(
"environment startup cancellation",
() => {
for (const phase of ["read", "bind"] as const) {
processTest(
`cancels paused startup at ${phase} without announcing a listener`,
async ({ processes }) => {
const root = await createTestTempDirectory(
"rea-environment-start-cancel-",
);
const files = await writeEnvironmentServeFiles(root);
const preload = join(root, "pause-read.mjs");
const marker = "REA_STARTUP_PAUSED";
const pause = `
const interrupted = new Promise(resolve => process.once("SIGTERM", resolve));
const keepAlive = setInterval(() => undefined, 1000);
process.stderr.write(${JSON.stringify(marker + "\n")});
try { await interrupted; } finally { clearInterval(keepAlive); }
`;
const source =
phase === "read"
? `
import fs from "node:fs/promises";
const open = fs.open;
let gated = false;
fs.open = async (path, ...args) => {
const handle = await open(path, ...args);
if (path === ${JSON.stringify(files.manifestPath)}) {
const read = handle.read.bind(handle);
handle.read = async (...args) => {
if (!gated) { gated = true; ${pause} }
return read(...args);
};
}
return handle;
};
`
: `
import dns from "node:dns";
const lookup = dns.lookup;
dns.lookup = (host, ...args) => {
if (host !== "localhost") return lookup(host, ...args);
const callback = args.at(-1);
void (async () => { ${pause} if (args[0]?.all) callback(null, [{address: "127.0.0.1", family: 4}]); else callback(null, "127.0.0.1", 4); })();
};
`;
await writeFile(
preload,
source +
`
import { syncBuiltinESMExports } from "node:module";
syncBuiltinESMExports();
`,
);
const child = processes.spawn(
process.execPath,
[
"--import",
preload,
"scripts/rea.mjs",
"environment-serve",
files.manifestPath,
"--evidence",
files.capturePath,
"--host",
phase === "bind" ? "localhost" : "127.0.0.1",
"--port",
"0",
"--json",
],
{
env: {
PATH: process.env.PATH ?? "",
NODE_OPTIONS: "--max-old-space-size=768",
REA_LOG_LEVEL: "info",
},
},
);
const closed = once(child, "close");
child.stdin.end();
let stdout = "";
let stderr = "";
child.stdout.on("data", (chunk: Buffer) => {
stdout += chunk.toString("utf8");
});
await new Promise<void>((resolve, reject) => {
const timeout = setTimeout(
() => reject(new Error(`No Evidence read marker: ${stderr}`)),
30_000,
);
child.stderr.on("data", (chunk: Buffer) => {
stderr = (stderr + chunk.toString("utf8")).slice(-64 * 1024);
if (stderr.includes(marker)) {
clearTimeout(timeout);
resolve();
}
});
child.once("exit", () => {
clearTimeout(timeout);
reject(new Error(`Exited before Evidence read: ${stderr}`));
});
});
child.kill("SIGTERM");
const [code, signal] = await closed;
expect(code).toBe(143);
expect(signal).toBeNull();
expect(JSON.parse(stdout)).toMatchObject({
code: "cancelled",
details: { operation: "environment-serve" },
});
expect(stderr).not.toContain("MCP transport listening");
},
SHUTDOWN_TIMEOUT_MS,
);
}
},
);