rea/tests/boundary/filesystem/referenceSourceImport.test.ts

278 lines
10 KiB
TypeScript

import fs from "node:fs";
import { mkdir, rm, writeFile } from "node:fs/promises";
import { join } from "node:path";
import { add, commit, init } from "isomorphic-git";
import { describe, expect, it } from "vitest";
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
import {
importReferenceSource,
normalizeHistoricalSourceParseFailures,
} from "../../../src/application/ReferenceSourceImport.js";
import { projectReferenceSourceEntryFailure } from "../../../src/application/ReferenceSourceImportEntries.js";
import {
projectReferenceSourceImportError,
type ReferenceSourceImportError,
} from "../../../src/application/ReferenceSourceImportTypes.js";
import { createHistoricalSourceManifest } from "../../../src/domain/referenceSourceGraph.js";
const fixture = async (parent: string, name: string): Promise<string> => {
const root = join(parent, name);
await mkdir(join(root, "src"), { recursive: true });
await Promise.all([
writeFile(join(root, "src", "main.ts"), 'import "./dep";\n'),
writeFile(join(root, "src", "dep.ts"), "export const value = 1;\n"),
writeFile(join(root, "src", "broken.ts"), "const = ;\n"),
writeFile(join(root, ".env"), "SECRET_SENTINEL=do-not-record\n"),
writeFile(join(root, "package.json"), '{"name":"fixture"}\n'),
]);
return root;
};
const importTree = (root: string, signal?: AbortSignal) =>
importReferenceSource({
root,
caller: "reference-import-test",
policy: {
secretPatterns: [".env", ".env.*"],
},
...(signal === undefined ? {} : { signal }),
});
describe("reference source import error projection", () => {
it("deduplicates exact parse failures without collapsing distinct reasons", () => {
const malformed = {
path: "src/main.ts",
parser: "babel",
reason: "Malformed input",
};
const unexpected = {
path: "src/main.ts",
parser: "babel",
reason: "Unexpected token",
};
expect(
normalizeHistoricalSourceParseFailures([
unexpected,
malformed,
unexpected,
]),
).toEqual([malformed, unexpected]);
});
it("projects entry failures without low-level reader diagnostics", () => {
for (const [kind, code] of [
["directory", "io"],
["symlink", "io"],
["file", "io"],
["file", "cancelled"],
["unknown", "unsupported"],
] as const) {
const message = projectReferenceSourceEntryFailure({
status: "failed",
path: "safe/path",
kind,
code,
message: "SECRET internal diagnostic /private/path",
});
expect(message).not.toContain("SECRET");
expect(message).not.toContain("/private/path");
expect(message).toMatch(/Check|try again|when ready|smaller|supported/u);
}
});
it("projects every import failure without raw parser or policy text", () => {
const expectedCategories = {
cancelled: "cancelled",
"invalid-root": "invalid_input",
io: "execution_failure",
parse: "execution_failure",
} as const;
for (const [code, category] of Object.entries(expectedCategories)) {
const projected = projectReferenceSourceImportError({
tag: "reference-source-import",
code: code as ReferenceSourceImportError["code"],
message: "SECRET parser stack and /private/path",
});
expect(projected.category).toBe(category);
expect(projected.message).not.toContain("SECRET");
expect(projected.message).not.toContain("/private/path");
expect(projected.message).toMatch(/try again|when ready|Check that/u);
}
});
});
describe("reference source import behavior", () => {
// The importer declares no byte ceiling and no entry-count ceiling. Scale
// independence cannot be asserted as a relationship over a bounded fixture,
// because a cap above the fixture size would pass unnoticed, so the fixture
// has to cross any ceiling a future change would plausibly introduce: 5,000
// entries is well past a round 1,000 or 2,000 cap, and one 4 MiB member is
// well past a 1 MiB cap.
//
// The two boundaries are crossed independently rather than as a cross
// product. Cycling the sizes over every entry would make a thousand 4 MiB
// members, which is nearly 4 GiB of fixture and cannot fit a runner's disk.
// Entry count is crossed by volume, byte size by a single large member.
it("returns every written entry with complete coverage at any scale", async () => {
const root = await createTestTempDirectory("rea-reference-scale-");
const sizes = [0, 1, 4_097, 65_536, 4_194_304];
const written = Array.from(
{ length: 5_000 },
(_, index) => `entry-${String(index).padStart(5, "0")}.txt`,
);
// Only the first entry of each size takes that size; the remaining 4,995
// stay one byte each. The fixture is therefore about 4.2 MiB in total
// while still containing a member far larger than any byte ceiling.
const sizeFor = (index: number): number => sizes[index] ?? 1;
await Promise.all(
written.map((name, index) =>
writeFile(join(root, name), "a".repeat(sizeFor(index))),
),
);
const result = await importReferenceSource({
root,
caller: "reference-import-test",
policy: { secretPatterns: [] },
});
expect(result.ok).toBe(true);
if (!result.ok) return;
// The importer always reports one standing advisory about pathname races,
// so completeness is asserted per entry rather than globally.
const limited = result.value.entries.filter(
(entry) => entry.limitations.length > 0,
);
expect(limited.map((entry) => entry.path)).toEqual([]);
expect(result.value.entries).toHaveLength(written.length);
expect(new Set(result.value.entries.map((entry) => entry.path))).toEqual(
new Set(written),
);
// Members of every size must still be hashed rather than skipped or
// truncated, including the 4 MiB member, so each entry is checked against
// the size it was actually written with.
for (const [index, size] of sizes.entries()) {
expect(result.value.entries).toContainEqual(
expect.objectContaining({
path: `entry-${String(index).padStart(5, "0")}.txt`,
kind: "file",
size,
content_state: "hashed",
}),
);
}
// The bulk of the fixture must also survive intact, so a cap on either
// dimension fails even though only five entries carry a distinct size.
const bulk = result.value.entries.filter(
(entry) =>
Number.parseInt(entry.path.replace("entry-", ""), 10) >= sizes.length,
);
expect(bulk).toHaveLength(written.length - sizes.length);
expect(bulk.every((entry) => "size" in entry && entry.size === 1)).toBe(
true,
);
// `inventory_state` is deliberately not asserted to equal "complete": the
// importer always reports a standing advisory that Node cannot offer
// descriptor-relative openat traversal, which forces "partial" on every
// host. Completeness is proven above at the entry level, where it is
// actually meaningful.
});
it("imports BMP and supplementary filenames in Unicode code point order", async () => {
const root = await createTestTempDirectory("reference-unicode-");
try {
const paths = ["\uE000.ts", "\u{10000}.ts"];
await Promise.all(
paths.map((path) =>
writeFile(join(root, path), "export const value = 1;\n"),
),
);
const result = await importTree(root);
expect(result.ok).toBe(true);
if (!result.ok) return;
expect(result.value.entries.map(({ path }) => path)).toEqual(paths);
const repeated = await importTree(root);
expect(repeated).toEqual(result);
} finally {
await rm(root, { recursive: true, force: true });
}
});
it("is relocation-stable, resolves imports, and excludes secrets before capture", async () => {
const parent = await createTestTempDirectory("rea-reference-import-");
try {
const leftRoot = await fixture(parent, "left");
const rightRoot = await fixture(parent, "right");
const [left, right] = await Promise.all([
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),
);
expect(left.value.relationships).toContainEqual(
expect.objectContaining({
from_path: "src/main.ts",
to: "src/dep.ts",
resolution: "internal",
}),
);
expect(left.value.parse_failures).toHaveLength(1);
expect(left.value.exclusions).toContainEqual({
path: ".env",
reason: "configured-secret",
});
expect(JSON.stringify(left.value)).not.toContain("SECRET_SENTINEL");
expect(JSON.stringify(left.value)).not.toContain(leftRoot);
} finally {
await rm(parent, { recursive: true, force: true });
}
});
it("imports the caller-selected directory and honors cancellation", async () => {
const outside = await createTestTempDirectory("rea-reference-outside-");
try {
const root = await fixture(outside, "tree");
expect(await importTree(root)).toMatchObject({ ok: true });
const controller = new AbortController();
controller.abort();
const cancelled = await importTree(root, controller.signal);
expect(cancelled).toMatchObject({
ok: false,
error: { code: "cancelled" },
});
} finally {
await rm(outside, { recursive: true, force: true });
}
});
it("records bounded local Git state without invoking Git", async () => {
const parent = await createTestTempDirectory("rea-reference-git-");
try {
const root = await fixture(parent, "repo");
await init({ fs, dir: root, defaultBranch: "main" });
for (const filepath of ["package.json", "src/main.ts", "src/dep.ts"])
await add({ fs, dir: root, filepath });
const oid = await commit({
fs,
dir: root,
author: { name: "REA Test", email: "rea@example.invalid" },
message: "fixture",
});
const result = await importTree(root);
expect(result.ok).toBe(true);
if (!result.ok) throw result.error;
expect(result.value.vcs).toEqual({ kind: "git", head: oid, dirty: null });
} finally {
await rm(parent, { recursive: true, force: true });
}
});
});