mirror of
https://github.com/morluto/rea
synced 2026-10-26 06:16:17 -04:00
40c235c642
Preserve format and Evidence semantics at their owning boundaries, with focused regression coverage and documentation where applicable. Fixes #6774
269 lines
8.7 KiB
TypeScript
269 lines
8.7 KiB
TypeScript
import { Client, InMemoryTransport } from "@modelcontextprotocol/client";
|
|
import { afterAll, beforeAll, describe, expect, it } from "vitest";
|
|
|
|
import { FUNCTION_COMPARISON_EXAMPLE } from "../../../src/contracts/functionComparisonExample.js";
|
|
import { createEvidence, parseEvidence } from "../../../src/domain/evidence.js";
|
|
import { functionDossierSchema } from "../../../src/domain/hopperValues.js";
|
|
import {
|
|
functionComparisonResultSchema,
|
|
type FunctionComparisonResult as Comparison,
|
|
} from "../../../src/domain/functionComparisonSchemas.js";
|
|
import { jsonValueSchema } from "../../../src/domain/jsonValue.js";
|
|
import { createServer } from "../../../src/server/createServer.js";
|
|
import { createTestBinarySession } from "../../fixtures/binarySession.js";
|
|
|
|
const template = FUNCTION_COMPARISON_EXAMPLE.left;
|
|
const base = functionDossierSchema.parse(template.normalized_result);
|
|
|
|
const evidence = (
|
|
id: string,
|
|
strings: readonly (typeof base.referenced_strings)[number][],
|
|
literalValue?: string,
|
|
) =>
|
|
createEvidence(
|
|
{
|
|
path: `/fixture/function-string-decoding/${id}`,
|
|
sha256: "1".repeat(64),
|
|
format: "mach-o",
|
|
},
|
|
template.provider,
|
|
{
|
|
operation: "analyze_function",
|
|
parameters: { ...template.parameters, observation: id },
|
|
result: jsonValueSchema.parse({
|
|
...base,
|
|
referenced_strings: [...strings],
|
|
swift_string_literals:
|
|
literalValue === undefined
|
|
? []
|
|
: [
|
|
{
|
|
source_address: "0x1002",
|
|
value: literalValue,
|
|
registers: ["x0", "x1"],
|
|
words: ["0x1", "0x2"],
|
|
representation: "small",
|
|
address: null,
|
|
},
|
|
],
|
|
swift_string_literal_omissions: [],
|
|
}),
|
|
confidence: template.confidence,
|
|
authority: template.authority,
|
|
relationshipCoverage: { status: "complete" },
|
|
},
|
|
);
|
|
|
|
const reference = (
|
|
value: string,
|
|
options: {
|
|
readonly decoding?: { readonly available: false; readonly reason: string };
|
|
readonly termination?: "missing" | "present_or_not_required";
|
|
readonly byteLength?: number;
|
|
readonly sourceAddress?: string;
|
|
readonly metadata?: boolean;
|
|
readonly encodingStatus?: "observed" | "inferred";
|
|
} = {},
|
|
) => ({
|
|
address: "0x2000",
|
|
source_address: options.sourceAddress ?? "0x1000",
|
|
value,
|
|
...(options.decoding === undefined ? {} : { decoding: options.decoding }),
|
|
...(options.metadata === false
|
|
? {}
|
|
: {
|
|
string: {
|
|
encoding: "utf-8",
|
|
...(options.encodingStatus === undefined
|
|
? {}
|
|
: { encoding_status: options.encodingStatus }),
|
|
termination: options.termination ?? "present_or_not_required",
|
|
byte_length: options.byteLength ?? 6,
|
|
},
|
|
}),
|
|
});
|
|
|
|
type Compare = (
|
|
left: ReturnType<typeof evidence>,
|
|
right: ReturnType<typeof evidence>,
|
|
) => Promise<Comparison>;
|
|
|
|
const session = createTestBinarySession(() => {
|
|
throw new Error("Function comparison must not launch a provider");
|
|
});
|
|
const server = createServer({ kind: "session", session });
|
|
const client = new Client({
|
|
name: "function-string-decoding-test",
|
|
version: "1",
|
|
});
|
|
const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair();
|
|
let compare: Compare;
|
|
|
|
beforeAll(async () => {
|
|
await server.connect(serverTransport);
|
|
await client.connect(clientTransport);
|
|
compare = async (left, right) => {
|
|
const response = await client.callTool({
|
|
name: "compare_functions",
|
|
arguments: { left, right },
|
|
});
|
|
expect(
|
|
response.isError,
|
|
JSON.stringify(response.structuredContent),
|
|
).not.toBe(true);
|
|
return functionComparisonResultSchema.parse(
|
|
parseEvidence(response.structuredContent).normalized_result,
|
|
);
|
|
};
|
|
});
|
|
|
|
afterAll(async () => {
|
|
await Promise.allSettled([client.close(), server.close(), session.close()]);
|
|
});
|
|
|
|
const stringsNames = (result: Comparison) =>
|
|
result.dimensions.find(({ dimension }) => dimension === "strings_names");
|
|
|
|
describe("compare_functions referenced string decoding", () => {
|
|
const reason = "The native string bytes are unavailable";
|
|
const undecodable = (id: string, display: string) =>
|
|
evidence(id, [
|
|
reference(display, { decoding: { available: false, reason } }),
|
|
]);
|
|
|
|
it("keeps unavailable previews unknown even when their display differs", async () => {
|
|
const samePreview = await compare(
|
|
undecodable("1", "display preview"),
|
|
undecodable("2", "display preview"),
|
|
);
|
|
expect(samePreview.status).toBe("unknown");
|
|
expect(stringsNames(samePreview)).toMatchObject({
|
|
status: "unknown",
|
|
limitations: [`Referenced string decoding unavailable: ${reason}`],
|
|
});
|
|
|
|
const differentPreview = await compare(
|
|
undecodable("3", "first display preview"),
|
|
undecodable("4", "second display preview"),
|
|
);
|
|
expect(differentPreview.status).toBe("unknown");
|
|
});
|
|
|
|
it("compares known typed string facts alongside unavailable content", async () => {
|
|
const metadataChange = await compare(
|
|
evidence("9", [
|
|
reference("display preview", {
|
|
decoding: { available: false, reason },
|
|
byteLength: 6,
|
|
}),
|
|
]),
|
|
evidence("a", [
|
|
reference("same display preview", {
|
|
decoding: { available: false, reason },
|
|
byteLength: 7,
|
|
}),
|
|
]),
|
|
);
|
|
expect(metadataChange.status).toBe("changed");
|
|
|
|
const mixedKnownChange = await compare(
|
|
evidence("b", [
|
|
reference("uncertain display", {
|
|
decoding: { available: false, reason },
|
|
sourceAddress: "0x1000",
|
|
}),
|
|
reference("before", { sourceAddress: "0x1001" }),
|
|
]),
|
|
evidence("c", [
|
|
reference("uncertain display", {
|
|
decoding: { available: false, reason },
|
|
sourceAddress: "0x1000",
|
|
}),
|
|
reference("after", { sourceAddress: "0x1001" }),
|
|
]),
|
|
);
|
|
expect(mixedKnownChange.status).toBe("changed");
|
|
});
|
|
|
|
it("retains decoder reasons when strings share a source site", async () => {
|
|
const reason = "Typed string bytes could not be read";
|
|
const rows = [
|
|
reference("display preview", {
|
|
decoding: { available: false, reason },
|
|
}),
|
|
reference("known content"),
|
|
];
|
|
const result = await compare(
|
|
evidence("repeated-left", rows),
|
|
evidence("repeated-right", rows),
|
|
);
|
|
expect(stringsNames(result)).toMatchObject({
|
|
status: "unknown",
|
|
limitations: expect.arrayContaining([
|
|
`Referenced string decoding unavailable: ${reason}`,
|
|
]),
|
|
});
|
|
});
|
|
|
|
it("compares decoded content and termination facts", async () => {
|
|
const decoded = (
|
|
id: string,
|
|
termination:
|
|
| "missing"
|
|
| "present_or_not_required" = "present_or_not_required",
|
|
byteLength = 6,
|
|
) => evidence(id, [reference("hello", { termination, byteLength })]);
|
|
const equal = await compare(decoded("5"), decoded("6"));
|
|
expect(equal.status).toBe("unchanged");
|
|
expect(stringsNames(equal)).toMatchObject({
|
|
status: "unchanged",
|
|
left_count: 1,
|
|
right_count: 1,
|
|
left_digest: expect.stringMatching(/^[a-f0-9]{64}$/u),
|
|
});
|
|
expect(stringsNames(equal)?.right_digest).toBe(
|
|
stringsNames(equal)?.left_digest,
|
|
);
|
|
|
|
const different = await compare(decoded("7"), decoded("8", "missing", 5));
|
|
expect(different.status).toBe("changed");
|
|
expect(stringsNames(different)).toMatchObject({ status: "changed" });
|
|
});
|
|
|
|
it("preserves paired metadata absence and ignores encoding provenance", async () => {
|
|
const missingMetadata = await compare(
|
|
evidence("d", [reference("same", { metadata: false })]),
|
|
evidence("e", [reference("same")]),
|
|
);
|
|
expect(missingMetadata.status).toBe("unknown");
|
|
|
|
const absentMetadata = await compare(
|
|
evidence("g", [reference("same", { metadata: false })]),
|
|
evidence("h", [reference("same", { metadata: false })]),
|
|
);
|
|
expect(absentMetadata.status).toBe("unchanged");
|
|
|
|
const provenanceOnly = await compare(
|
|
evidence("f", [reference("same", { encodingStatus: "observed" })]),
|
|
evidence("0", [reference("same", { encodingStatus: "inferred" })]),
|
|
);
|
|
expect(provenanceOnly.status).toBe("unchanged");
|
|
});
|
|
|
|
it("keeps literal changes in the combined string dimension", async () => {
|
|
const changed = await compare(
|
|
evidence("literal-before", [reference("hello")], "literal one"),
|
|
evidence("literal-after", [reference("hello")], "literal two"),
|
|
);
|
|
expect(changed.status).toBe("changed");
|
|
expect(stringsNames(changed)).toMatchObject({
|
|
status: "changed",
|
|
left_count: 2,
|
|
right_count: 2,
|
|
});
|
|
expect(stringsNames(changed)?.left_digest).not.toBe(
|
|
stringsNames(changed)?.right_digest,
|
|
);
|
|
});
|
|
});
|