fix(reference-source): distinguish Git API probes from failed reads
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user