fix(json/comments): validateJSONArrayFile 先驗證再寫入避免毀損原檔、findingRow 對空值防呆

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Jeffery
2026-06-26 15:26:08 +08:00
co-authored by Claude Opus 4.8
parent fbb74092cb
commit 402630fa69
3 changed files with 90 additions and 6 deletions
+79
View File
@@ -140,6 +140,85 @@ describe('json helpers', () => {
});
});
describe('validateJSONArrayFile repair failure paths', () => {
let workspace;
beforeEach(() => {
workspace = fs.mkdtempSync(path.join(os.tmpdir(), 'json-test-repair-'));
});
afterEach(() => {
fs.rmSync(workspace, { recursive: true, force: true });
});
it('overwrites the invalid file with the valid array returned by the repairer', async () => {
const fullPath = path.join(workspace, '.gitea/ai-review/findings.json');
fs.mkdirSync(path.dirname(fullPath), { recursive: true });
fs.writeFileSync(fullPath, '{ this is not json', 'utf8');
let receivedOriginal;
const result = await validateJSONArrayFile(
fullPath,
'.gitea/ai-review/findings.json',
async (passedPath, passedLabel, original) => {
// repairer is called with (fullPath, label, original) per json.js line 109
assert.equal(passedPath, fullPath);
assert.equal(passedLabel, '.gitea/ai-review/findings.json');
receivedOriginal = original;
return '[{"id":1},{"id":2}]';
}
);
assert.equal(receivedOriginal, '{ this is not json');
assert.deepEqual(result, { exists: true, valid: true, repaired: true });
// file is overwritten with the repaired content, trailing newline appended (line 110)
const written = fs.readFileSync(fullPath, 'utf8');
assert.equal(written, '[{"id":1},{"id":2}]\n');
assert.deepEqual(JSON.parse(written), [{ id: 1 }, { id: 2 }]);
});
it('throws when the repaired text is still invalid JSON and does NOT overwrite the original file', async () => {
const fullPath = path.join(workspace, '.gitea/ai-review/findings.json');
fs.mkdirSync(path.dirname(fullPath), { recursive: true });
const originalBytes = '{ still broken';
fs.writeFileSync(fullPath, originalBytes, 'utf8');
const stillInvalid = 'not a json array either';
await assert.rejects(
() => validateJSONArrayFile(
fullPath,
'.gitea/ai-review/findings.json',
async () => stillInvalid
),
SyntaxError
);
// json.js validates the repaired text in-memory BEFORE writing, so an invalid
// repair throws without corrupting the file — the original bytes are preserved.
const after = fs.readFileSync(fullPath, 'utf8');
assert.equal(after, originalBytes);
});
it('propagates an error thrown by the repairer', async () => {
const fullPath = path.join(workspace, '.gitea/ai-review/findings.json');
fs.mkdirSync(path.dirname(fullPath), { recursive: true });
fs.writeFileSync(fullPath, '{ broken', 'utf8');
const boom = new Error('repairer exploded');
await assert.rejects(
() => validateJSONArrayFile(
fullPath,
'.gitea/ai-review/findings.json',
async () => { throw boom; }
),
/repairer exploded/
);
// repairer threw before any write happened, so the original bytes remain untouched (line 109)
assert.equal(fs.readFileSync(fullPath, 'utf8'), '{ broken');
});
});
describe('repairJSONArrayWithAI', () => {
it('returns a clean JSON array string parseable by the caller', async () => {
const repaired = await repairJSONArrayWithAI(