mirror of
https://github.com/morluto/rea
synced 2026-10-27 14:42:18 -04:00
270 lines
10 KiB
TypeScript
270 lines
10 KiB
TypeScript
import { writeFile } from "node:fs/promises";
|
|
import { dirname, join } from "node:path";
|
|
import { expect, onTestFinished, test, vi } from "vitest";
|
|
|
|
import { AnalysisCancelledError } from "../../../src/domain/analysisErrorCore.js";
|
|
import { ProviderCleanupError } from "../../../src/domain/providerCleanupError.js";
|
|
import { resolvedAnalysisError } from "../../../src/domain/analysisErrorBase.js";
|
|
import { PrivateRuntimeRoot } from "../../../src/process/PrivateRuntimeRoot.js";
|
|
import { cleanupOwnedProcessGroup } from "../../../src/process/ProcessOwnership.js";
|
|
import { spawnOwnedProviderProcess } from "../../../src/process/ProviderProcess.js";
|
|
import { PcapProvider } from "../../../src/pcap/PcapProvider.js";
|
|
import type { PcapCommandLauncher } from "../../../src/pcap/PcapCommand.js";
|
|
import { projectAnalysisError } from "../../../src/domain/analysisErrorProjection.js";
|
|
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
|
|
|
|
const it = test.skipIf(process.platform === "win32");
|
|
|
|
vi.mock("../../../src/pcap/PcapNativeBound.js", async (importOriginal) => ({
|
|
...(await importOriginal<
|
|
typeof import("../../../src/pcap/PcapNativeBound.js")
|
|
>()),
|
|
// Exercise the provider's capture consumer with real owned children on hosts
|
|
// where its production native bound is unavailable (including macOS).
|
|
preparePcapNativeBound: async () => ({ nativeBound: {}, limitations: [] }),
|
|
}));
|
|
|
|
const writeTool = async (
|
|
root: string,
|
|
name: string,
|
|
body: string,
|
|
): Promise<string> => {
|
|
const path = join(root, name);
|
|
await writeFile(path, `#!/usr/bin/env -S ${process.execPath}\n${body}\n`, {
|
|
mode: 0o755,
|
|
});
|
|
return path;
|
|
};
|
|
|
|
const writeTools = async (
|
|
root: string,
|
|
capinfosMode: "active" | "physical" | "complete",
|
|
activeVersion = false,
|
|
): Promise<{ readonly tshark: string; readonly capinfos: string }> => {
|
|
const tshark = await writeTool(
|
|
root,
|
|
"tshark.cjs",
|
|
`if (process.argv[2] === "--version") { ${activeVersion ? 'process.stdout.write("ready\\n"); setInterval(() => {}, 1000);' : 'console.log("TShark (Wireshark) 4.6.6-test"); process.exit(0);'} }\nconsole.log("[]");`,
|
|
);
|
|
const capinfosBody =
|
|
capinfosMode === "active"
|
|
? `if (process.argv[2] === "--version") { console.log("Capinfos (Wireshark) 4.6.6-test"); process.exit(0); }\nprocess.stdout.write("ready\\n"); setInterval(() => {}, 1000);`
|
|
: capinfosMode === "physical"
|
|
? `if (process.argv[2] === "--version") { console.log("Capinfos (Wireshark) 4.6.6-test"); process.exit(0); }\nprocess.stderr.write("authored producer failure\\n"); process.exitCode = 7;`
|
|
: `if (process.argv[2] === "--version") { console.log("Capinfos (Wireshark) 4.6.6-test"); process.exit(0); }\nprocess.stdout.write(${JSON.stringify("File name: /work/capture.pcap\nFile type: Wireshark/tcpdump/... - pcap\nFile encapsulation: Ethernet\nPacket size limit: file hdr: 262144 bytes\nNumber of packets: 0\nCapture duration: 0.000 seconds\nEarliest packet time: 1791728020.608505\nLatest packet time: 1791728020.608505\n")});`;
|
|
const capinfos = await writeTool(root, "capinfos.cjs", capinfosBody);
|
|
return { tshark, capinfos };
|
|
};
|
|
|
|
const fixture = async (
|
|
mode: "active" | "physical" | "complete",
|
|
controller: AbortController,
|
|
reason: unknown,
|
|
failedCleanupAttempts: number,
|
|
activeVersion = false,
|
|
) => {
|
|
const root = await createTestTempDirectory("rea-pcap-provider-cancel-");
|
|
const tools = await writeTools(root, mode, activeVersion);
|
|
const capture = join(root, "capture.pcap");
|
|
await writeFile(capture, "controlled capture bytes");
|
|
let cleanupAttempts = 0;
|
|
const launcher: PcapCommandLauncher = async (spawn) => {
|
|
const launched = await spawnOwnedProviderProcess(spawn);
|
|
const captureCommand = spawn.arguments.includes("-M");
|
|
const activeVersionCommand =
|
|
activeVersion && spawn.arguments.includes("--version");
|
|
if (!captureCommand && !activeVersionCommand) return launched;
|
|
if (activeVersionCommand) controller.abort(reason);
|
|
if (mode === "active" && captureCommand)
|
|
launched.process.stdout?.on("data", (chunk: Buffer) => {
|
|
if (chunk.toString().includes("ready")) controller.abort(reason);
|
|
});
|
|
if (!captureCommand) return launched;
|
|
const cleanup =
|
|
launched.cleanup ?? (() => cleanupOwnedProcessGroup(launched.ownership));
|
|
return {
|
|
...launched,
|
|
cleanup: async () => {
|
|
cleanupAttempts += 1;
|
|
if (mode === "physical") controller.abort(reason);
|
|
if (cleanupAttempts <= failedCleanupAttempts)
|
|
throw new Error("injected retained cleanup failure");
|
|
const result = await cleanup();
|
|
return result;
|
|
},
|
|
};
|
|
};
|
|
const provider = new PcapProvider({
|
|
environment: {
|
|
REA_TSHARK_COMMAND: tools.tshark,
|
|
REA_CAPINFOS_COMMAND: tools.capinfos,
|
|
},
|
|
launcher,
|
|
});
|
|
onTestFinished(async () => provider.close());
|
|
return { provider, root, capture, cleanupAttempts: () => cleanupAttempts };
|
|
};
|
|
|
|
it("availability preserves the producer's exact cancellation reason", async () => {
|
|
const controller = new AbortController();
|
|
const reason = new ProviderCleanupError(
|
|
"caller-owned",
|
|
["foreign resource"],
|
|
{ reason: "opaque caller reason" },
|
|
);
|
|
const { provider } = await fixture("active", controller, reason, 0, true);
|
|
let failure: unknown;
|
|
try {
|
|
await provider.inspectAvailability(controller.signal);
|
|
} catch (cause) {
|
|
failure = cause;
|
|
}
|
|
expect(failure).toBeInstanceOf(AnalysisCancelledError);
|
|
expect(failure).toMatchObject({ cause: reason });
|
|
});
|
|
|
|
it("active cancellation stays primary through retained command and snapshot cleanup retry", async () => {
|
|
const controller = new AbortController();
|
|
const reason = "caller cancellation token";
|
|
const { provider, capture, cleanupAttempts } = await fixture(
|
|
"active",
|
|
controller,
|
|
reason,
|
|
2,
|
|
);
|
|
const result = await provider.execute(
|
|
{
|
|
operation: "inspect_pcap_capture",
|
|
input: { path: capture },
|
|
},
|
|
{ signal: controller.signal },
|
|
);
|
|
expect(result.ok).toBe(false);
|
|
if (result.ok) return;
|
|
const returned = result.error;
|
|
const primary = returned.cleanupPrimary;
|
|
expect(primary).toBeInstanceOf(AnalysisCancelledError);
|
|
expect(primary?.cause).toBe(reason);
|
|
expect(projectAnalysisError(returned).code).toBe("cleanup_incomplete");
|
|
expect(
|
|
returned.cleanupResources.some((path) => path.startsWith("process-group:")),
|
|
).toBe(true);
|
|
expect(
|
|
returned.cleanupResources.some((path) => path.endsWith("/capture.pcap")),
|
|
).toBe(true);
|
|
|
|
const blocked = await provider.execute({
|
|
operation: "inspect_pcap_capture",
|
|
input: { path: capture },
|
|
});
|
|
expect(blocked.ok).toBe(false);
|
|
if (!blocked.ok) {
|
|
expect(blocked.error).toBeInstanceOf(ProviderCleanupError);
|
|
expect(blocked.error.cleanupResources).toEqual(returned.cleanupResources);
|
|
}
|
|
|
|
const snapshotPath = returned.cleanupResources.find((path) =>
|
|
path.endsWith("/capture.pcap"),
|
|
);
|
|
if (snapshotPath === undefined) throw new Error("Missing retained snapshot");
|
|
const close = PrivateRuntimeRoot.prototype.close;
|
|
const denied = vi
|
|
.spyOn(PrivateRuntimeRoot.prototype, "close")
|
|
.mockImplementation(function (this: PrivateRuntimeRoot) {
|
|
if (this.path === dirname(snapshotPath))
|
|
return Promise.reject(new Error("injected snapshot close failure"));
|
|
return close.call(this);
|
|
});
|
|
try {
|
|
let firstCloseFailure: unknown;
|
|
try {
|
|
await provider.close();
|
|
} catch (cause) {
|
|
firstCloseFailure = cause;
|
|
}
|
|
expect(firstCloseFailure).toBeInstanceOf(ProviderCleanupError);
|
|
expect(firstCloseFailure).toMatchObject({ cleanupIncomplete: true });
|
|
const closeFailure = firstCloseFailure as ProviderCleanupError;
|
|
expect(closeFailure.cleanupResources).toContain(snapshotPath);
|
|
expect(
|
|
closeFailure.cleanupResources.some((path) =>
|
|
path.startsWith("process-group:"),
|
|
),
|
|
).toBe(true);
|
|
expect(cleanupAttempts()).toBe(2);
|
|
// The next close releases the command but the deliberately denied snapshot
|
|
// close leaves only that exact owner in the first close error.
|
|
await expect(provider.close()).rejects.toMatchObject({
|
|
cleanupIncomplete: true,
|
|
});
|
|
expect(closeFailure.cleanupResources).toEqual([snapshotPath]);
|
|
expect(closeFailure.cleanupIncomplete).toBe(true);
|
|
expect(returned.cleanupIncomplete).toBe(true);
|
|
expect(returned.cleanupResources).toEqual([snapshotPath]);
|
|
denied.mockRestore();
|
|
await provider.close();
|
|
expect(closeFailure.cleanupIncomplete).toBe(false);
|
|
expect(closeFailure.cleanupResources).toEqual([]);
|
|
} finally {
|
|
denied.mockRestore();
|
|
}
|
|
expect(returned.cleanupIncomplete).toBe(false);
|
|
expect(resolvedAnalysisError(returned)).toBe(primary);
|
|
});
|
|
|
|
it("a physical command failure survives a late cancellation and cleanup retry", async () => {
|
|
const controller = new AbortController();
|
|
const reason = { caller: "late abort" };
|
|
const { provider, capture } = await fixture(
|
|
"physical",
|
|
controller,
|
|
reason,
|
|
1,
|
|
);
|
|
const result = await provider.execute(
|
|
{
|
|
operation: "inspect_pcap_capture",
|
|
input: { path: capture },
|
|
},
|
|
{ signal: controller.signal },
|
|
);
|
|
expect(result.ok).toBe(false);
|
|
if (result.ok) return;
|
|
const returned = result.error;
|
|
const primary = returned.cleanupPrimary;
|
|
expect(primary?._tag).toBe("AnalysisProtocolError");
|
|
expect(primary?.cause).not.toBe(reason);
|
|
expect(projectAnalysisError(returned).code).toBe("cleanup_incomplete");
|
|
expect(returned.capturedOutput?.stderr).toContain(
|
|
"authored producer failure",
|
|
);
|
|
await provider.close();
|
|
expect(returned.cleanupIncomplete).toBe(false);
|
|
expect(resolvedAnalysisError(returned)).toBe(primary);
|
|
});
|
|
|
|
it("resolves the original completed-output cleanup error after retry", async () => {
|
|
const controller = new AbortController();
|
|
const { provider, capture, cleanupAttempts } = await fixture(
|
|
"complete",
|
|
controller,
|
|
undefined,
|
|
1,
|
|
);
|
|
const result = await provider.execute({
|
|
operation: "inspect_pcap_capture",
|
|
input: { path: capture },
|
|
});
|
|
expect(result.ok).toBe(false);
|
|
if (result.ok) return;
|
|
const returned = result.error;
|
|
expect(returned).toBeInstanceOf(ProviderCleanupError);
|
|
expect(returned.diagnostics).toMatchObject({ previous_error: null });
|
|
expect(returned.partialObservation).toBeDefined();
|
|
expect(returned.cleanupIncomplete).toBe(true);
|
|
await provider.close();
|
|
expect(cleanupAttempts()).toBe(2);
|
|
expect(returned.cleanupIncomplete).toBe(false);
|
|
expect(resolvedAnalysisError(returned)).toBe(returned);
|
|
});
|