fix(reference-source): distinguish Git API probes from failed reads

This commit is contained in:
N0zoM1z0
2026-10-10 16:59:30 +00:00
parent 3b66532e26
commit 87b7bf8db1
3 changed files with 81 additions and 16 deletions
+18 -2
View File
@@ -53,9 +53,25 @@ const resolveSourceHead = async (
...fs.promises,
// isomorphic-git treats every read failure as absence. Preserve real
// failures so a readable packed ref cannot hide an unreadable loose ref.
readFile: async (path: string) => {
readFile: async (path?: string, options?: unknown) => {
// isomorphic-git probes promise support with readFile() before reading
// metadata. Its rejected probe must not count as a failed file read.
if (path === undefined)
throw new TypeError("Git metadata path is required");
try {
return await readRegularFile(path, { signal });
const bytes = await readRegularFile(path, { signal });
const encoding =
typeof options === "string"
? options
: typeof options === "object" &&
options !== null &&
"encoding" in options
? options.encoding
: undefined;
if (encoding === undefined || encoding === null) return bytes;
if (typeof encoding !== "string" || !Buffer.isEncoding(encoding))
throw new TypeError("Unsupported Git metadata text encoding");
return bytes.toString(encoding);
} catch (cause: unknown) {
if (errorCode(cause) !== "ENOENT") failedRead ??= { cause };
throw cause;
+24 -13
View File
@@ -8,23 +8,18 @@ import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
import { cliTest } from "../../support/cli/cliFixture.js";
cliTest(
"rejects malformed oversized CLI input and preserves Evidence decoder resource failures",
"rejects malformed oversized input through CLI and Evidence readers",
async ({ cli, processes }) => {
const root = await createTestTempDirectory("rea-json-string-limit-");
const input = join(root, "oversized.json");
const file = await open(input, "wx");
try {
// Sparse zero bytes are valid UTF-8. The Evidence reader reaches the
// decoded-length limit; the streamed CLI rejects their invalid JSON syntax.
// Sparse zero bytes are valid UTF-8. Both streaming readers reject
// their invalid JSON syntax without assembling a whole-document string.
await file.truncate(constants.MAX_STRING_LENGTH + 1);
} finally {
await file.close();
}
const limits = {
input_path: input,
input_bytes: constants.MAX_STRING_LENGTH + 1,
max_string_code_units: constants.MAX_STRING_LENGTH,
};
// Run readers sequentially in owned child processes, releasing each large
// read buffer when its process exits instead of retaining it in Vitest.
const result = await cli.run({
@@ -48,11 +43,11 @@ cliTest(
input,
]);
expect(JSON.parse(evidence.stdout)).toMatchObject({
code: "resource_constraint",
code: "evidence_integrity_mismatch",
details: {
operation: "read_evidence_file",
resource: "memory",
reported_limits: limits,
operation: "read",
reason: "invalid-json",
path: input,
},
});
expect(evidence.stderr).toBe("");
@@ -123,7 +118,7 @@ cliTest(
cliTest(
"returns a typed string assembly constraint under a bounded heap",
async ({ cli }) => {
async ({ cli, processes }) => {
const root = await createTestTempDirectory("rea-json-string-headroom-");
const input = join(root, "long-string.json");
const file = await open(input, "wx");
@@ -155,5 +150,21 @@ cliTest(
action: expect.stringContaining("re-analyze a smaller selection"),
},
});
const evidence = await processes.run(process.execPath, [
"--max-old-space-size=768",
"--input-type=module",
"-e",
`import { readJsonFile } from "./dist/application/JsonFiles.js";
import { projectAnalysisError } from "./dist/domain/analysisErrorProjection.js";
const result = await readJsonFile(process.argv[1]);
console.log(JSON.stringify(result.ok ? result : projectAnalysisError(result.error)));`,
input,
]);
expect(JSON.parse(evidence.stdout)).toMatchObject({
code: "resource_constraint",
details: { operation: "read_evidence_file", resource: "memory" },
});
expect(evidence.stderr).toBe("");
expect(evidence.exitCode).toBe(0);
},
);
@@ -1,4 +1,4 @@
import { mkdir, writeFile } from "node:fs/promises";
import { chmod, mkdir, rm, writeFile } from "node:fs/promises";
import { join } from "node:path";
import { expect, it } from "vitest";
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
@@ -92,3 +92,41 @@ it("reads a linked worktree's own branch and detached HEAD through shared Git re
dirty: null,
});
});
it.skipIf(process.platform === "win32")(
"keeps unreadable loose refs unknown instead of falling back to packed refs",
async () => {
const root = await createTestTempDirectory("rea-reference-loose-ref-");
const gitdir = join(root, ".git");
const looseRef = join(gitdir, "refs", "heads", "main");
await mkdir(join(gitdir, "refs", "heads"), { recursive: true });
const head = "1234567890abcdef1234567890abcdef12345678";
await writeFile(join(gitdir, "HEAD"), "ref: refs/heads/main\n");
await writeFile(join(gitdir, "packed-refs"), `${head} refs/heads/main\n`);
expect(await readReferenceSourceVcs(root)).toEqual({
kind: "git",
head,
dirty: null,
});
// A non-file loose ref is an actual read failure, not an absent ref.
await mkdir(looseRef);
expect(await readReferenceSourceVcs(root)).toEqual({
kind: "unknown",
head: null,
dirty: null,
});
await rm(looseRef, { recursive: true });
if (process.getuid?.() === 0) return;
await writeFile(looseRef, `${head}\n`);
await chmod(looseRef, 0);
try {
expect(await readReferenceSourceVcs(root)).toEqual({
kind: "unknown",
head: null,
dirty: null,
});
} finally {
await chmod(looseRef, 0o600);
}
},
);