mirror of
https://github.com/morluto/rea
synced 2026-10-26 06:16:17 -04:00
302 lines
10 KiB
TypeScript
302 lines
10 KiB
TypeScript
import { create, toBinary } from "@bufbuild/protobuf";
|
|
import {
|
|
DescriptorProtoSchema,
|
|
FieldDescriptorProtoSchema,
|
|
FileDescriptorProtoSchema,
|
|
FileDescriptorSetSchema,
|
|
FileOptionsSchema,
|
|
} from "@bufbuild/protobuf/wkt";
|
|
import { BinaryWriter, WireType } from "@bufbuild/protobuf/wire";
|
|
import { createHash } from "node:crypto";
|
|
import { readFile, writeFile } from "node:fs/promises";
|
|
import { join } from "node:path";
|
|
import { expect } from "vitest";
|
|
import { z } from "zod";
|
|
|
|
import { analysisViewResultSchema } from "../../../src/domain/analysisView/analysisView.js";
|
|
import { parseEvidence } from "../../../src/domain/evidence.js";
|
|
import { webNetworkCaptureSchema } from "../../../src/domain/webNetworkCapture.js";
|
|
import { parseMcpToolError } from "../../fixtures/mcpToolError.js";
|
|
import { protobufHar } from "../../fixtures/protobufCapture.js";
|
|
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
|
|
import { cliTest } from "../../support/cli/cliFixture.js";
|
|
|
|
const sha256 = (bytes: Uint8Array) =>
|
|
createHash("sha256").update(bytes).digest("hex");
|
|
const body = Buffer.from("0a020801", "hex"); // Message { payload { value=1 } }
|
|
const descriptorDefinition = () =>
|
|
create(FileDescriptorSetSchema, {
|
|
file: [
|
|
{
|
|
name: "shared.proto",
|
|
package: "capture",
|
|
syntax: "proto3",
|
|
messageType: [
|
|
{
|
|
name: "Payload",
|
|
field: [{ name: "value", number: 1, type: 5, label: 1 }],
|
|
},
|
|
],
|
|
},
|
|
{
|
|
name: "entry.proto",
|
|
package: "entry",
|
|
syntax: "proto3",
|
|
dependency: ["shared.proto"],
|
|
messageType: [
|
|
{
|
|
name: "Message",
|
|
field: [
|
|
{
|
|
name: "payload",
|
|
number: 1,
|
|
type: 11,
|
|
typeName: ".capture.Payload",
|
|
label: 1,
|
|
},
|
|
],
|
|
},
|
|
],
|
|
},
|
|
],
|
|
});
|
|
|
|
const conflictingFiles = (change: string, reverse: boolean) => {
|
|
const definition = descriptorDefinition();
|
|
const shared = definition.file[0]!;
|
|
const duplicate = structuredClone(shared);
|
|
let captured = body;
|
|
if (change === "declarations") duplicate.messageType[0]!.name = "Other";
|
|
else if (change === "package") duplicate.package = "other";
|
|
else if (change === "imports") {
|
|
definition.file.unshift(
|
|
create(FileDescriptorProtoSchema, { name: "a.proto", syntax: "proto3" }),
|
|
create(FileDescriptorProtoSchema, { name: "b.proto", syntax: "proto3" }),
|
|
);
|
|
shared.messageType = [];
|
|
duplicate.messageType = [];
|
|
shared.dependency = ["a.proto"];
|
|
duplicate.dependency = ["b.proto"];
|
|
definition.file.at(-1)!.messageType[0]!.field[0] = create(
|
|
FieldDescriptorProtoSchema,
|
|
{ name: "value", number: 1, type: 5, label: 1 },
|
|
);
|
|
captured = Buffer.from([8, 1]);
|
|
} else if (change === "presence") {
|
|
shared.options = create(FileOptionsSchema);
|
|
duplicate.options = create(FileOptionsSchema, { javaMultipleFiles: false });
|
|
} else if (change === "opaque-file") {
|
|
duplicate.$unknown = [
|
|
{ no: 50000, wireType: WireType.Varint, data: Uint8Array.of(1) },
|
|
];
|
|
} else {
|
|
shared.options = create(FileOptionsSchema);
|
|
duplicate.options = create(FileOptionsSchema);
|
|
shared.options.$unknown = [
|
|
{ no: 50000, wireType: WireType.Varint, data: Uint8Array.of(1) },
|
|
];
|
|
duplicate.options.$unknown = [
|
|
{ no: 50000, wireType: WireType.Varint, data: Uint8Array.of(2) },
|
|
];
|
|
}
|
|
const sharedIndex = definition.file.indexOf(shared);
|
|
definition.file.splice(
|
|
sharedIndex,
|
|
1,
|
|
...(reverse ? [duplicate, shared] : [shared, duplicate]),
|
|
);
|
|
return { bytes: toBinary(FileDescriptorSetSchema, definition), captured };
|
|
};
|
|
|
|
const retainedDetailsSchema = z.object({
|
|
partial_observation: z.strictObject({
|
|
kind: z.literal("retained-evidence"),
|
|
evidence_id: z.string(),
|
|
}),
|
|
});
|
|
|
|
cliTest(
|
|
"refuses conflicting descriptor file identities before registry import lookup through CLI/MCP",
|
|
async ({ cli }) => {
|
|
const root = await createTestTempDirectory("rea-protobuf-file-conflicts-");
|
|
const call = await cli.sharedMcpCall!();
|
|
for (const change of [
|
|
"declarations",
|
|
"package",
|
|
"imports",
|
|
"presence",
|
|
"opaque-file",
|
|
"opaque-option",
|
|
])
|
|
for (const reverse of [false, true]) {
|
|
const { bytes, captured } = conflictingFiles(change, reverse);
|
|
const descriptor = join(root, `${change}-${reverse}.pb`);
|
|
const capture = join(root, `${change}-${reverse}.har`);
|
|
const text = protobufHar(captured, "application/x-protobuf");
|
|
await writeFile(descriptor, bytes);
|
|
await writeFile(capture, text);
|
|
const expected = {
|
|
code: "invalid_request",
|
|
details: {
|
|
issues: [
|
|
{
|
|
path: ["protobuf", "schema", "descriptor_set_path"],
|
|
reason: "invalid_format",
|
|
},
|
|
],
|
|
},
|
|
};
|
|
if (change === "declarations") {
|
|
const output = await cli.sharedCliRun!({
|
|
arguments: [
|
|
"inspect-web-network-capture",
|
|
capture,
|
|
"har",
|
|
"--record",
|
|
"0",
|
|
"--protobuf-schema",
|
|
descriptor,
|
|
"--protobuf-message",
|
|
"entry.Message",
|
|
"--json",
|
|
],
|
|
});
|
|
expect(output.exitCode).toBe(1);
|
|
expect(output.json).toMatchObject(expected);
|
|
}
|
|
const response = await call("inspect_web_network_capture", {
|
|
capture_path: capture,
|
|
format: "har",
|
|
record_ordinals: [0],
|
|
protobuf: {
|
|
schema: {
|
|
descriptor_set_path: descriptor,
|
|
message_type: "entry.Message",
|
|
},
|
|
},
|
|
});
|
|
const error = parseMcpToolError(response).error;
|
|
expect(error, `${change}-${reverse}`).toMatchObject(expected);
|
|
if (change === "declarations") {
|
|
const source = retainedDetailsSchema.parse(
|
|
error.details,
|
|
).partial_observation;
|
|
const retained = await call("inspect_analysis_view", {
|
|
source,
|
|
view: { kind: "summary" },
|
|
});
|
|
expect(retained.isError, JSON.stringify(retained)).not.toBe(true);
|
|
expect(
|
|
analysisViewResultSchema.parse(
|
|
parseEvidence(retained.structuredContent).normalized_result,
|
|
),
|
|
).toMatchObject({
|
|
summary: {
|
|
artifact: { path: capture, sha256: sha256(Buffer.from(text)) },
|
|
retained_records: 1,
|
|
protobuf_coverage: { status: "partial", inspected_records: 0 },
|
|
},
|
|
});
|
|
}
|
|
expect(await readFile(descriptor)).toEqual(Buffer.from(bytes));
|
|
expect(await readFile(capture, "utf8")).toBe(text);
|
|
}
|
|
},
|
|
);
|
|
|
|
/** Reorder known FileDescriptorProto fields without changing their facts. */
|
|
const reorderedSharedFile = () => {
|
|
const shared = descriptorDefinition().file[0]!;
|
|
return new BinaryWriter()
|
|
.tag(12, WireType.LengthDelimited)
|
|
.string(shared.syntax)
|
|
.tag(4, WireType.LengthDelimited)
|
|
.bytes(toBinary(DescriptorProtoSchema, shared.messageType[0]!))
|
|
.tag(2, WireType.LengthDelimited)
|
|
.string(shared.package)
|
|
.tag(1, WireType.LengthDelimited)
|
|
.string(shared.name)
|
|
.finish();
|
|
};
|
|
|
|
cliTest(
|
|
"coalesces equal descriptor file facts while preserving original descriptor identity through CLI/MCP",
|
|
async ({ cli }) => {
|
|
const root = await createTestTempDirectory("rea-protobuf-file-repeat-");
|
|
const capture = join(root, "capture.har");
|
|
await writeFile(capture, protobufHar(body, "application/x-protobuf"));
|
|
const call = await cli.sharedMcpCall!();
|
|
for (const mode of ["identical", "reordered", "opaque"]) {
|
|
const definition = descriptorDefinition();
|
|
const shared = definition.file[0]!;
|
|
if (mode === "opaque") {
|
|
shared.options = create(FileOptionsSchema);
|
|
shared.options.$unknown = [
|
|
{ no: 50000, wireType: WireType.Varint, data: Uint8Array.of(7) },
|
|
];
|
|
}
|
|
const duplicate =
|
|
mode === "reordered"
|
|
? reorderedSharedFile()
|
|
: toBinary(FileDescriptorProtoSchema, shared);
|
|
const writer = new BinaryWriter();
|
|
writer
|
|
.tag(1, WireType.LengthDelimited)
|
|
.bytes(toBinary(FileDescriptorProtoSchema, shared));
|
|
writer.tag(1, WireType.LengthDelimited).bytes(duplicate);
|
|
writer
|
|
.tag(1, WireType.LengthDelimited)
|
|
.bytes(toBinary(FileDescriptorProtoSchema, definition.file[1]!));
|
|
// Repeated imported facts after their consumer must coalesce as well.
|
|
writer.tag(1, WireType.LengthDelimited).bytes(duplicate);
|
|
const bytes = writer.finish();
|
|
const descriptor = join(root, `${mode}.pb`);
|
|
await writeFile(descriptor, bytes);
|
|
const output = await cli.sharedCliRun!({
|
|
arguments: [
|
|
"inspect-web-network-capture",
|
|
capture,
|
|
"har",
|
|
"--record",
|
|
"0",
|
|
"--protobuf-schema",
|
|
descriptor,
|
|
"--protobuf-message",
|
|
"entry.Message",
|
|
"--json",
|
|
],
|
|
});
|
|
expect(output.exitCode, output.stdout + output.stderr).toBe(0);
|
|
const evidence = parseEvidence(output.json);
|
|
const report = webNetworkCaptureSchema.parse(evidence.normalized_result);
|
|
expect(report.protobuf_coverage?.status).toBe("complete");
|
|
const decoded = report.records[0]?.protobuf?.[0];
|
|
expect(decoded?.schema).toMatchObject({
|
|
descriptor_set_path: descriptor,
|
|
descriptor_set_sha256: sha256(bytes),
|
|
descriptor_set_bytes: bytes.length,
|
|
});
|
|
expect(decoded?.fields).toMatchObject([
|
|
{ number: 1, schema_name: "payload", schema_type: "capture.Payload" },
|
|
{ number: 1, schema_name: "value", schema_value: "1" },
|
|
]);
|
|
const response = await call("inspect_web_network_capture", {
|
|
capture_path: capture,
|
|
format: "har",
|
|
record_ordinals: [0],
|
|
protobuf: {
|
|
schema: {
|
|
descriptor_set_path: descriptor,
|
|
message_type: "entry.Message",
|
|
},
|
|
},
|
|
});
|
|
expect(response.isError, JSON.stringify(response)).not.toBe(true);
|
|
expect(parseEvidence(response.structuredContent).evidence_id).toBe(
|
|
evidence.evidence_id,
|
|
);
|
|
expect(await readFile(descriptor)).toEqual(Buffer.from(bytes));
|
|
}
|
|
},
|
|
);
|