Skip to content

Commit bd4c5d9

Browse files
test: prove deterministic failures spend zero audit-provider calls
1 parent 9aa3a00 commit bd4c5d9

1 file changed

Lines changed: 80 additions & 0 deletions

File tree

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
import test from 'node:test';
2+
import assert from 'node:assert/strict';
3+
import fs from 'node:fs';
4+
import os from 'node:os';
5+
import path from 'node:path';
6+
import { guardedAudit, sanitizeAuditInputs } from '../lib/audit-guard.mjs';
7+
import { projectPaths, sha256, writeJson } from '../lib/core.mjs';
8+
9+
function pageText(page) {
10+
return `---\ntitle: ${JSON.stringify(page.title)}\ndescription: ${JSON.stringify(page.summary)}\npageId: ${JSON.stringify(page.id)}\ncategory: ${JSON.stringify(page.category)}\nmode: "explanation"\ntype: ${JSON.stringify(page.type)}\norder: ${page.order}\n---\n# ${page.title}\n\nIdentical material body.\n`;
11+
}
12+
13+
function fixture() {
14+
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'docgen-audit-guard-'));
15+
const paths = projectPaths(root);
16+
const pages = [
17+
{ id: 'page-a', title: 'Page A', summary: 'First page', category: 'security', path: 'docs/security/page-a.md', type: 'security', order: 1, requiredSections: [], relatedPages: [] },
18+
{ id: 'page-b', title: 'Page B', summary: 'Second page', category: 'security', path: 'docs/security/page-b.md', type: 'security', order: 2, requiredSections: [], relatedPages: [] }
19+
];
20+
fs.mkdirSync(path.join(root, 'docs', 'security'), { recursive: true });
21+
fs.mkdirSync(path.join(paths.traceability, 'pages'), { recursive: true });
22+
for (const page of pages) {
23+
const text = pageText(page);
24+
fs.writeFileSync(path.join(root, page.path), text);
25+
writeJson(path.join(paths.traceability, 'pages', `${page.id}.json`), {
26+
schemaVersion: '2.0', pageId: page.id, pagePath: page.path,
27+
pageHash: sha256(text), inputHash: `${page.id}-input`, claims: []
28+
});
29+
}
30+
writeJson(paths.project, { schemaVersion: '2.0', kitVersion: '2.0.0' });
31+
writeJson(paths.config, {
32+
audit: { llmEnabled: true, llmRiskThreshold: 0, requireLineEvidenceForFacts: true, requireContextBoundEvidence: true },
33+
execution: { maxPlannedPages: 30 }
34+
});
35+
writeJson(paths.inventory, { schemaVersion: '2.0', fingerprint: 'fixture', files: [], excluded: [] });
36+
writeJson(paths.plan, { schemaVersion: '2.0', pages });
37+
writeJson(paths.state, {
38+
schemaVersion: '2.0', stages: {},
39+
pages: Object.fromEntries(pages.map((page) => [page.id, { status: 'completed', inputHash: `${page.id}-input` }]))
40+
});
41+
return { root, paths, pages };
42+
}
43+
44+
test('deterministic failure stops before the costly audit provider', async () => {
45+
const { root, paths } = fixture();
46+
let baseAuditCalls = 0;
47+
await assert.rejects(
48+
() => guardedAudit(root, async () => { baseAuditCalls++; throw new Error('provider path must not run'); }),
49+
/Quality failed before LLM audit: deterministicFailures=1, highRiskFindings=0/
50+
);
51+
assert.equal(baseAuditCalls, 0);
52+
const summary = JSON.parse(fs.readFileSync(path.join(paths.audit, 'quality-summary.json'), 'utf8'));
53+
assert.equal(summary.llmAuditedPages, 0);
54+
assert.equal(summary.highRiskFindings, 0);
55+
assert.equal(summary.llmSkippedReason, 'deterministic-fail-fast');
56+
assert.equal(summary.pass, false);
57+
});
58+
59+
test('sanitizer drops out-of-context evidence and refs and downgrades unsupported FACT', () => {
60+
const { root, paths, pages } = fixture();
61+
const page = pages[0];
62+
const contextFile = path.join(paths.context, 'generate', `${page.id}.json`);
63+
writeJson(contextFile, { id: 'ctx-a', modelItems: [], facts: [] });
64+
const traceFile = path.join(paths.traceability, 'pages', `${page.id}.json`);
65+
const trace = JSON.parse(fs.readFileSync(traceFile, 'utf8'));
66+
trace.contextId = 'ctx-a';
67+
trace.claims = [{
68+
id: 'claim-a', statement: 'Unsupported provider claim', classification: 'FACT', confidence: 1,
69+
evidence: [{ path: '../../outside.txt', startLine: 999 }], sourceModelRefs: ['missing:model']
70+
}];
71+
writeJson(traceFile, trace);
72+
const result = sanitizeAuditInputs(root, { pages: [page] });
73+
const sanitized = JSON.parse(fs.readFileSync(traceFile, 'utf8')).claims[0];
74+
assert.equal(result.traces.droppedEvidence, 1);
75+
assert.equal(result.traces.droppedRefs, 1);
76+
assert.equal(sanitized.classification, 'INFERENCE');
77+
assert.equal(sanitized.confidence, 0.7);
78+
assert.deepEqual(sanitized.evidence, []);
79+
assert.deepEqual(sanitized.sourceModelRefs, []);
80+
});

0 commit comments

Comments
 (0)