fix(pcap): retain producer contradiction diagnostics (#7937)

* fix(pcap): retain producer contradiction diagnostics

* test(pcap): align DNS fixture with empty captures
This commit is contained in:
morluto authored and GitHub committed 2026-10-29 08:43:14 +08:00
1 parent c92322b473
commit 0386efd1e1
4 files changed
+118 -1

No files matched your search

+14
View File
@@ -1007,6 +1007,13 @@ export class PcapProvider {
if (metadata.sha256 !== null && metadata.sha256 !== identity.sha256)
throw new AnalysisProtocolError(
"capinfos SHA256 disagrees with the owned capture snapshot",
{
diagnostics: {
reason: "capture_sha256_mismatch",
capinfos_sha256: metadata.sha256,
snapshot_sha256: identity.sha256,
},
},
);
} catch (cause) {
throw withOutputCleanup(cause, info);
@@ -1119,6 +1126,13 @@ export class PcapProvider {
if (fields.packetCount !== metadata.packetCount)
throw new AnalysisProtocolError(
`capinfos counted ${String(metadata.packetCount)} packets but tshark returned ${String(fields.packetCount)}`,
{
diagnostics: {
reason: "packet_count_mismatch",
capinfos_packet_count: metadata.packetCount,
tshark_packet_count: fields.packetCount,
},
},
);
const captured = {
result: {
@@ -1,4 +1,6 @@
import { expect } from "vitest";
import { writeFile } from "node:fs/promises";
import { join } from "node:path";
import { toolContract } from "../../src/contracts/toolContracts.js";
import { parseEvidence } from "../../src/domain/evidence.js";
@@ -47,6 +49,46 @@ const environmentFor = (
REA_CAPINFOS_COMMAND: tools.capinfos,
});
cliTest.skipIf(process.platform !== "linux")(
"preserves contradictory packet counts through CLI and MCP",
async ({ cli }) => {
const root = await createTestTempDirectory("rea-pcap-count-mismatch-");
const path = join(root, "capture.pcap");
await writeFile(path, Buffer.from("pcap-fixture-bytes"));
const { tshark, capinfos } = await writeStubTools(root, {
tsharkJson: "[]",
capinfosText:
"File type: pcap\nFile encapsulation: Ethernet\nNumber of packets: 1\n",
});
const environment = environmentFor(root, { tshark, capinfos });
const response = await cli.run({
arguments: ["inspect-pcap-capture", path, "--json"],
environment,
});
expect(response.exitCode, response.stdout).toBe(1);
expect(response.json).toMatchObject({
code: "unreadable_output",
details: {
diagnostics: {
reason: "packet_count_mismatch",
capinfos_packet_count: 1,
tshark_packet_count: 0,
},
},
});
const { client, call } = await connectLocalToolsMcp(environment);
try {
const reply = await call("inspect_pcap_capture", { path });
expect(reply.isError).toBe(true);
const text = reply.content.find((item) => item.type === "text");
if (text?.type !== "text") throw new Error("Missing MCP error text");
expect(JSON.parse(text.text)).toEqual({ error: response.json });
} finally {
await client.close();
}
},
);
cliTest.skipIf(process.platform === "win32")(
"returns the same unreadable-output diagnostic for invalid version streams through CLI and stdio MCP",
async ({ cli }) => {
+60
View File
@@ -158,6 +158,66 @@ it("inspects a capture with endpoints, DNS, TLS SNI, and HTTP", async () => {
expect(result.coverage).toBe("complete");
});
it.each([0, 1])(
"preserves packet-count mismatch facts while accepting an empty capture: capinfos=%s",
async (count) => {
const { provider, root } = await providerWith({
tsharkJson: "[]",
dnsJson: "[]",
capinfosText: `File type: pcap\nFile encapsulation: Ethernet\nNumber of packets: ${String(count)}\n`,
});
const path = await captureFixture(root);
const result = await provider.execute({
operation: "inspect_pcap_capture",
input: { path },
});
if (count === 0) {
if (!result.ok) throw result.error;
expect(result.value.result).toMatchObject({
packet_count: 0,
coverage: "complete",
});
} else {
if (result.ok) throw new Error("Expected contradictory counts");
expect(projectAnalysisError(result.error)).toMatchObject({
code: "unreadable_output",
details: {
diagnostics: {
reason: "packet_count_mismatch",
capinfos_packet_count: 1,
tshark_packet_count: 0,
},
},
});
}
},
);
it("retains both observed digests when capinfos disagrees with the snapshot", async () => {
const reported = "0".repeat(64);
const { provider, root } = await providerWith({
capinfosText: `File type: pcap\nFile encapsulation: Ethernet\nNumber of packets: 0\nSHA256: ${reported}\n`,
});
const path = await captureFixture(root);
const result = await provider.execute({
operation: "inspect_pcap_capture",
input: { path },
});
if (result.ok) throw new Error("Expected contradictory snapshot digest");
expect(projectAnalysisError(result.error)).toMatchObject({
code: "unreadable_output",
details: {
diagnostics: {
reason: "capture_sha256_mismatch",
capinfos_sha256: reported,
snapshot_sha256: createHash("sha256")
.update("pcap-fixture-bytes")
.digest("hex"),
},
},
});
});
it("retains an absolute HTTP request target without a Host-derived full URI", async () => {
const { provider, root } = await providerWith({ httpAbsoluteUriOnly: true });
const path = await captureFixture(root);
+2 -1
View File
@@ -93,6 +93,7 @@ export const TSHARK_DNS_JSON = JSON.stringify([
export interface PcapStubToolsOptions {
readonly failCapinfos?: boolean;
readonly malformedDnsJson?: boolean;
readonly dnsJson?: string;
readonly tsharkJson?: string;
readonly capinfosText?: string;
readonly httpAbsoluteUriOnly?: boolean;
@@ -142,7 +143,7 @@ case " $* " in
case " $* " in *" --no-duplicate-keys "*) ;; *) exit 2 ;; esac
case " $* " in *" -J frame dns mdns llmnr "*) ;; *) exit 2 ;; esac
case " $* " in *" -Y dns || mdns || llmnr "*) ;; *) exit 2 ;; esac
printf '%s' '${(options.malformedDnsJson ? "{" : TSHARK_DNS_JSON).replace(/'/gu, "'\\''")}'
printf '%s' '${(options.malformedDnsJson ? "{" : (options.dnsJson ?? TSHARK_DNS_JSON)).replace(/'/gu, "'\\''")}'
exit 0
;;
*)