mirror of
https://github.com/morluto/rea.git
synced 2026-11-05 22:36:08 +00:00
253 lines
8.0 KiB
TypeScript
253 lines
8.0 KiB
TypeScript
import { mkdir, writeFile } from "node:fs/promises";
|
|
import { join } from "node:path";
|
|
|
|
import { describe, expect, it } from "vitest";
|
|
|
|
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
|
|
|
|
import { inventoryArtifact } from "../../../src/application/ArtifactInventory.js";
|
|
import { artifactInventoryResultSchema } from "../../../src/domain/artifactGraph.js";
|
|
import {
|
|
artifactComparisonResultSchema,
|
|
compareArtifacts,
|
|
} from "../../../src/domain/artifactComparison.js";
|
|
import { createEvidence } from "../../../src/domain/evidence.js";
|
|
import { jsonValueSchema } from "../../../src/domain/jsonValue.js";
|
|
|
|
const PROVIDER = {
|
|
id: "rea-artifact-graph",
|
|
name: "REA artifact graph",
|
|
version: "1",
|
|
} as const;
|
|
|
|
const observe = async (path: string) => {
|
|
const inventory = await inventoryArtifact(path);
|
|
return createEvidence(
|
|
{
|
|
path,
|
|
sha256: inventory.manifest.root_sha256,
|
|
format: inventory.manifest.root_format,
|
|
},
|
|
PROVIDER,
|
|
{
|
|
operation: "inventory_artifact",
|
|
parameters: {},
|
|
result: jsonValueSchema.parse(inventory),
|
|
confidence: "observed",
|
|
authority: "shipped-artifact",
|
|
},
|
|
);
|
|
};
|
|
|
|
const evidencePages = (
|
|
inventory: Awaited<ReturnType<typeof observe>>,
|
|
side: string,
|
|
) =>
|
|
Array.from({ length: 101 }, (_, index) =>
|
|
createEvidence(
|
|
{
|
|
path: `${side}-${String(index)}`,
|
|
sha256: inventory.subject?.digest.sha256 ?? "0".repeat(64),
|
|
format: inventory.subject?.format ?? "directory",
|
|
},
|
|
PROVIDER,
|
|
{
|
|
operation: "inventory_artifact",
|
|
parameters: { page: index, side },
|
|
result: jsonValueSchema.parse(inventory.normalized_result),
|
|
},
|
|
),
|
|
);
|
|
|
|
const changedArtifactComparison = async () => {
|
|
const parent = await createTestTempDirectory("rea-artifact-compare-");
|
|
const leftPath = join(parent, "left.app");
|
|
const rightPath = join(parent, "right.app");
|
|
await Promise.all([mkdir(leftPath), mkdir(rightPath)]);
|
|
await Promise.all([
|
|
writeFile(join(leftPath, "main.js"), "old();"),
|
|
writeFile(join(leftPath, "same.txt"), "same"),
|
|
writeFile(join(rightPath, "main.js"), "newer();"),
|
|
writeFile(join(rightPath, "same.txt"), "same"),
|
|
writeFile(join(rightPath, "added.txt"), "added"),
|
|
]);
|
|
const left = await observe(leftPath);
|
|
const right = await observe(rightPath);
|
|
return {
|
|
left,
|
|
right,
|
|
result: compareArtifacts(left, right),
|
|
};
|
|
};
|
|
|
|
describe("artifact comparison", () => {
|
|
it("classifies deterministic path changes and cites both inventories", async () => {
|
|
const firstObservation = await changedArtifactComparison();
|
|
const secondObservation = await changedArtifactComparison();
|
|
const first = firstObservation.result;
|
|
const second = secondObservation.result;
|
|
expect(first).toEqual(second);
|
|
expect(artifactComparisonResultSchema.parse(first)).toMatchObject({
|
|
status: "changed",
|
|
summary: { added: 1, changed: 2, unknown: 0 },
|
|
changes: expect.arrayContaining([
|
|
expect.objectContaining({ logical_path: "added.txt" }),
|
|
]),
|
|
});
|
|
expect(first.changes[0]?.evidence_links).toEqual([
|
|
firstObservation.left.evidence_id,
|
|
firstObservation.right.evidence_id,
|
|
]);
|
|
expect(first.changes).toEqual(
|
|
expect.arrayContaining([
|
|
expect.objectContaining({
|
|
logical_path: "added.txt",
|
|
classification: "added",
|
|
}),
|
|
expect.objectContaining({
|
|
logical_path: "main.js",
|
|
classification: "changed",
|
|
dimensions: expect.arrayContaining(["content", "size"]),
|
|
}),
|
|
]),
|
|
);
|
|
});
|
|
|
|
it("accepts all reported comparison dimensions", async () => {
|
|
const result = (await changedArtifactComparison()).result;
|
|
const firstChange = result.changes[0];
|
|
if (firstChange === undefined) throw new Error("Expected artifact change");
|
|
|
|
const dimensions = [
|
|
"content",
|
|
"kind",
|
|
"format",
|
|
"size",
|
|
"executable",
|
|
"relations",
|
|
"metadata",
|
|
"availability",
|
|
"integrity",
|
|
] as const;
|
|
expect(
|
|
artifactComparisonResultSchema.parse({
|
|
...result,
|
|
changes: [{ ...firstChange, dimensions }, ...result.changes.slice(1)],
|
|
}).changes[0]?.dimensions,
|
|
).toEqual(dimensions);
|
|
});
|
|
|
|
it("reports incomplete inventory as truncated, never unchanged", async () => {
|
|
const root = await createTestTempDirectory("rea-artifact-truncated-");
|
|
await writeFile(join(root, "one.txt"), "one");
|
|
const complete = await observe(root);
|
|
const completeInventory = artifactInventoryResultSchema.parse(
|
|
complete.normalized_result,
|
|
);
|
|
const incomplete = createEvidence(
|
|
{
|
|
path: root,
|
|
sha256: complete.subject?.digest.sha256 ?? "0".repeat(64),
|
|
format: "directory",
|
|
},
|
|
PROVIDER,
|
|
{
|
|
operation: "inventory_artifact",
|
|
parameters: {},
|
|
result: jsonValueSchema.parse({
|
|
...completeInventory,
|
|
nodes: [],
|
|
}),
|
|
confidence: "observed",
|
|
authority: "shipped-artifact",
|
|
limitations: ["Inventory node observations are unavailable."],
|
|
},
|
|
);
|
|
const comparison = compareArtifacts(incomplete, incomplete);
|
|
expect(comparison).toMatchObject({
|
|
status: "truncated",
|
|
summary: { unchanged: 0, unknown: 2 },
|
|
});
|
|
expect(comparison.limitations).toContain(
|
|
"Left artifact inventory is incomplete.",
|
|
);
|
|
});
|
|
});
|
|
|
|
describe("artifact comparison completeness", () => {
|
|
it("compares every graph member for inventories larger than 500 entries", async () => {
|
|
const root = await createTestTempDirectory("rea-artifact-pages-");
|
|
await Promise.all(
|
|
Array.from({ length: 501 }, async (_, index) =>
|
|
writeFile(
|
|
join(root, `file-${String(index).padStart(3, "0")}.txt`),
|
|
String(index),
|
|
),
|
|
),
|
|
);
|
|
const complete = await observe(root);
|
|
expect(complete.normalized_result).toMatchObject({
|
|
nodes: expect.arrayContaining([
|
|
expect.objectContaining({ kind: expect.any(String) }),
|
|
]),
|
|
occurrences: expect.any(Array),
|
|
edges: expect.any(Array),
|
|
});
|
|
expect(compareArtifacts(complete, complete)).toMatchObject({
|
|
status: "unchanged",
|
|
summary: { unchanged: 502, unknown: 0 },
|
|
changes: [],
|
|
});
|
|
}, 15_000);
|
|
|
|
it("retains every inventory citation when comparing many evidence pages", async () => {
|
|
const parent = await createTestTempDirectory(
|
|
"rea-artifact-pages-evidence-",
|
|
);
|
|
const leftPath = join(parent, "left.app");
|
|
const rightPath = join(parent, "right.app");
|
|
await Promise.all([mkdir(leftPath), mkdir(rightPath)]);
|
|
await Promise.all([
|
|
writeFile(join(leftPath, "main.js"), "left();"),
|
|
writeFile(join(rightPath, "main.js"), "right();"),
|
|
]);
|
|
|
|
const leftInventory = await observe(leftPath);
|
|
const rightInventory = await observe(rightPath);
|
|
const comparison = compareArtifacts(
|
|
evidencePages(leftInventory, "left"),
|
|
evidencePages(rightInventory, "right"),
|
|
);
|
|
|
|
expect(comparison.changes[0]?.evidence_links).toHaveLength(202);
|
|
});
|
|
|
|
it("rejects non-inventory and tampered Evidence", async () => {
|
|
const root = await createTestTempDirectory("rea-artifact-invalid-");
|
|
const evidence = await observe(root);
|
|
expect(() =>
|
|
compareArtifacts({ ...evidence, operation: "binary_overview" }, evidence),
|
|
).toThrow(/identifier/u);
|
|
const wrongOperation = createEvidence(undefined, PROVIDER, {
|
|
operation: "binary_overview",
|
|
parameters: {},
|
|
result: evidence.normalized_result,
|
|
});
|
|
expect(() => compareArtifacts(wrongOperation, evidence)).toThrow(
|
|
/inspect_artifact/u,
|
|
);
|
|
const mismatchedSubject = createEvidence(
|
|
{ path: root, sha256: "f".repeat(64), format: "directory" },
|
|
PROVIDER,
|
|
{
|
|
operation: "inventory_artifact",
|
|
parameters: {},
|
|
result: evidence.normalized_result,
|
|
},
|
|
);
|
|
expect(() => compareArtifacts(mismatchedSubject, evidence)).toThrow(
|
|
/root digest/u,
|
|
);
|
|
});
|
|
});
|