mirror of
https://github.com/morluto/rea
synced 2026-10-25 22:08:11 -04:00
229 lines
7.5 KiB
TypeScript
229 lines
7.5 KiB
TypeScript
import { expect, it, vi } from "vitest";
|
|
import { AnalysisError } from "../../src/domain/analysisErrorBase.js";
|
|
import { createCli } from "../../src/cli.js";
|
|
import type { ServedEnvironment } from "../../src/server/environment/serveEnvironment.js";
|
|
import type { ServeStreamableHttpOptions } from "../../src/server/http/serveStreamableHttp.js";
|
|
import { StreamableHttpListenError } from "../../src/server/http/streamableHttpListenError.js";
|
|
import { createTestTempDirectory } from "../fixtures/temporaryDirectory.js";
|
|
import { writeEnvironmentServeFiles } from "../fixtures/environmentServe.js";
|
|
|
|
const control = vi.hoisted(() => ({
|
|
ready: undefined as (() => void) | undefined,
|
|
served: undefined as ServedEnvironment | undefined,
|
|
onerror: undefined as ServeStreamableHttpOptions["onerror"],
|
|
failClose: false,
|
|
closeCalls: 0,
|
|
cleanupFailure: undefined as AnalysisError | undefined,
|
|
}));
|
|
|
|
// Read asynchronous mock observations through their declared owner contracts;
|
|
// assignments inside imported callbacks are not visible to local control flow.
|
|
const observedEnvironment = (): ServedEnvironment | undefined => control.served;
|
|
const observedCleanupFailure = (): AnalysisError | undefined =>
|
|
control.cleanupFailure;
|
|
|
|
vi.mock(
|
|
"../../src/server/http/serveStreamableHttp.js",
|
|
async (importOriginal) => {
|
|
const actual =
|
|
await importOriginal<
|
|
typeof import("../../src/server/http/serveStreamableHttp.js")
|
|
>();
|
|
return {
|
|
...actual,
|
|
serveStreamableHttp: (options: ServeStreamableHttpOptions) => {
|
|
control.onerror = options.onerror;
|
|
return actual.serveStreamableHttp({
|
|
...options,
|
|
createMcpServer: async (context) => {
|
|
const server = await options.createMcpServer(context);
|
|
const close = server.close.bind(server);
|
|
server.close = async () => {
|
|
control.closeCalls += 1;
|
|
if (control.failClose) {
|
|
control.failClose = false;
|
|
throw new Error("controlled MCP cleanup failure");
|
|
}
|
|
await close();
|
|
};
|
|
return server;
|
|
},
|
|
});
|
|
},
|
|
};
|
|
},
|
|
);
|
|
|
|
vi.mock(
|
|
"../../src/server/environment/serveEnvironment.js",
|
|
async (importOriginal) => {
|
|
const actual =
|
|
await importOriginal<
|
|
typeof import("../../src/server/environment/serveEnvironment.js")
|
|
>();
|
|
return {
|
|
...actual,
|
|
serveEnvironment: async (
|
|
...args: Parameters<typeof actual.serveEnvironment>
|
|
) => {
|
|
const started = await actual.serveEnvironment(...args);
|
|
if (!started.ok) return started;
|
|
const served = {
|
|
...started.value,
|
|
close: async () => {
|
|
try {
|
|
await started.value.close();
|
|
} catch (cause) {
|
|
if (cause instanceof AnalysisError)
|
|
control.cleanupFailure = cause;
|
|
throw cause;
|
|
}
|
|
},
|
|
};
|
|
control.served = served;
|
|
control.ready?.();
|
|
return { ...started, value: served };
|
|
},
|
|
};
|
|
},
|
|
);
|
|
|
|
it.each([
|
|
{ ending: "signal", fails: false },
|
|
{ ending: "signal", fails: true },
|
|
{ ending: "listener", fails: false },
|
|
{ ending: "listener", fails: true },
|
|
] as const)(
|
|
"retains environment $ending facts when final cleanup fails=$fails",
|
|
async ({ ending, fails }) => {
|
|
const files = await writeEnvironmentServeFiles(
|
|
await createTestTempDirectory("rea-environment-finalization-"),
|
|
);
|
|
const originalExitCode = process.exitCode;
|
|
const originalListeners = new Set(process.listeners("SIGINT"));
|
|
let ready!: () => void;
|
|
const started = new Promise<void>((resolve) => {
|
|
ready = resolve;
|
|
});
|
|
control.ready = ready;
|
|
control.served = undefined;
|
|
control.failClose = fails;
|
|
control.closeCalls = 0;
|
|
control.cleanupFailure = undefined;
|
|
let stdout = "";
|
|
const pending = createCli({ REA_LOG_LEVEL: "silent" }).serve(
|
|
[
|
|
"environment-serve",
|
|
files.manifestPath,
|
|
"--evidence",
|
|
files.capturePath,
|
|
"--port",
|
|
"0",
|
|
"--json",
|
|
],
|
|
{
|
|
env: {},
|
|
exit: () => {},
|
|
stdout: (text) => {
|
|
stdout += text;
|
|
},
|
|
},
|
|
);
|
|
try {
|
|
await started;
|
|
const served = observedEnvironment();
|
|
if (served === undefined) throw new Error("Environment was not admitted");
|
|
if (ending === "signal") {
|
|
const listeners = process
|
|
.listeners("SIGINT")
|
|
.filter((listener) => !originalListeners.has(listener));
|
|
expect(listeners).toHaveLength(1);
|
|
listeners[0]?.call(process, "SIGINT");
|
|
} else
|
|
control.onerror?.(
|
|
new StreamableHttpListenError(
|
|
Object.assign(new Error("controlled listener failure"), {
|
|
code: "EIO",
|
|
}),
|
|
"serving",
|
|
),
|
|
);
|
|
await pending;
|
|
const output: unknown = JSON.parse(stdout);
|
|
expect(control.closeCalls).toBe(1);
|
|
expect(process.exitCode).toBe(ending === "signal" ? 130 : 1);
|
|
if (!fails) {
|
|
expect(output).toMatchObject(
|
|
ending === "signal"
|
|
? {
|
|
status: "stopped",
|
|
url: served.url,
|
|
evidence_id: expect.any(String),
|
|
}
|
|
: {
|
|
code: "resource_constraint",
|
|
details: { diagnostics: { listener_phase: "serving" } },
|
|
},
|
|
);
|
|
} else {
|
|
expect(JSON.stringify(output)).toContain(
|
|
"controlled MCP cleanup failure",
|
|
);
|
|
expect(output).not.toHaveProperty("status");
|
|
expect(output).toMatchObject({
|
|
code: "cleanup_incomplete",
|
|
details: { resources: [served.url] },
|
|
});
|
|
expect(output).toMatchObject(
|
|
ending === "signal"
|
|
? {
|
|
details: {
|
|
diagnostics: {
|
|
shutdown_observation: {
|
|
status: "shutdown_requested",
|
|
url: served.url,
|
|
environment: served.card.name,
|
|
tools: served.card.capabilities.tools.length,
|
|
evidence_id: expect.any(String),
|
|
manifest_evidence_id: expect.any(String),
|
|
},
|
|
},
|
|
},
|
|
}
|
|
: {
|
|
details: {
|
|
diagnostics: {
|
|
primary_error: {
|
|
code: "resource_constraint",
|
|
details: {
|
|
diagnostics: {
|
|
listener_phase: "serving",
|
|
listener_failure: { system_code: "EIO" },
|
|
},
|
|
},
|
|
remediation: { action: expect.any(String) },
|
|
},
|
|
},
|
|
},
|
|
},
|
|
);
|
|
expect(observedCleanupFailure()?.cleanupIncomplete).toBe(true);
|
|
expect(observedCleanupFailure()?.cleanupResources).toEqual([
|
|
served.url,
|
|
]);
|
|
await served.close();
|
|
expect(control.closeCalls).toBe(2);
|
|
expect(observedCleanupFailure()?.cleanupIncomplete).toBe(false);
|
|
expect(observedCleanupFailure()?.cleanupResources).toEqual([]);
|
|
}
|
|
expect(process.listeners("SIGINT")).toEqual([...originalListeners]);
|
|
} finally {
|
|
control.failClose = false;
|
|
await observedEnvironment()?.close();
|
|
control.ready = undefined;
|
|
control.served = undefined;
|
|
process.exitCode = originalExitCode;
|
|
}
|
|
},
|
|
);
|