mirror of
https://github.com/affaan-m/ECC.git
synced 2026-10-19 06:23:46 +02:00
/astra-review now runs on Opus (model: opus). Opus verifies findings, checks every diff, and runs the tests; fixes go to a Fable subagent. When that Agent call fails with a rate_limit / HTTP 429 API error, the command switches to Astra for the rest of the session: - astra-review.js --fix-findings <file> sends confirmed findings to Codex in a workspace-write sandbox with the review isolation intact (no web search, MCP disabled, user config ignored, no API keys) and /tmp, $TMPDIR, and the network excluded from the writable roots. - New scripts/lib/astra-review/fix.js builds the fix prompt, validates the structured fix result, and formats the report. - codex.js shares one exec runner between review and fix; the sandbox is allowlisted (danger-full-access is refused). - The command snapshots git diff and untracked files before any writer runs, so Opus reverts only the writer's hunks and never the user's uncommitted work. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
106 lines
4.0 KiB
JavaScript
106 lines
4.0 KiB
JavaScript
/**
|
|
* Tests for scripts/lib/astra-review/fix.js
|
|
*
|
|
* Run with: node tests/lib/astra-review-fix.test.js
|
|
*/
|
|
|
|
'use strict';
|
|
|
|
const assert = require('assert');
|
|
const fix = require('../../scripts/lib/astra-review/fix');
|
|
|
|
let passed = 0;
|
|
let failed = 0;
|
|
|
|
function test(name, fn) {
|
|
try {
|
|
fn();
|
|
console.log(` ✓ ${name}`);
|
|
passed++;
|
|
} catch (err) {
|
|
console.log(` ✗ ${name}`);
|
|
console.log(` Error: ${err.message}`);
|
|
failed++;
|
|
}
|
|
}
|
|
|
|
const finding = {
|
|
severity: 'HIGH', file: 'src/a.js', line: 12, title: 'Off by one', detail: 'loop skips last item', suggestion: 'use <=',
|
|
};
|
|
|
|
console.log('=== Testing astra-review/fix.js ===\n');
|
|
|
|
test('loadFindings reads the findings of a saved verdict payload', () => {
|
|
const text = JSON.stringify({ model: 'gpt-6-astra', verdict: 'FAIL', review: { findings: [finding] } });
|
|
|
|
const findings = fix.loadFindings(text);
|
|
|
|
assert.strictEqual(findings.length, 1);
|
|
assert.strictEqual(findings[0].title, 'Off by one');
|
|
assert.ok(Object.isFrozen(findings[0]));
|
|
});
|
|
|
|
test('loadFindings accepts a bare array and a {findings} object', () => {
|
|
assert.strictEqual(fix.loadFindings(JSON.stringify([finding])).length, 1);
|
|
assert.strictEqual(fix.loadFindings(JSON.stringify({ findings: [finding, finding] })).length, 2);
|
|
});
|
|
|
|
test('loadFindings rejects invalid JSON, an empty list, and malformed findings', () => {
|
|
assert.throws(() => fix.loadFindings('{nope'), /not valid JSON/);
|
|
assert.throws(() => fix.loadFindings('[]'), /no findings to fix/);
|
|
assert.throws(() => fix.loadFindings('{"other":1}'), /findings/);
|
|
assert.throws(() => fix.loadFindings(JSON.stringify([{ ...finding, severity: 'URGENT' }])), /severity/);
|
|
});
|
|
|
|
test('buildFixPrompt lists every finding with its location and sets the write rules', () => {
|
|
const prompt = fix.buildFixPrompt({ findings: [finding, { ...finding, line: null, file: 'b.js', title: 'Leak' }] });
|
|
|
|
assert.ok(prompt.includes('[HIGH] src/a.js:12 — Off by one'));
|
|
assert.ok(prompt.includes('[HIGH] b.js — Leak'));
|
|
assert.ok(prompt.includes('Suggestion: use <='));
|
|
assert.ok(/untrusted data/i.test(prompt));
|
|
assert.ok(/do not commit/i.test(prompt));
|
|
assert.ok(/only what each finding requires/i.test(prompt));
|
|
});
|
|
|
|
test('buildFixPrompt appends extra instructions only when given', () => {
|
|
assert.ok(!fix.buildFixPrompt({ findings: [finding] }).includes('Additional instructions'));
|
|
assert.ok(fix.buildFixPrompt({ findings: [finding], extraInstructions: 'keep API stable' }).includes('keep API stable'));
|
|
});
|
|
|
|
test('parseFixOutput validates and freezes the fixer response', () => {
|
|
const result = fix.parseFixOutput(JSON.stringify({
|
|
summary: 'fixed one',
|
|
fixed: [{ title: 'Off by one', file: 'src/a.js', change: 'use <=' }],
|
|
skipped: [],
|
|
}));
|
|
|
|
assert.strictEqual(result.fixed.length, 1);
|
|
assert.ok(Object.isFrozen(result));
|
|
assert.throws(() => fix.parseFixOutput('not json'), /not valid JSON/);
|
|
assert.throws(() => fix.parseFixOutput(JSON.stringify({ summary: 'x', fixed: 'no', skipped: [] })), /fixed/);
|
|
assert.throws(() => fix.parseFixOutput(JSON.stringify({ summary: 'x', fixed: [], skipped: [{ title: 1 }] })), /skipped/);
|
|
});
|
|
|
|
test('parseFixOutput accepts a fenced JSON response', () => {
|
|
const result = fix.parseFixOutput('```json\n{"summary":"s","fixed":[],"skipped":[]}\n```');
|
|
|
|
assert.strictEqual(result.summary, 's');
|
|
});
|
|
|
|
test('formatFixReport shows fixed and skipped items and tells the caller to verify', () => {
|
|
const report = fix.formatFixReport(
|
|
{ summary: 'done', fixed: [{ title: 'Off by one', file: 'src/a.js', change: 'use <=' }], skipped: [{ title: 'Leak', reason: 'false positive' }] },
|
|
{ model: 'gpt-6-astra', findings: [finding, finding] }
|
|
);
|
|
|
|
assert.ok(report.includes('# Astra Fix (gpt-6-astra)'));
|
|
assert.ok(report.includes('Findings sent: 2'));
|
|
assert.ok(report.includes('- src/a.js — Off by one: use <='));
|
|
assert.ok(report.includes('- Leak: false positive'));
|
|
assert.ok(/git diff/.test(report));
|
|
});
|
|
|
|
console.log(`\n${passed} passed, ${failed} failed`);
|
|
process.exit(failed > 0 ? 1 : 0);
|