mirror of
https://github.com/morluto/rea
synced 2026-10-27 06:33:32 -04:00
d8a9673fc4
Name REA_SWIFTC_COMPILE_TIMEOUT_MS, its effective milliseconds, the 1..2147483647 range, and the omitted 120000 ms default when a Darwin ownership-helper compile times out. A change applies after REA restarts, on the next ownership-helper compile. Producer SIGXCPU and SIGXFSZ recovery says REA retains the tighter inherited limit. Agent-visible fixed caps in this theme say this release has no runtime override. Numbers are unchanged. Refs #5910 Co-authored-by: Cursor Agent <cursoragent@cursor.com> Co-authored-by: morluto <morluto@users.noreply.github.com>
733 lines
26 KiB
TypeScript
733 lines
26 KiB
TypeScript
import { AnalysisResourceConstraintError } from "../../../src/domain/analysisErrorCore.js";
|
|
import { packetFieldsCatalogSchema } from "../../../src/domain/captures/packetFields.js";
|
|
import { OwnedCommandFailure } from "../../../src/process/OwnedCommand.js";
|
|
import { it, expect, onTestFinished } from "vitest";
|
|
import { writeFile, readdir, chmod, symlink } from "node:fs/promises";
|
|
import { join } from "node:path";
|
|
import { readStableArtifact } from "../../../src/artifacts/readStableArtifact.js";
|
|
import { TsharkCaptureProvider } from "../../../src/captures/TsharkCaptureProvider.js";
|
|
import { PacketCaptureService } from "../../../src/application/captures/PacketCaptureService.js";
|
|
import { packetCaptureSchema } from "../../../src/domain/captures/packetCapture.js";
|
|
import { PrivateRuntimeRoot } from "../../../src/process/PrivateRuntimeRoot.js";
|
|
import { spawnOwnedProviderProcess } from "../../../src/process/ProviderProcess.js";
|
|
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
|
|
import { createDeferred } from "../../fixtures/binarySession.js";
|
|
import { Cli } from "incur";
|
|
import { registerCaptureCommands } from "../../../src/cli/captureCommands.js";
|
|
import { silentLogger } from "../../../src/logger.js";
|
|
import { analysisErrorRemediationAction } from "../../../src/domain/analysisErrorPresentation.js";
|
|
|
|
// A command-boundary simulator: real process ownership/filesystem, not real TShark coverage.
|
|
const fixture = async (mode = "ok", failCleanup = false) => {
|
|
const directory = await createTestTempDirectory("rea-capture-test-");
|
|
const path = join(directory, "recorded bytes.pcap");
|
|
await writeFile(path, "recording bytes");
|
|
const script = join(directory, "producer.mjs");
|
|
await writeFile(
|
|
script,
|
|
`import {readFileSync, writeFileSync, chmodSync, truncateSync} from 'node:fs';
|
|
if (process.argv.includes('--version')) { console.log(${JSON.stringify(mode)} === 'bad-version' ? 'TShark (Wireshark) 3.0.0' : 'TShark (Wireshark) 4.6.9 (fixture)'); console.error('version warning'); }
|
|
else if (${JSON.stringify(mode)} === 'nonzero') { console.error('invalid decoder'); process.exitCode = 1; }
|
|
else if (${JSON.stringify(mode)} === 'malformed-copy') { chmodSync('capture', 0o600); writeFileSync('capture', 'changed'); }
|
|
else if (${JSON.stringify(mode)} === 'oversized-copy') { chmodSync('capture', 0o600); truncateSync('capture', 32 * 1024 * 1024 + 1); console.log('[]'); }
|
|
else if (${JSON.stringify(mode)} === 'overflow') process.stdout.write('x'.repeat((process.argv.includes('-G') ? 33 : 5) * 1024 * 1024));
|
|
else if (${JSON.stringify(mode)} === 'wait') { console.log('ready'); setInterval(() => {}, 1000); }
|
|
else if (${JSON.stringify(mode)} === 'invalid-json') console.log('not json');
|
|
else if (${JSON.stringify(mode)} === 'many') console.log(JSON.stringify(Array.from({length:90},(_,i)=>({_source:{layers:{"frame.number":[String(i+1)],"ip.addr":["192.0.2.1"]}}}))));
|
|
else if (process.argv.includes('-G')) { console.log('P\\tUniversal Serial Bus\\tusb\\nF\\tVendor ID\\tusb.idVendor\\tFT_UINT16\\tusb\\tBASE_HEX\\t0x0\\t\\nF\\tOther Vendor\\tusb.idVendor\\tFT_UINT32\\tusb\\tBASE_HEX\\t0x0\\tSecond registration\\nP\\tInternet Protocol\\tip\\nF\\tAddress\\tip.addr\\tFT_IPv4\\tip\\t\\t0x0\\tSource or destination'); console.error('glossary warning'); }
|
|
else { if(readFileSync('capture', 'utf8') !== 'recording bytes') throw Error('wrong copy'); console.log(JSON.stringify([{_source:{layers:{"frame.number":["1"],"ip.addr":["192.0.2.1","192.0.2.2"]}}}])); console.error('producer warning'); }
|
|
`,
|
|
);
|
|
await chmod(script, 0o700);
|
|
const configuredCommand =
|
|
mode === "symlink" ? join(directory, "configured-tool") : script;
|
|
if (mode === "symlink") await symlink(script, configuredCommand);
|
|
const controller = new AbortController();
|
|
let calls = 0;
|
|
let mayClean = !failCleanup;
|
|
const acquired = createDeferred<void>();
|
|
const release = createDeferred<void>();
|
|
const provider = new TsharkCaptureProvider(
|
|
{
|
|
...process.env,
|
|
REA_TSHARK_COMMAND: configuredCommand,
|
|
REA_TSHARK_PRLIMIT_COMMAND: configuredCommand,
|
|
},
|
|
async (input) => {
|
|
calls++;
|
|
if (mode === "launch-failure" && !input.arguments.includes("--version"))
|
|
throw Object.assign(new Error("spawn E2BIG"), { code: "E2BIG" });
|
|
if (mode === "file-limit" && !input.arguments.includes("--version")) {
|
|
const stdout = { text: "", bytes: 0, observedBytes: 0 };
|
|
const stderr = {
|
|
text: "before file limit\n",
|
|
bytes: 18,
|
|
observedBytes: 18,
|
|
};
|
|
throw new OwnedCommandFailure("process", "Synthetic SIGXFSZ snapshot", {
|
|
stdout,
|
|
stderr,
|
|
exitCode: null,
|
|
signal: "SIGXFSZ",
|
|
});
|
|
}
|
|
const spawned = await spawnOwnedProviderProcess({
|
|
...input,
|
|
command: process.execPath,
|
|
arguments: [script, ...input.arguments],
|
|
expectedCommand: process.execPath,
|
|
});
|
|
if (mode === "wait" && !input.arguments.includes("--version"))
|
|
spawned.process.stdout?.once("data", () => controller.abort());
|
|
return spawned;
|
|
},
|
|
async () => {
|
|
if (mode === "workspace-full")
|
|
throw Object.assign(new Error("No space left on device"), {
|
|
code: "ENOSPC",
|
|
});
|
|
const root = await PrivateRuntimeRoot.create({
|
|
parent: directory,
|
|
prefix: "decoder-",
|
|
});
|
|
if (mode === "gated") {
|
|
acquired.resolve();
|
|
await release.promise;
|
|
}
|
|
if (mode === "snapshot-write-failure")
|
|
await writeFile(join(root.path, "capture"), "existing");
|
|
return {
|
|
path: root.path,
|
|
close: async () => {
|
|
if (mode === "close-abort")
|
|
controller.abort({ request: "cancel during workspace close" });
|
|
if (!mayClean) throw new Error("cleanup fixture failure");
|
|
await root.close();
|
|
},
|
|
};
|
|
},
|
|
"linux",
|
|
async (...args) => {
|
|
if (mode === "oversized-tool" && calls === 2 && args[0] === script) {
|
|
const { truncate } = await import("node:fs/promises");
|
|
await truncate(script, 64 * 1024 * 1024 + 1);
|
|
}
|
|
if (mode === "post-run-abort" && calls === 2) controller.abort();
|
|
return readStableArtifact(...args);
|
|
},
|
|
async (limits) => ({ ...limits, file_bytes: 1048576 }),
|
|
);
|
|
onTestFinished(async () => {
|
|
release.resolve();
|
|
mayClean = true;
|
|
await provider.close();
|
|
});
|
|
const service = new PacketCaptureService(provider);
|
|
return {
|
|
service,
|
|
directory,
|
|
path,
|
|
controller,
|
|
calls: () => calls,
|
|
acquired: acquired.promise,
|
|
release: () => release.resolve(),
|
|
allowCleanup: () => {
|
|
mayClean = true;
|
|
},
|
|
};
|
|
};
|
|
|
|
it("refuses new acquisition after provider shutdown", async () => {
|
|
const f = await fixture();
|
|
await f.service.close();
|
|
expect(
|
|
await f.service.inspect({ path: f.path, fields: ["ip.addr"] }),
|
|
).toMatchObject({
|
|
ok: false,
|
|
error: { _tag: "AnalysisCancelledError" },
|
|
});
|
|
expect(f.calls()).toBe(0);
|
|
});
|
|
|
|
it("pages matching observations without changing dissection scope or frame identities", async () => {
|
|
const f = await fixture("many");
|
|
const input = { path: f.path, fields: ["ip.addr"] };
|
|
for (const [options, first, count] of [
|
|
[{}, 1, 50],
|
|
[{ match_offset: 50, match_limit: 30 }, 51, 30],
|
|
[{ match_limit: null }, 1, 90],
|
|
[{ match_offset: 100 }, undefined, 0],
|
|
] as const) {
|
|
const result = await f.service.inspect({ ...input, ...options });
|
|
if (!result.ok) throw result.error;
|
|
const report = packetCaptureSchema.parse(result.value.normalized_result);
|
|
expect(report.packets).toHaveLength(count);
|
|
expect(report.packets[0]?.["frame.number"][0]).toBe(
|
|
first === undefined ? undefined : String(first),
|
|
);
|
|
expect(report.output_window.matching_packets).toBe(90);
|
|
expect(report.output_window.returned_packets).toBe(count);
|
|
expect(report.read_scope).toBe("to-eof");
|
|
expect(report.tool.arguments).not.toContain("-c");
|
|
}
|
|
});
|
|
|
|
it("bounds admission without queuing additional snapshots or delaying cancellation", async () => {
|
|
const f = await fixture("gated");
|
|
const input = { path: f.path, fields: ["ip.addr"] };
|
|
const first = f.service.inspect(input);
|
|
await f.acquired;
|
|
const second = f.service.inspect(input);
|
|
const cancelled = f.service.inspect(input, { signal: AbortSignal.abort() });
|
|
f.release();
|
|
expect(await first).toMatchObject({ ok: true });
|
|
expect(await second).toMatchObject({
|
|
ok: false,
|
|
error: { _tag: "AnalysisResourceConstraintError", resource: "capacity" },
|
|
});
|
|
expect(await cancelled).toMatchObject({
|
|
ok: false,
|
|
error: { _tag: "AnalysisCancelledError" },
|
|
});
|
|
expect(f.calls()).toBe(2);
|
|
expect(await f.service.inspect(input)).toMatchObject({ ok: true });
|
|
});
|
|
|
|
it("inspects an immutable copy, preserves options and warnings, and releases its directory", async () => {
|
|
const f = await fixture();
|
|
const result = await f.service.inspect({
|
|
path: f.path,
|
|
fields: ["ip.addr"],
|
|
display_filter: "udp",
|
|
packet_limit: 10,
|
|
decode_as: ["udp.port==5555,dns"],
|
|
});
|
|
if (!result.ok) throw result.error;
|
|
const report = packetCaptureSchema.parse(result.value.normalized_result);
|
|
expect(report.packets).toEqual([
|
|
{ "frame.number": ["1"], "ip.addr": ["192.0.2.1", "192.0.2.2"] },
|
|
]);
|
|
expect(report.diagnostics).toBe("producer warning\n");
|
|
expect(report.tool.version_diagnostics).toBe("version warning\n");
|
|
expect(report.read_scope).toBe("up-to-input-packet-limit");
|
|
expect(report.tool.arguments).toEqual([
|
|
"-n",
|
|
"-r",
|
|
"capture",
|
|
"-T",
|
|
"json",
|
|
"--no-duplicate-keys",
|
|
"--temp-dir=.",
|
|
"-2",
|
|
"-c",
|
|
"10",
|
|
"-Y",
|
|
"udp",
|
|
"-d",
|
|
"udp.port==5555,dns",
|
|
"-e",
|
|
"frame.number",
|
|
"-e",
|
|
"ip.addr",
|
|
]);
|
|
expect((await readdir(f.directory)).sort()).toEqual([
|
|
"producer.mjs",
|
|
"recorded bytes.pcap",
|
|
]);
|
|
});
|
|
|
|
it.each([
|
|
["invalid-json", "AnalysisOutputError"],
|
|
["nonzero", "ProviderAdapterError"],
|
|
["malformed-copy", "AnalysisArtifactChangedError"],
|
|
["overflow", "AnalysisResourceConstraintError"],
|
|
["wait", "AnalysisCancelledError"],
|
|
])(
|
|
"preserves %s failure and releases the process and snapshot",
|
|
async (mode, tag) => {
|
|
const f = await fixture(mode);
|
|
const result = await f.service.inspect(
|
|
{ path: f.path, fields: ["ip.addr"] },
|
|
{ signal: f.controller.signal },
|
|
);
|
|
if (result.ok) throw new Error("Expected capture analysis failure");
|
|
expect(result.error._tag).toBe(tag);
|
|
if (mode === "wait")
|
|
expect(result.error.cause).toBe(f.controller.signal.reason);
|
|
expect(result.error.cleanupIncomplete).toBe(false);
|
|
expect(
|
|
(await readdir(f.directory)).filter((name) =>
|
|
name.startsWith("decoder-"),
|
|
),
|
|
).toEqual([]);
|
|
},
|
|
);
|
|
|
|
it.each(["capture", "fields"] as const)(
|
|
"does not attribute the version probe's output to a %s command that fails to launch",
|
|
async (operation) => {
|
|
const f = await fixture("launch-failure");
|
|
const result =
|
|
operation === "capture"
|
|
? await f.service.inspect({ path: f.path, fields: ["ip.addr"] })
|
|
: await f.service.inspectFields({ kind: "fields" });
|
|
if (result.ok) throw new Error("Expected launch failure");
|
|
expect(f.calls()).toBe(2);
|
|
expect(result.error.message).toContain("spawn E2BIG");
|
|
expect(result.error.capturedOutput).toBeUndefined();
|
|
},
|
|
);
|
|
|
|
it("retains a completed observation and cleanup ownership for retry", async () => {
|
|
const f = await fixture("ok", true);
|
|
const result = await f.service.inspect({ path: f.path, fields: ["ip.addr"] });
|
|
if (result.ok) throw new Error("Expected cleanup failure");
|
|
expect(result.error.cleanupIncomplete).toBe(true);
|
|
expect(result.error.partialObservation).toMatchObject({
|
|
operation: "inspect_packet_capture",
|
|
});
|
|
const retry = await f.service.inspect({ path: f.path, fields: ["ip.addr"] });
|
|
expect(retry.ok).toBe(false);
|
|
expect(f.calls()).toBe(2);
|
|
f.allowCleanup();
|
|
await f.service.close();
|
|
expect(
|
|
(await readdir(f.directory)).filter((name) => name.startsWith("decoder-")),
|
|
).toEqual([]);
|
|
});
|
|
|
|
it("rejects cancelled and invalid requests before any provider work", async () => {
|
|
const f = await fixture();
|
|
for (const input of [
|
|
{ path: "relative.pcap", fields: ["ip.addr"] },
|
|
{ path: f.path, fields: ["ip.addr"], interface: "lo0" },
|
|
]) {
|
|
const result = await f.service.inspect(input);
|
|
expect(result.ok).toBe(false);
|
|
}
|
|
const result = await f.service.inspect(
|
|
{ path: f.path, fields: ["ip.addr"] },
|
|
{ signal: AbortSignal.abort() },
|
|
);
|
|
if (result.ok) throw new Error("Expected cancellation");
|
|
expect(result.error._tag).toBe("AnalysisCancelledError");
|
|
expect(f.calls()).toBe(0);
|
|
});
|
|
|
|
it("retains a completed observation when cancellation arrives at the service boundary", async () => {
|
|
const f = await fixture();
|
|
const input = {
|
|
path: f.path,
|
|
fields: ["ip.addr"],
|
|
display_filter: null,
|
|
decode_as: [],
|
|
packet_limit: null,
|
|
two_pass: true,
|
|
match_offset: 0,
|
|
match_limit: 50,
|
|
};
|
|
const execution = await f.service.provider.inspect(input);
|
|
if (!execution.ok) throw execution.error;
|
|
const service = new PacketCaptureService({
|
|
inspectFields: async () => {
|
|
throw new Error("Unexpected field discovery call");
|
|
},
|
|
inspect: async () => {
|
|
f.controller.abort({ request: "cancel at Evidence boundary" });
|
|
return execution;
|
|
},
|
|
close: async () => {},
|
|
});
|
|
const result = await service.inspect(input, { signal: f.controller.signal });
|
|
if (result.ok) throw new Error("Expected late cancellation");
|
|
expect(result.error._tag).toBe("AnalysisCancelledError");
|
|
expect(result.error.cause).toBe(f.controller.signal.reason);
|
|
expect(result.error.partialObservation).toMatchObject({
|
|
operation: "inspect_packet_capture",
|
|
normalized_result: {
|
|
packets: [
|
|
{ "frame.number": ["1"], "ip.addr": ["192.0.2.1", "192.0.2.2"] },
|
|
],
|
|
},
|
|
});
|
|
});
|
|
|
|
it.each([
|
|
["workspace-full", "AnalysisResourceConstraintError"],
|
|
["snapshot-write-failure", "ProviderAdapterError"],
|
|
])(
|
|
"reports %s against the private workspace, not the selected input",
|
|
async (mode, tag) => {
|
|
const f = await fixture(mode);
|
|
const result = await f.service.inspect({
|
|
path: f.path,
|
|
fields: ["ip.addr"],
|
|
});
|
|
if (result.ok) throw new Error("Expected workspace failure");
|
|
expect(result.error._tag).toBe(tag);
|
|
if (result.error._tag === "ProviderAdapterError")
|
|
expect(result.error).toMatchObject({
|
|
diagnostics: {
|
|
stage: "workspace",
|
|
system_code: "EEXIST",
|
|
},
|
|
});
|
|
if (result.error._tag === "AnalysisResourceConstraintError")
|
|
expect(result.error).toMatchObject({
|
|
reportedLimits: {
|
|
system_code: "ENOSPC",
|
|
},
|
|
});
|
|
expect(f.calls()).toBe(mode === "workspace-full" ? 0 : 1);
|
|
expect(
|
|
(await readdir(f.directory)).filter((name) =>
|
|
name.startsWith("decoder-"),
|
|
),
|
|
).toEqual([]);
|
|
},
|
|
);
|
|
|
|
it("refuses unsupported hosts before launching or allocating a workspace", async () => {
|
|
const provider = new TsharkCaptureProvider(
|
|
{ REA_TSHARK_COMMAND: process.execPath },
|
|
async () => {
|
|
throw new Error("Must not launch");
|
|
},
|
|
async () => {
|
|
throw new Error("Must not allocate");
|
|
},
|
|
"darwin",
|
|
);
|
|
const result = await new PacketCaptureService(provider).inspect({
|
|
path: "/recording.pcap",
|
|
fields: ["ip.addr"],
|
|
});
|
|
if (result.ok) throw new Error("Expected host refusal");
|
|
expect(result.error).toMatchObject({
|
|
_tag: "AnalysisCapabilityUnavailableError",
|
|
reason: expect.stringContaining("Linux"),
|
|
});
|
|
});
|
|
|
|
it("preserves complete Evidence when aborted during post-run integrity checks", async () => {
|
|
const f = await fixture("post-run-abort");
|
|
const result = await f.service.inspect(
|
|
{ path: f.path, fields: ["ip.addr"] },
|
|
{ signal: f.controller.signal },
|
|
);
|
|
if (result.ok) throw new Error("Expected late cancellation");
|
|
expect(result.error._tag).toBe("AnalysisCancelledError");
|
|
expect(result.error.partialObservation).toMatchObject({
|
|
normalized_result: {
|
|
packets: [
|
|
{ "frame.number": ["1"], "ip.addr": ["192.0.2.1", "192.0.2.2"] },
|
|
],
|
|
diagnostics: "producer warning\n",
|
|
},
|
|
});
|
|
expect(
|
|
(await readdir(f.directory)).filter((name) => name.startsWith("decoder-")),
|
|
).toEqual([]);
|
|
});
|
|
it("preserves configured symlink paths separately from resolved tool identities", async () => {
|
|
const f = await fixture("symlink");
|
|
const result = await f.service.inspect({ path: f.path, fields: ["ip.addr"] });
|
|
if (!result.ok) throw result.error;
|
|
expect(result.value.normalized_result).toMatchObject({
|
|
tool: {
|
|
configured_path: join(f.directory, "configured-tool"),
|
|
path: join(f.directory, "producer.mjs"),
|
|
},
|
|
resource_limiter: {
|
|
configured_path: join(f.directory, "configured-tool"),
|
|
path: join(f.directory, "producer.mjs"),
|
|
},
|
|
});
|
|
});
|
|
|
|
it.each(["oversized-copy", "oversized-tool"])(
|
|
"identifies the mutated %s without blaming the admitted source",
|
|
async (mode) => {
|
|
const f = await fixture(mode);
|
|
const result = await f.service.inspect({
|
|
path: f.path,
|
|
fields: ["ip.addr"],
|
|
});
|
|
if (result.ok) throw new Error("Expected changed artifact");
|
|
expect(result.error).toMatchObject({
|
|
_tag: "AnalysisArtifactChangedError",
|
|
path:
|
|
mode === "oversized-tool"
|
|
? join(f.directory, "producer.mjs")
|
|
: expect.stringMatching(/decoder-.*[/\\]capture$/u),
|
|
});
|
|
expect(
|
|
(await readdir(f.directory)).filter((name) =>
|
|
name.startsWith("decoder-"),
|
|
),
|
|
).toEqual([]);
|
|
},
|
|
);
|
|
it("preserves termination signal and effective file ceiling as a resource failure", async () => {
|
|
const f = await fixture("file-limit");
|
|
const result = await f.service.inspect({ path: f.path, fields: ["ip.addr"] });
|
|
if (result.ok) throw new Error("Expected resource failure");
|
|
expect(result.error).toMatchObject({
|
|
_tag: "AnalysisResourceConstraintError",
|
|
resource: "file-size",
|
|
reportedLimits: { file_bytes: 1048576 },
|
|
capturedOutput: { signal: "SIGXFSZ", stderr: "before file limit\n" },
|
|
});
|
|
expect(analysisErrorRemediationAction(result.error)).toContain(
|
|
"REA retains the tighter inherited limit",
|
|
);
|
|
});
|
|
|
|
it("rejects oversized selector lists and UTF-8 argv expansion before provider work", async () => {
|
|
const f = await fixture();
|
|
for (const selections of [
|
|
{ fields: Array.from({ length: 1025 }, () => "ip.addr") },
|
|
{ fields: ["x".repeat(8193)] },
|
|
{ fields: ["x".repeat(8192)], display_filter: "界".repeat(8192) },
|
|
{
|
|
fields: ["ip.addr"],
|
|
decode_as: Array.from({ length: 1025 }, () => "udp.port==5555,dns"),
|
|
},
|
|
]) {
|
|
const result = await f.service.inspect({ path: f.path, ...selections });
|
|
if (result.ok) throw new Error("Expected invalid selectors");
|
|
expect(result.error._tag).toBe("AnalysisInputError");
|
|
}
|
|
expect(f.calls()).toBe(0);
|
|
});
|
|
|
|
it("discovers installed protocols and field definitions without a capture", async () => {
|
|
const f = await fixture();
|
|
for (const [input, expected] of [
|
|
[
|
|
{ query: "USB" },
|
|
{
|
|
records: [{ abbreviation: "usb", kind: "protocol", field_type: null }],
|
|
output_window: { examined: 2, matching: 1, returned: 1 },
|
|
},
|
|
],
|
|
[
|
|
{ kind: "fields", protocol: "usb", limit: 1 },
|
|
{
|
|
records: [
|
|
{
|
|
abbreviation: "usb.idVendor",
|
|
field_type: "FT_UINT16",
|
|
source_line: 2,
|
|
},
|
|
],
|
|
output_window: {
|
|
examined: 3,
|
|
matching: 2,
|
|
returned: 1,
|
|
has_more: true,
|
|
},
|
|
},
|
|
],
|
|
[
|
|
{ kind: "fields", protocol: "usb", offset: 1, limit: null },
|
|
{
|
|
records: [
|
|
{
|
|
abbreviation: "usb.idVendor",
|
|
field_type: "FT_UINT32",
|
|
source_line: 3,
|
|
},
|
|
],
|
|
output_window: { matching: 2, returned: 1, has_more: false },
|
|
},
|
|
],
|
|
[
|
|
{ kind: "fields", query: "destination" },
|
|
{
|
|
records: [
|
|
{
|
|
abbreviation: "ip.addr",
|
|
description: "Source or destination",
|
|
display_base: "",
|
|
},
|
|
],
|
|
},
|
|
],
|
|
[
|
|
{ query: "no-such-protocol" },
|
|
{ records: [], output_window: { matching: 0, returned: 0 } },
|
|
],
|
|
] as const) {
|
|
const result = await f.service.inspectFields(input);
|
|
if (!result.ok) throw result.error;
|
|
const report = packetFieldsCatalogSchema.parse(
|
|
result.value.normalized_result,
|
|
);
|
|
expect(report).toMatchObject(expected);
|
|
expect(report.tool.arguments).toEqual(["-G", "fields"]);
|
|
expect(report.tool.version_diagnostics).toBe("version warning\n");
|
|
expect(report.diagnostics).toBe("glossary warning\n");
|
|
expect(report.limits.output_bytes).toBe(32 * 1024 * 1024);
|
|
}
|
|
expect(
|
|
(await readdir(f.directory)).filter((name) => name.startsWith("decoder-")),
|
|
).toEqual([]);
|
|
});
|
|
|
|
it("shares discovery admission with inspection and refuses work after shutdown", async () => {
|
|
const f = await fixture("gated");
|
|
const active = f.service.inspect({ path: f.path, fields: ["ip.addr"] });
|
|
await f.acquired;
|
|
const busy = await f.service.inspectFields({});
|
|
f.release();
|
|
if (!(await active).ok) throw new Error("Inspection failed");
|
|
expect(busy).toMatchObject({
|
|
ok: false,
|
|
error: { operation: "inspect_packet_fields", resource: "capacity" },
|
|
});
|
|
await f.service.close();
|
|
expect(await f.service.inspectFields({})).toMatchObject({
|
|
ok: false,
|
|
error: {
|
|
operation: "inspect_packet_fields",
|
|
_tag: "AnalysisCancelledError",
|
|
},
|
|
});
|
|
});
|
|
|
|
it.each([
|
|
"bad-version",
|
|
"nonzero",
|
|
"overflow",
|
|
"wait",
|
|
"oversized-tool",
|
|
"invalid-json",
|
|
])("retains field discovery failure facts: %s", async (mode) => {
|
|
const f = await fixture(mode);
|
|
const result = await f.service.inspectFields(
|
|
{},
|
|
{ signal: f.controller.signal },
|
|
);
|
|
if (result.ok) throw new Error("Expected failure");
|
|
expect(result.error).toMatchObject({ operation: "inspect_packet_fields" });
|
|
if (mode === "bad-version")
|
|
expect(result.error.capturedOutput).toMatchObject({
|
|
stdout: "TShark (Wireshark) 3.0.0\n",
|
|
stderr: "version warning\n",
|
|
});
|
|
if (mode === "invalid-json")
|
|
expect(result.error.capturedOutput?.stdout).toBe("not json\n");
|
|
if (mode === "overflow") {
|
|
expect(result.error).toBeInstanceOf(AnalysisResourceConstraintError);
|
|
if (!(result.error instanceof AnalysisResourceConstraintError))
|
|
throw new Error("Expected command-output allowance refusal");
|
|
expect(result.error).toMatchObject({
|
|
resource: "memory",
|
|
reportedLimits: { output_bytes: 32 * 1024 * 1024 },
|
|
retryable: false,
|
|
});
|
|
expect(analysisErrorRemediationAction(result.error)).toContain(
|
|
"The retained command-output allowance",
|
|
);
|
|
expect(analysisErrorRemediationAction(result.error)).not.toMatch(
|
|
/Review the reported worker memory limits/u,
|
|
);
|
|
}
|
|
});
|
|
|
|
it("retains completed discovery on late cancellation and cleanup failure", async () => {
|
|
for (const mode of ["post-run-abort", "cleanup"]) {
|
|
const f = await fixture(mode, mode === "cleanup");
|
|
const result = await f.service.inspectFields(
|
|
{ query: "usb" },
|
|
{ signal: f.controller.signal },
|
|
);
|
|
expect(result).toMatchObject({
|
|
ok: false,
|
|
error: {
|
|
partialObservation: {
|
|
operation: "inspect_packet_fields",
|
|
normalized_result: { records: [{ abbreviation: "usb" }] },
|
|
},
|
|
},
|
|
});
|
|
if (mode === "cleanup") {
|
|
await f.service.inspectFields({});
|
|
await f.service.inspect({ path: f.path, fields: ["ip.addr"] });
|
|
expect(f.calls()).toBe(2);
|
|
for (let attempt = 0; attempt < 2; attempt++)
|
|
await expect(f.service.close()).rejects.toMatchObject({
|
|
operation: "inspect_packet_fields",
|
|
cleanupIncomplete: true,
|
|
});
|
|
}
|
|
f.allowCleanup();
|
|
await f.service.close();
|
|
}
|
|
});
|
|
|
|
it("preserves discovery resource ownership through CLI shutdown retries", async () => {
|
|
const f = await fixture("cleanup", true);
|
|
const cli = Cli.create("discovery-cleanup", { sync: false });
|
|
registerCaptureCommands(cli, silentLogger, {}, f.service);
|
|
let stdout = "";
|
|
try {
|
|
await cli.serve(
|
|
["inspect-packet-fields", "--query", "usb", "--format", "json"],
|
|
{
|
|
stdout: (text) => {
|
|
stdout += text;
|
|
},
|
|
exit: () => {},
|
|
},
|
|
);
|
|
expect(JSON.parse(stdout)).toMatchObject({
|
|
code: "cleanup_incomplete",
|
|
details: {
|
|
diagnostics: {
|
|
cleanup_error: {
|
|
details: { operation: "inspect_packet_fields" },
|
|
},
|
|
},
|
|
},
|
|
});
|
|
} finally {
|
|
f.allowCleanup();
|
|
await f.service.close();
|
|
}
|
|
});
|
|
|
|
it.each(
|
|
[false, true].flatMap((failsClose) =>
|
|
[false, true].map((discovery) => ({ failsClose, discovery })),
|
|
),
|
|
)(
|
|
"retains completed Evidence and exact cancellation during workspace close: %j",
|
|
async ({ failsClose, discovery }) => {
|
|
const f = await fixture("close-abort", failsClose);
|
|
const result = discovery
|
|
? await f.service.inspectFields({}, { signal: f.controller.signal })
|
|
: await f.service.inspect(
|
|
{ path: f.path, fields: ["ip.addr"] },
|
|
{ signal: f.controller.signal },
|
|
);
|
|
if (result.ok) throw new Error("Expected finalization cancellation");
|
|
const primary = failsClose ? result.error.cleanupPrimary : result.error;
|
|
expect(primary).toMatchObject({ _tag: "AnalysisCancelledError" });
|
|
expect(primary?.cause).toBe(f.controller.signal.reason);
|
|
expect(result.error.partialObservation).toMatchObject({
|
|
operation: discovery ? "inspect_packet_fields" : "inspect_packet_capture",
|
|
});
|
|
expect(result.error.cleanupIncomplete).toBe(failsClose);
|
|
if (failsClose)
|
|
await expect(f.service.close()).rejects.toMatchObject({
|
|
cleanupIncomplete: true,
|
|
});
|
|
f.allowCleanup();
|
|
await f.service.close();
|
|
expect(result.error.cleanupIncomplete).toBe(false);
|
|
expect(primary?.cause).toBe(f.controller.signal.reason);
|
|
},
|
|
);
|