180 lines
6.4 KiB
TypeScript
180 lines
6.4 KiB
TypeScript
import { createHash } from "node:crypto";
|
|
import { readFile, rm, writeFile } from "node:fs/promises";
|
|
import { join } from "node:path";
|
|
import { expect } from "vitest";
|
|
import { artifactInspectionResultSchema } from "../../src/domain/artifactInspection.js";
|
|
import {
|
|
artifactInventoryResultSchema,
|
|
artifactExtractionResultSchema,
|
|
} from "../../src/domain/artifactGraph.js";
|
|
import { parseEvidence } from "../../src/domain/evidence.js";
|
|
import { cliTest } from "../support/cli/cliFixture.js";
|
|
import { createTestTempDirectory } from "../fixtures/temporaryDirectory.js";
|
|
import { connectLocalToolsMcp } from "../fixtures/localToolsMcp.js";
|
|
import { parseMcpToolError } from "../fixtures/mcpToolError.js";
|
|
import { inesFixture } from "../fixtures/ines.js";
|
|
|
|
const inventoryFrom = (input: unknown) => {
|
|
const evidence = parseEvidence(input);
|
|
expect(evidence.subject).toMatchObject({
|
|
format: "ines",
|
|
architecture: null,
|
|
});
|
|
const inspection = artifactInspectionResultSchema.parse(
|
|
evidence.normalized_result,
|
|
);
|
|
expect(inspection.coverage.status).toBe("complete-within-substeps");
|
|
expect(inspection.observations.map(({ kind }) => kind)).toEqual([
|
|
"root-manifest",
|
|
"container-header",
|
|
"reconstruction",
|
|
]);
|
|
const substep = inspection.substeps[0];
|
|
if (substep === undefined) throw new Error("Missing inventory substep");
|
|
parseEvidence(substep.evidence);
|
|
return artifactInventoryResultSchema.parse(
|
|
substep.evidence.normalized_result,
|
|
);
|
|
};
|
|
|
|
cliTest(
|
|
"classic iNES has the same complete byte inventory through compiled CLI and stdio MCP",
|
|
async ({ cli }) => {
|
|
const root = await createTestTempDirectory("rea-ines-public-");
|
|
const path = join(root, "卡带.data");
|
|
const bytes = inesFixture({
|
|
trainer: true,
|
|
trailing: Buffer.from([0, 255, 3, 0]),
|
|
});
|
|
await writeFile(path, bytes);
|
|
const cliResult = await cli.run({
|
|
arguments: ["inspect-artifact", path, "--json"],
|
|
});
|
|
expect(cliResult.exitCode, cliResult.stderr).toBe(0);
|
|
const inventory = inventoryFrom(cliResult.json);
|
|
const digest = createHash("sha256").update(bytes).digest("hex");
|
|
expect(inventory.ines).toMatchObject({
|
|
header: {
|
|
mapper: 19,
|
|
trainer_present: true,
|
|
prg_rom_units_16k: 2,
|
|
chr_rom_units_8k: 1,
|
|
},
|
|
reconstruction: { bytes: bytes.length, sha256: digest, verified: true },
|
|
});
|
|
const expected = [
|
|
["header.bin", 0, 16],
|
|
["trainer.bin", 16, 512],
|
|
["prg/000.bin", 528, 16_384],
|
|
["prg/001.bin", 16_912, 16_384],
|
|
["chr/000.bin", 33_296, 8_192],
|
|
["trailing.bin", 41_488, 4],
|
|
] as const;
|
|
const nodes = new Map(
|
|
inventory.nodes.map((node) => [node.artifact_id, node]),
|
|
);
|
|
for (const [logical_path, offset, length] of expected) {
|
|
const occurrence = inventory.occurrences.find(
|
|
(item) => item.logical_path === logical_path,
|
|
);
|
|
if (
|
|
occurrence?.artifact_id === undefined ||
|
|
occurrence.artifact_id === null
|
|
)
|
|
throw new Error(`Missing region ${logical_path}`);
|
|
expect(occurrence.source_location).toEqual({ offset, length });
|
|
expect(nodes.get(occurrence.artifact_id)?.sha256).toBe(
|
|
createHash("sha256")
|
|
.update(bytes.subarray(offset, offset + length))
|
|
.digest("hex"),
|
|
);
|
|
}
|
|
|
|
const { call } = await connectLocalToolsMcp();
|
|
const opened = await call("open_binary", { path });
|
|
if (opened.isError)
|
|
throw new Error(JSON.stringify(parseMcpToolError(opened)));
|
|
const inspected = await call("inspect_artifact", {});
|
|
expect(
|
|
inspected.isError,
|
|
JSON.stringify(inspected.structuredContent),
|
|
).not.toBe(true);
|
|
expect(inventoryFrom(inspected.structuredContent)).toEqual(inventory);
|
|
const extracted = await call("extract_artifact", {});
|
|
expect(
|
|
extracted.isError,
|
|
JSON.stringify(extracted.structuredContent),
|
|
).not.toBe(true);
|
|
const extraction = artifactExtractionResultSchema.parse(
|
|
parseEvidence(extracted.structuredContent).normalized_result,
|
|
);
|
|
try {
|
|
expect(extraction.artifacts).toHaveLength(expected.length);
|
|
expect(
|
|
Buffer.concat(
|
|
await Promise.all(
|
|
expected.map(([region]) =>
|
|
readFile(join(extraction.output_root, region)),
|
|
),
|
|
),
|
|
),
|
|
).toEqual(bytes);
|
|
} finally {
|
|
// The successful extraction transfers this exact output directory to the caller.
|
|
await rm(extraction.output_root, { recursive: true, force: true });
|
|
}
|
|
await call("close_binary", {});
|
|
expect(await readFile(path)).toEqual(bytes);
|
|
},
|
|
);
|
|
|
|
cliTest(
|
|
"unsupported NES containers fail explicitly and leave a valid MCP session usable",
|
|
async ({ cli }) => {
|
|
const root = await createTestTempDirectory("rea-ines-public-refusal-");
|
|
const validPath = join(root, "valid.nes");
|
|
await writeFile(validPath, inesFixture({ chrUnits: 0 }));
|
|
const { call } = await connectLocalToolsMcp();
|
|
const opened = await call("open_binary", { path: validPath });
|
|
if (opened.isError)
|
|
throw new Error(JSON.stringify(parseMcpToolError(opened)));
|
|
for (const [name, bytes, reason] of [
|
|
["truncated", inesFixture().subarray(0, 20), "truncated iNES payload"],
|
|
[
|
|
"nes2",
|
|
(() => {
|
|
const bytes = inesFixture();
|
|
bytes[7] = 8;
|
|
return bytes;
|
|
})(),
|
|
"nes-2.0",
|
|
],
|
|
["unif", Buffer.from("UNIF"), "unif"],
|
|
] as const) {
|
|
const path = join(root, `${name}.nes`);
|
|
await writeFile(path, bytes);
|
|
const refused = await cli.run({
|
|
arguments: ["inspect-artifact", path, "--json"],
|
|
});
|
|
expect(refused.exitCode).not.toBe(0);
|
|
expect(JSON.stringify(refused.json)).toContain(reason);
|
|
const switched = await call("open_binary", { path });
|
|
expect(switched.isError).toBe(true);
|
|
expect(JSON.stringify(parseMcpToolError(switched))).toContain(reason);
|
|
}
|
|
const retained = await call("inspect_artifact", {});
|
|
expect(
|
|
retained.isError,
|
|
JSON.stringify(retained.structuredContent),
|
|
).not.toBe(true);
|
|
const inventory = inventoryFrom(retained.structuredContent);
|
|
expect(inventory.ines?.header.chr_rom_units_8k).toBe(0);
|
|
expect(
|
|
inventory.occurrences.some(({ logical_path }) =>
|
|
logical_path.startsWith("chr/"),
|
|
),
|
|
).toBe(false);
|
|
await call("close_binary", {});
|
|
},
|
|
);
|