Files
rea/tests/boundary/pcap/pcapProviderCancellation.test.ts
T

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);
});