fix(reference): preserve missing symlink target facts

Treat ENOTDIR while resolving a symlink target as missing target evidence, preserving its spelling and partial inventory. Keep graph-level excluded-ancestor rejection separate from import success. The compiled boundary regression passes.
This commit is contained in:
morluto
2026-10-14 08:43:59 +08:00
parent 3ea5fdd7cf
commit 0f4c484be5
2 changed files with 16 additions and 5 deletions
@@ -357,7 +357,7 @@ const readSymlinkTarget = async (
} catch (cause: unknown) {
const code =
cause instanceof Error ? Reflect.get(cause, "code") : undefined;
if (code !== "ENOENT") throw cause;
if (code !== "ENOENT" && code !== "ENOTDIR") throw cause;
return {
status: "read",
kind: "symlink",
@@ -167,10 +167,21 @@ describe("reference source symlink import", () => {
policy: { secretPatterns: [] },
excludePaths: ["secret.txt"],
});
expect(result.ok).toBe(false);
if (result.ok) return;
expect(result.error.code).toBe("parse");
expect(result.error.message).toMatch(/recorded exclusion/u);
if (!result.ok) throw new Error(result.error.message);
expect(result.value.inventory_state).toBe("partial");
expect(
result.value.entries.find(({ path }) => path === "alias"),
).toMatchObject({
kind: "symlink",
target: "secret.txt/child",
target_state: "missing",
});
expect(result.value.exclusions).toContainEqual({
path: "secret.txt",
entry_kind: "file",
reason: "caller-excluded",
pattern: "secret.txt",
});
});
it("preserves an unreadable target as unknown instead of a synthetic path", () => {