test(review-resolve): 更新 wrapper 與模型驗證測試
This commit is contained in:
@@ -21,10 +21,14 @@ describe('saveFindings', () => {
|
||||
|
||||
saveFindings(workspace, findings, mirrorDir);
|
||||
|
||||
const workspaceText = fs.readFileSync(path.join(workspace, FINDINGS_PATH), 'utf8');
|
||||
const mirrorText = fs.readFileSync(path.join(mirrorDir, FINDINGS_PATH), 'utf8');
|
||||
assert.equal(workspaceText, JSON.stringify(findings, null, 2) + '\n');
|
||||
assert.equal(mirrorText, JSON.stringify(findings, null, 2) + '\n');
|
||||
const workspaceData = JSON.parse(fs.readFileSync(path.join(workspace, FINDINGS_PATH), 'utf8'));
|
||||
const mirrorData = JSON.parse(fs.readFileSync(path.join(mirrorDir, FINDINGS_PATH), 'utf8'));
|
||||
assert.equal(typeof workspaceData.generatedAt, 'string');
|
||||
assert.equal(typeof workspaceData.commitSha, 'string');
|
||||
assert.ok(Array.isArray(workspaceData.findings));
|
||||
assert.deepEqual(workspaceData.findings, findings);
|
||||
assert.deepEqual(workspaceData.excluded, []);
|
||||
assert.deepEqual(mirrorData, workspaceData);
|
||||
});
|
||||
|
||||
it('writes only to workspace when mirrorDir is omitted', () => {
|
||||
@@ -33,8 +37,9 @@ describe('saveFindings', () => {
|
||||
|
||||
saveFindings(workspace, findings);
|
||||
|
||||
const workspaceText = fs.readFileSync(path.join(workspace, FINDINGS_PATH), 'utf8');
|
||||
assert.equal(workspaceText, JSON.stringify(findings, null, 2) + '\n');
|
||||
const workspaceData = JSON.parse(fs.readFileSync(path.join(workspace, FINDINGS_PATH), 'utf8'));
|
||||
assert.deepEqual(workspaceData.findings, findings);
|
||||
assert.deepEqual(workspaceData.excluded, []);
|
||||
});
|
||||
|
||||
it('does not duplicate writes when mirrorDir matches workspace', () => {
|
||||
@@ -58,13 +63,14 @@ describe('saveFindings', () => {
|
||||
assert.equal(writeCalls[0], path.join(workspace, FINDINGS_PATH));
|
||||
});
|
||||
|
||||
it('writes an empty JSON array when findings is empty', () => {
|
||||
it('writes an empty findings wrapper when findings is empty', () => {
|
||||
const workspace = makeTempDir('findings-empty-');
|
||||
|
||||
saveFindings(workspace, []);
|
||||
|
||||
const workspaceText = fs.readFileSync(path.join(workspace, FINDINGS_PATH), 'utf8');
|
||||
assert.equal(workspaceText, '[]\n');
|
||||
const workspaceData = JSON.parse(fs.readFileSync(path.join(workspace, FINDINGS_PATH), 'utf8'));
|
||||
assert.deepEqual(workspaceData.findings, []);
|
||||
assert.deepEqual(workspaceData.excluded, []);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
|
||||
@@ -72,6 +72,16 @@ describe('getLLMConfig', () => {
|
||||
assert.match(cfg.modelError, /無效的 model 參數/);
|
||||
});
|
||||
|
||||
it('accepts slash-delimited model names', () => {
|
||||
process.env.CLI_PROXY_API = 'https://proxy.example';
|
||||
process.env.MODEL = 'provider/gpt-5.5';
|
||||
|
||||
const cfg = getLLMConfig();
|
||||
|
||||
assert.equal(cfg.model, 'provider/gpt-5.5');
|
||||
assert.equal(cfg.modelError, null);
|
||||
});
|
||||
|
||||
it('returns null provider when CLI_PROXY_API is missing', () => {
|
||||
process.env.MODEL = 'gpt-5.5';
|
||||
const cfg = getLLMConfig();
|
||||
|
||||
@@ -348,12 +348,19 @@ describe('findings exclusions', () => {
|
||||
assert.ok(logs.some(line => line.includes(`path=${path.relative(workspace, fullPath)}`)));
|
||||
});
|
||||
|
||||
it('logs findings file metadata when loading old findings', () => {
|
||||
it('loads wrapper findings and logs findings file metadata', () => {
|
||||
const fullPath = path.join(workspace, FINDINGS_PATH);
|
||||
fs.mkdirSync(path.dirname(fullPath), { recursive: true });
|
||||
fs.writeFileSync(fullPath, JSON.stringify([
|
||||
fs.writeFileSync(fullPath, JSON.stringify({
|
||||
generatedAt: '2026/08/07 16:47:53',
|
||||
commitSha: 'deadbeef',
|
||||
prNumber: 7,
|
||||
tool: { name: 'ai-code-review', version: '1.0.0', model: 'auto' },
|
||||
findings: [
|
||||
{ level: 'info', role: 'Maya', location: 'README.md:12', suggestion: 'keep' },
|
||||
], null, 2));
|
||||
],
|
||||
excluded: [],
|
||||
}, null, 2));
|
||||
|
||||
const findings = loadOldFindings(workspace);
|
||||
|
||||
|
||||
+32
-21
@@ -37,6 +37,19 @@ describe('json helpers', () => {
|
||||
assert.ok(capturedUserContent.includes('"{broken"'));
|
||||
});
|
||||
|
||||
it('creates an empty findings wrapper when asked to ensure existence', () => {
|
||||
const fullPath = path.join(workspace, '.gitea/ai-review/findings.json');
|
||||
|
||||
const created = ensureJSONArrayFileExists(fullPath, '.gitea/ai-review/findings.json');
|
||||
|
||||
assert.equal(created, true);
|
||||
const written = JSON.parse(fs.readFileSync(fullPath, 'utf8'));
|
||||
assert.equal(typeof written.generatedAt, 'string');
|
||||
assert.ok(Array.isArray(written.findings));
|
||||
assert.deepEqual(written.findings, []);
|
||||
assert.deepEqual(written.excluded, []);
|
||||
});
|
||||
|
||||
it('reports missing file without creating it', async () => {
|
||||
const fullPath = path.join(workspace, '.gitea/ai-review/findings.json');
|
||||
|
||||
@@ -46,15 +59,6 @@ describe('json helpers', () => {
|
||||
assert.equal(fs.existsSync(fullPath), false);
|
||||
});
|
||||
|
||||
it('creates an empty array file when asked to ensure existence', () => {
|
||||
const fullPath = path.join(workspace, '.gitea/ai-review/findings.json');
|
||||
|
||||
const created = ensureJSONArrayFileExists(fullPath, '.gitea/ai-review/findings.json');
|
||||
|
||||
assert.equal(created, true);
|
||||
assert.equal(fs.readFileSync(fullPath, 'utf8'), '[]\n');
|
||||
});
|
||||
|
||||
it('returns false when ensuring an existing file', () => {
|
||||
const fullPath = path.join(workspace, '.gitea/ai-review/exclusions.json');
|
||||
fs.mkdirSync(path.dirname(fullPath), { recursive: true });
|
||||
@@ -77,29 +81,32 @@ describe('json helpers', () => {
|
||||
assert.equal(fs.readFileSync(fullPath, 'utf8'), '[]\n');
|
||||
});
|
||||
|
||||
it('rejects repaired JSON that is not an array', async () => {
|
||||
it('rejects repaired JSON that is not a findings wrapper', async () => {
|
||||
const fullPath = path.join(workspace, '.gitea/ai-review/findings.json');
|
||||
fs.mkdirSync(path.dirname(fullPath), { recursive: true });
|
||||
fs.writeFileSync(fullPath, '{broken', 'utf8');
|
||||
|
||||
await assert.rejects(
|
||||
() => validateJSONArrayFile(fullPath, '.gitea/ai-review/findings.json', async () => '{"ok":true}'),
|
||||
/不是 JSON 陣列/,
|
||||
/不是 findings wrapper/,
|
||||
);
|
||||
assert.equal(fs.readFileSync(fullPath, 'utf8'), '{broken');
|
||||
});
|
||||
|
||||
it('reads a valid JSON file whose size equals the maximum limit', async () => {
|
||||
it('normalizes a valid legacy findings array whose size equals the maximum limit', async () => {
|
||||
const fullPath = path.join(workspace, '.gitea/ai-review/findings.json');
|
||||
fs.mkdirSync(path.dirname(fullPath), { recursive: true });
|
||||
fs.writeFileSync(fullPath, `[]${' '.repeat(MAX_JSON_BYTES - 2)}`, 'utf8');
|
||||
|
||||
const result = await validateJSONArrayFile(fullPath, '.gitea/ai-review/findings.json');
|
||||
|
||||
assert.deepEqual(result, { exists: true, valid: true, repaired: false });
|
||||
assert.deepEqual(result, { exists: true, valid: true, repaired: true });
|
||||
const written = JSON.parse(fs.readFileSync(fullPath, 'utf8'));
|
||||
assert.deepEqual(written.findings, []);
|
||||
assert.deepEqual(written.excluded, []);
|
||||
});
|
||||
|
||||
it('repairs invalid JSON using AI output and rewrites the file', async () => {
|
||||
it('repairs invalid findings JSON using AI output and rewrites the file as a wrapper', async () => {
|
||||
const fullPath = path.join(workspace, '.gitea/ai-review/findings.json');
|
||||
fs.mkdirSync(path.dirname(fullPath), { recursive: true });
|
||||
fs.writeFileSync(fullPath, '{broken', 'utf8');
|
||||
@@ -110,7 +117,9 @@ describe('json helpers', () => {
|
||||
});
|
||||
|
||||
assert.deepEqual(result, { exists: true, valid: true, repaired: true });
|
||||
assert.equal(fs.readFileSync(fullPath, 'utf8'), '[{"fixed":true}]\n');
|
||||
const written = JSON.parse(fs.readFileSync(fullPath, 'utf8'));
|
||||
assert.deepEqual(written.findings, [{ fixed: true }]);
|
||||
assert.deepEqual(written.excluded, []);
|
||||
});
|
||||
|
||||
it('preserves a trailing newline returned by AI repair', async () => {
|
||||
@@ -124,7 +133,9 @@ describe('json helpers', () => {
|
||||
});
|
||||
|
||||
assert.deepEqual(result, { exists: true, valid: true, repaired: true });
|
||||
assert.equal(fs.readFileSync(fullPath, 'utf8'), '[{"fixed":true}]\n');
|
||||
const written = JSON.parse(fs.readFileSync(fullPath, 'utf8'));
|
||||
assert.deepEqual(written.findings, [{ fixed: true }]);
|
||||
assert.deepEqual(written.excluded, []);
|
||||
});
|
||||
|
||||
it('throws when AI repair fails', async () => {
|
||||
@@ -163,7 +174,7 @@ describe('validateJSONArrayFile repair failure paths', () => {
|
||||
fs.rmSync(workspace, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
it('overwrites the invalid file with the valid array returned by the repairer', async () => {
|
||||
it('overwrites the invalid findings file with a wrapper built from the repaired array', 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');
|
||||
@@ -183,10 +194,10 @@ describe('validateJSONArrayFile repair failure paths', () => {
|
||||
|
||||
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 }]);
|
||||
const written = JSON.parse(fs.readFileSync(fullPath, 'utf8'));
|
||||
assert.equal(typeof written.generatedAt, 'string');
|
||||
assert.deepEqual(written.findings, [{ id: 1 }, { id: 2 }]);
|
||||
assert.deepEqual(written.excluded, []);
|
||||
});
|
||||
|
||||
it('throws when the repaired text is still invalid JSON and does NOT overwrite the original file', async () => {
|
||||
|
||||
Reference in New Issue
Block a user