Deliver complete Evidence directly through MCP with required nullable analysis profiles and immutable per-server delivery settings. Bind selected environments at construction and require typed cleanup outcomes with retained retry ownership. Remove test-only routing, duplicate session and snapshot metadata, producer-field repairs, and ambiguous provider arguments. Separate artifact byte identity from occurrence classification and retain typed source-map and partial native facts across CLI, MCP, and verifier consumers. BREAKING CHANGE: obsolete Evidence envelopes, session aliases, incomplete snapshots, and missing normalized producer facts are no longer accepted.
179 lines
5.3 KiB
TypeScript
179 lines
5.3 KiB
TypeScript
import { access, rm, writeFile } from "node:fs/promises";
|
|
import { join } from "node:path";
|
|
import { createTestTempDirectory } from "../../tests/fixtures/temporaryDirectory.js";
|
|
import { PrivateRuntimeRoot } from "../process/PrivateRuntimeRoot.js";
|
|
|
|
import { EventEmitter } from "node:events";
|
|
import { Socket } from "node:net";
|
|
|
|
import pino from "pino";
|
|
import { describe, expect, it } from "vitest";
|
|
|
|
import { ok } from "../domain/result.js";
|
|
import type { BridgeLaunch } from "./BridgeLauncher.js";
|
|
import { cleanupHopperSession } from "./HopperCleanup.js";
|
|
|
|
const loggerHarness = () => {
|
|
const logs: unknown[] = [];
|
|
return {
|
|
logs,
|
|
logger: pino(
|
|
{ level: "warn" },
|
|
{
|
|
write: (line) => logs.push(JSON.parse(line)),
|
|
},
|
|
),
|
|
};
|
|
};
|
|
|
|
const processCleanupLaunch = (): BridgeLaunch => ({
|
|
process: Object.assign(new EventEmitter(), {
|
|
stdout: null,
|
|
stderr: null,
|
|
exitCode: null,
|
|
signalCode: null,
|
|
kill: () => true,
|
|
}),
|
|
ownsProcessLifetime: true,
|
|
providerLifetime: "launcher-process",
|
|
shutdownMode: "process-cleanup",
|
|
cleanup: () =>
|
|
Promise.resolve({
|
|
cleaned: false as const,
|
|
reason: "fixture cleanup unavailable",
|
|
}),
|
|
});
|
|
|
|
const failure = (message: string) =>
|
|
Object.assign(new TypeError(message), { code: "ECONNRESET" });
|
|
|
|
describe("Hopper shutdown rejection diagnostics", () => {
|
|
it("retains the primary shutdown operation and rejection cause", async () => {
|
|
const { logger, logs } = loggerHarness();
|
|
const result = await cleanupHopperSession({
|
|
socket: new Socket(),
|
|
resources: {
|
|
launch: undefined,
|
|
processSupervisor: undefined,
|
|
runtimeRoot: undefined,
|
|
shutdownConfirmed: false,
|
|
},
|
|
activeRequest: null,
|
|
retainDocument: false,
|
|
progress: undefined,
|
|
logger,
|
|
onDiagnostic: undefined,
|
|
request: () => Promise.reject(failure("fixture primary rejection")),
|
|
releaseTransport: () => undefined,
|
|
});
|
|
|
|
expect(result).toMatchObject({
|
|
ok: false,
|
|
error: { cleanupResources: ["hopper-document"] },
|
|
});
|
|
expect(logs).toContainEqual(
|
|
expect.objectContaining({
|
|
msg: "Hopper document shutdown was not confirmed",
|
|
status: "failed",
|
|
errorTag: "HopperProcessError",
|
|
operation: "shutdown_document",
|
|
failure_cause: {
|
|
name: "TypeError",
|
|
message: "fixture primary rejection",
|
|
code: "ECONNRESET",
|
|
},
|
|
}),
|
|
);
|
|
});
|
|
|
|
it("retains the fallback shutdown operation and rejection cause", async () => {
|
|
const { logger, logs } = loggerHarness();
|
|
const methods: string[] = [];
|
|
const result = await cleanupHopperSession({
|
|
socket: new Socket(),
|
|
resources: {
|
|
launch: processCleanupLaunch(),
|
|
processSupervisor: undefined,
|
|
runtimeRoot: undefined,
|
|
shutdownConfirmed: false,
|
|
},
|
|
activeRequest: null,
|
|
retainDocument: false,
|
|
progress: undefined,
|
|
logger,
|
|
onDiagnostic: undefined,
|
|
request: (method) => {
|
|
methods.push(method);
|
|
return method === "shutdown"
|
|
? Promise.resolve(ok({ shutdown: true, cleanup_required: true }))
|
|
: Promise.reject(failure("fixture fallback rejection"));
|
|
},
|
|
releaseTransport: () => undefined,
|
|
});
|
|
|
|
expect(methods).toEqual(["shutdown", "shutdown_document"]);
|
|
expect(result).toMatchObject({
|
|
ok: false,
|
|
error: { cleanupResources: ["hopper-document"] },
|
|
});
|
|
expect(logs).toContainEqual(
|
|
expect.objectContaining({
|
|
msg: "Hopper document shutdown fallback was not confirmed",
|
|
status: "failed",
|
|
errorTag: "HopperProcessError",
|
|
operation: "shutdown_document",
|
|
failure_cause: {
|
|
name: "TypeError",
|
|
message: "fixture fallback rejection",
|
|
code: "ECONNRESET",
|
|
},
|
|
}),
|
|
);
|
|
});
|
|
});
|
|
|
|
it("keeps a prepared backing image when native document closure cannot be confirmed", async () => {
|
|
const parent = await createTestTempDirectory("rea-hopper-backing-lifetime-");
|
|
const runtimeRoot = await PrivateRuntimeRoot.create({ parent });
|
|
const preparedImagePath = join(runtimeRoot.path, "image.macho");
|
|
await writeFile(preparedImagePath, "owned backing bytes");
|
|
const { logger } = loggerHarness();
|
|
try {
|
|
const launch = {
|
|
...processCleanupLaunch(),
|
|
preparedImagePath,
|
|
shutdownMode: "bridge-request" as const,
|
|
providerLifetime: "external-application" as const,
|
|
};
|
|
const result = await cleanupHopperSession({
|
|
socket: new Socket(),
|
|
resources: {
|
|
launch,
|
|
runtimeRoot,
|
|
processSupervisor: undefined,
|
|
shutdownConfirmed: false,
|
|
},
|
|
activeRequest: null,
|
|
retainDocument: true,
|
|
progress: undefined,
|
|
logger,
|
|
onDiagnostic: undefined,
|
|
request: () => Promise.reject(failure("document closure unobserved")),
|
|
releaseTransport: () => undefined,
|
|
});
|
|
expect(result).toMatchObject({
|
|
ok: false,
|
|
error: {
|
|
cleanupResources: expect.arrayContaining([
|
|
"hopper-document",
|
|
runtimeRoot.path,
|
|
]),
|
|
},
|
|
});
|
|
await expect(access(preparedImagePath)).resolves.toBeUndefined();
|
|
} finally {
|
|
await runtimeRoot.close();
|
|
await rm(parent, { recursive: true, force: true });
|
|
}
|
|
});
|