fix(artifacts): retain relative paths in directory identities (#580)

This commit is contained in:
Kaoru0822 2026-10-05 08:07:24 -07:00 committed by GitHub
parent 8eac290e8d
commit 348323dd23
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 180 additions and 1 deletions

View File

@ -81,7 +81,8 @@ export const materializeDirectoryNodes = (
for (const directory of directories) {
const children = (childrenByParent.get(directory.occurrence_id) ?? [])
.map(({ logical_path, artifact_id, entry_kind }) => ({
name: logical_path.split("/").at(-1),
// Intermediate directory entries may be absent from an archive.
name: logical_path.slice(directory.logical_path.length + 1),
artifact_id,
entry_kind,
}))

View File

@ -0,0 +1,178 @@
import { writeFile } from "node:fs/promises";
import { join } from "node:path";
import { TextReader, Uint8ArrayWriter, ZipWriter } from "@zip.js/zip.js";
import { describe, expect, it } from "vitest";
import { digestCanonical } from "../../../src/application/ArtifactGraphConstruction.js";
import { inventoryArtifact } from "../../../src/application/ArtifactInventory.js";
import { compareArtifacts } from "../../../src/domain/artifactComparison.js";
import type { ArtifactInventoryResult } from "../../../src/domain/artifactGraph.js";
import { createEvidence } from "../../../src/domain/evidence.js";
import { jsonValueSchema } from "../../../src/domain/jsonValue.js";
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
const observeZip = async (entries: readonly (readonly [string, string])[]) => {
const root = await createTestTempDirectory("rea-directory-identity-");
const path = join(root, "fixture.zip");
const writer = new ZipWriter(new Uint8ArrayWriter());
for (const [name, content] of entries)
await writer.add(name, new TextReader(content), {
directory: name.endsWith("/"),
lastModDate: new Date("2020-01-01T00:00:00Z"),
});
await writeFile(path, await writer.close());
const inventory = await inventoryArtifact(path);
return {
inventory,
evidence: createEvidence(
{ path, sha256: inventory.manifest.root_sha256, format: "zip" },
{ id: "rea-artifact-graph", name: "REA artifact graph", version: "1" },
{
operation: "inventory_artifact",
parameters: {},
result: jsonValueSchema.parse(inventory),
confidence: "observed",
authority: "shipped-artifact",
},
),
};
};
const occurrenceAt = (inventory: ArtifactInventoryResult, path: string) => {
const occurrence = inventory.occurrences.find(
({ logical_path }) => logical_path === path,
);
if (occurrence === undefined) throw new Error(`Missing ${path}`);
expect(occurrence.hash_status).toBe("verified");
expect(occurrence.artifact_id).not.toBeNull();
return occurrence;
};
// Preserve the pre-fix name-based hash for ordinary immediate children.
const legacyDirectoryId = (
inventory: ArtifactInventoryResult,
paths: readonly string[],
) =>
`art_${digestCanonical({
sha256: digestCanonical({
kind: "directory",
children: paths
.map((path) => {
const child = occurrenceAt(inventory, path);
return {
name: path.split("/").at(-1),
artifact_id: child.artifact_id,
entry_kind: child.entry_kind,
};
})
.sort((left, right) =>
String(left.name).localeCompare(String(right.name)),
),
}),
})}`;
describe("virtual directory relative-path identity", () => {
it("detects a renamed implicit intermediate directory", async () => {
const left = await observeZip([
["pkg/", ""],
["pkg/a/data.txt", "same bytes"],
]);
const right = await observeZip([
["pkg/", ""],
["pkg/b/data.txt", "same bytes"],
]);
expect(occurrenceAt(left.inventory, "pkg").artifact_id).not.toBe(
occurrenceAt(right.inventory, "pkg").artifact_id,
);
expect(occurrenceAt(left.inventory, "pkg/a/data.txt").artifact_id).toBe(
occurrenceAt(right.inventory, "pkg/b/data.txt").artifact_id,
);
expect(left.inventory.occurrences.map((item) => item.logical_path)).toEqual(
[".", "pkg", "pkg/a/data.txt"],
);
const comparison = compareArtifacts(left.evidence, right.evidence);
expect(comparison.changes).toContainEqual(
expect.objectContaining({
logical_path: "pkg",
classification: "changed",
}),
);
expect(comparison.summary).toMatchObject({ changed: 2, unchanged: 0 });
});
it("ignores order of same-basename children beneath implicit directories", async () => {
const left = await observeZip([
["pkg/", ""],
["pkg/a/data.txt", "A"],
["pkg/b/data.txt", "B"],
]);
const right = await observeZip([
["pkg/", ""],
["pkg/b/data.txt", "B"],
["pkg/a/data.txt", "A"],
]);
expect(occurrenceAt(left.inventory, "pkg").artifact_id).toBe(
occurrenceAt(right.inventory, "pkg").artifact_id,
);
const comparison = compareArtifacts(left.evidence, right.evidence);
expect(comparison.changes.map((change) => change.logical_path)).toEqual([
".",
]);
expect(comparison.summary).toMatchObject({ changed: 1, unchanged: 3 });
});
it.each(["data.txt", "implicit/data.txt"])(
"keeps directory relocation independent for %s",
async (suffix) => {
const left = await observeZip([
["pkg/", ""],
[`pkg/${suffix}`, "same bytes"],
]);
const right = await observeZip([
["moved/", ""],
[`moved/${suffix}`, "same bytes"],
]);
expect(occurrenceAt(left.inventory, "pkg").artifact_id).toBe(
occurrenceAt(right.inventory, "moved").artifact_id,
);
},
);
it("preserves exact legacy identities for immediate children and empty directories", async () => {
const { inventory } = await observeZip([
["pkg/", ""],
["pkg/z.txt", "Z"],
["pkg/a.txt", "A"],
["empty/", ""],
]);
expect(occurrenceAt(inventory, "pkg").artifact_id).toBe(
legacyDirectoryId(inventory, ["pkg/z.txt", "pkg/a.txt"]),
);
expect(occurrenceAt(inventory, "empty").artifact_id).toBe(
legacyDirectoryId(inventory, []),
);
});
it("preserves nested explicit-directory identities and rename detection", async () => {
const left = await observeZip([
["pkg/", ""],
["pkg/a/", ""],
["pkg/a/data.txt", "same bytes"],
]);
const right = await observeZip([
["pkg/", ""],
["pkg/b/", ""],
["pkg/b/data.txt", "same bytes"],
]);
expect(occurrenceAt(left.inventory, "pkg/a").artifact_id).toBe(
legacyDirectoryId(left.inventory, ["pkg/a/data.txt"]),
);
expect(occurrenceAt(left.inventory, "pkg").artifact_id).toBe(
legacyDirectoryId(left.inventory, ["pkg/a"]),
);
expect(occurrenceAt(left.inventory, "pkg").artifact_id).not.toBe(
occurrenceAt(right.inventory, "pkg").artifact_id,
);
});
});