fix(reference)!: represent unreadable symlink targets explicitly
BREAKING CHANGE: Historical source symlinks with unreadable or unknown target_state require target: null. Placeholder target strings are rejected.
This commit is contained in:
parent
0696bde266
commit
b7b4333703
|
|
@ -59,7 +59,7 @@ const failedEntry = (
|
|||
return {
|
||||
path: entry.path,
|
||||
kind: "symlink",
|
||||
target: "<unreadable>",
|
||||
target: null,
|
||||
target_state: "unreadable",
|
||||
classifications,
|
||||
limitations: [limitation],
|
||||
|
|
|
|||
|
|
@ -58,7 +58,11 @@ const unavailableFileEntry = (
|
|||
...overrides,
|
||||
});
|
||||
|
||||
const symlinkEntry = (overrides: Partial<SymlinkEntry> = {}): SymlinkEntry => ({
|
||||
type KnownTargetSymlinkEntry = Extract<SymlinkEntry, { target: string }>;
|
||||
|
||||
const symlinkEntry = (
|
||||
overrides: Partial<KnownTargetSymlinkEntry> = {},
|
||||
): KnownTargetSymlinkEntry => ({
|
||||
path: "src/package.json",
|
||||
kind: "symlink",
|
||||
target: "main.ts",
|
||||
|
|
@ -237,6 +241,50 @@ describe("historical source graph", () => {
|
|||
});
|
||||
|
||||
describe("partial historical source graphs", () => {
|
||||
it("keeps an unreadable symlink target unknown and out of root identity", () => {
|
||||
const base = graphInput();
|
||||
base.inventory_state = "partial";
|
||||
base.entries[2] = {
|
||||
path: "src/package.json",
|
||||
kind: "symlink",
|
||||
target: null,
|
||||
target_state: "unreadable",
|
||||
classifications: ["manifest"],
|
||||
limitations: ["Target could not be read."],
|
||||
};
|
||||
const graph = createHistoricalSourceGraph(base);
|
||||
expect(graph.entries[2]).toMatchObject({
|
||||
kind: "symlink",
|
||||
target: null,
|
||||
target_state: "unreadable",
|
||||
});
|
||||
|
||||
const changedMessage = structuredClone(base);
|
||||
changedMessage.entries[2] = {
|
||||
path: "src/package.json",
|
||||
kind: "symlink",
|
||||
target: null,
|
||||
target_state: "unreadable",
|
||||
classifications: ["manifest"],
|
||||
limitations: ["A different diagnostic."],
|
||||
};
|
||||
const revised = createHistoricalSourceGraph(changedMessage);
|
||||
expect(revised.root_sha256).toBe(graph.root_sha256);
|
||||
expect(computeHistoricalSourceGraphSha256(revised)).not.toBe(
|
||||
computeHistoricalSourceGraphSha256(graph),
|
||||
);
|
||||
expect(() =>
|
||||
createHistoricalSourceGraph({
|
||||
...base,
|
||||
entries: base.entries.map((entry) =>
|
||||
entry.kind === "symlink"
|
||||
? { ...entry, target: "<unreadable>" }
|
||||
: entry,
|
||||
),
|
||||
}),
|
||||
).toThrow();
|
||||
});
|
||||
|
||||
it("preserves partial observations but rejects every partial complete graph", () => {
|
||||
const partial = graphInput();
|
||||
partial.inventory_state = "partial";
|
||||
|
|
|
|||
|
|
@ -92,18 +92,20 @@ const isPortableAbsoluteSymlinkTarget = (target: string): boolean =>
|
|||
/^[A-Za-z]:[\\/]/u.test(target) ||
|
||||
/^\\\\[^\\/]+[\\/][^\\/]+/u.test(target);
|
||||
|
||||
const sourceSymlinkSchema = z.strictObject({
|
||||
...entryBaseShape,
|
||||
kind: z.literal("symlink"),
|
||||
target: symlinkTargetSchema,
|
||||
target_state: z.enum([
|
||||
"internal",
|
||||
"external",
|
||||
"missing",
|
||||
"unreadable",
|
||||
"unknown",
|
||||
]),
|
||||
});
|
||||
const sourceSymlinkSchema = z.union([
|
||||
z.strictObject({
|
||||
...entryBaseShape,
|
||||
kind: z.literal("symlink"),
|
||||
target: symlinkTargetSchema,
|
||||
target_state: z.enum(["internal", "external", "missing"]),
|
||||
}),
|
||||
z.strictObject({
|
||||
...entryBaseShape,
|
||||
kind: z.literal("symlink"),
|
||||
target: z.null(),
|
||||
target_state: z.enum(["unreadable", "unknown"]),
|
||||
}),
|
||||
]);
|
||||
|
||||
const sourceEntrySchema = z.union([
|
||||
sourceFileSchema,
|
||||
|
|
@ -314,6 +316,7 @@ const checkSymlinks = (
|
|||
): void => {
|
||||
for (const [index, entry] of graph.entries.entries()) {
|
||||
if (entry.kind !== "symlink") continue;
|
||||
if (entry.target === null) continue;
|
||||
const absolute = isPortableAbsoluteSymlinkTarget(entry.target);
|
||||
if (
|
||||
(entry.target_state === "external" && !absolute) ||
|
||||
|
|
|
|||
|
|
@ -11,7 +11,11 @@ import {
|
|||
importReferenceSource,
|
||||
normalizeHistoricalSourceParseFailures,
|
||||
} from "../../../src/application/ReferenceSourceImport.js";
|
||||
import { projectReferenceSourceEntryFailure } from "../../../src/application/ReferenceSourceImportEntries.js";
|
||||
import {
|
||||
parseReferenceSourceEntries,
|
||||
projectReferenceSourceEntryFailure,
|
||||
} from "../../../src/application/ReferenceSourceImportEntries.js";
|
||||
import type { ReferenceSourceRead } from "../../../src/reference/ReferenceSourceReader.js";
|
||||
import {
|
||||
projectReferenceSourceImportError,
|
||||
type ReferenceSourceImportError,
|
||||
|
|
@ -114,6 +118,33 @@ describe("reference source import error projection", () => {
|
|||
});
|
||||
|
||||
describe("reference source symlink import", () => {
|
||||
it("preserves an unreadable target as unknown instead of a synthetic path", () => {
|
||||
const read: ReferenceSourceRead = {
|
||||
root: "/reference",
|
||||
entries: [
|
||||
{
|
||||
status: "failed",
|
||||
kind: "symlink",
|
||||
path: "src/link.ts",
|
||||
code: "io",
|
||||
message: "Symbolic link target could not be read",
|
||||
},
|
||||
],
|
||||
bytesRead: 0,
|
||||
limitations: [],
|
||||
};
|
||||
|
||||
const parsed = parseReferenceSourceEntries(read, new Set());
|
||||
expect(parsed.entries).toContainEqual(
|
||||
expect.objectContaining({
|
||||
kind: "symlink",
|
||||
path: "src/link.ts",
|
||||
target: null,
|
||||
target_state: "unreadable",
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("retains external symlink targets as local graph diagnostics", async () => {
|
||||
const root = await createTestTempDirectory("rea-reference-links-");
|
||||
const outside = await createTestTempDirectory("rea-reference-outside-");
|
||||
|
|
@ -289,8 +320,6 @@ describe("reference source import behavior", () => {
|
|||
importTree(leftRoot),
|
||||
importTree(rightRoot),
|
||||
]);
|
||||
expect(left.ok).toBe(true);
|
||||
expect(right.ok).toBe(true);
|
||||
if (!left.ok || !right.ok) throw new Error("expected imports to pass");
|
||||
expect(createHistoricalSourceManifest(left.value)).toEqual(
|
||||
createHistoricalSourceManifest(right.value),
|
||||
|
|
|
|||
Loading…
Reference in New Issue