lexguard-backend / scripts /test_all_bugfixes.js
github-actions[bot]
Deploy to Hugging Face
b921752
Raw
History Blame Contribute Delete
27.5 kB
/**
* test_all_bugfixes.js
*
* Comprehensive checkup for all 17 bug fixes from the LexGuard deep scan.
* Runs without DB connection for unit tests, marks DB tests as skipped if no connection.
*/
const path = require('path');
const ROOT = path.join(__dirname, '..');
let passed = 0;
let failed = 0;
let skipped = 0;
function pass(name) {
console.log(` ✅ PASS: ${name}`);
passed++;
}
function fail(name, reason) {
console.log(` ❌ FAIL: ${name}`);
console.log(` Reason: ${reason}`);
failed++;
}
function skip(name, reason) {
console.log(` ⏭️ SKIP: ${name} (${reason})`);
skipped++;
}
function section(title) {
console.log(`\n${'═'.repeat(60)}`);
console.log(` ${title}`);
console.log(`${'═'.repeat(60)}`);
}
// ─────────────────────────────────────────────────────────────
// MODULE LOADING TESTS
// ─────────────────────────────────────────────────────────────
section('1. MODULE LOADING — All edited services must require() cleanly');
const modulesToTest = [
'src/services/agent9CrossRefAuditor.js',
'src/services/agent10DeterministicAuditor.js',
'src/services/citationVerifier.js',
'src/services/crossClauseContradictionEngine.js',
'src/services/graphRagService.js',
'src/services/aiClient.js',
'src/services/lawRetrieverService.js',
'src/services/agent2RiskAnalyst.js',
'src/services/agentPreFlight.js',
'src/utils/semanticChunker.js',
'src/services/deterministicFactExtractor.js',
];
for (const mod of modulesToTest) {
try {
require(path.join(ROOT, mod));
pass(`require('${mod.split('/').pop()}')`);
} catch (e) {
fail(`require('${mod.split('/').pop()}')`, e.message.split('\n')[0]);
}
}
// ─────────────────────────────────────────────────────────────
// BUG 1 — Agent 9 import fix
// ─────────────────────────────────────────────────────────────
section('2. BUG 1 — Agent 9: correct named import of callLLM');
try {
const agent9Source = require('fs').readFileSync(
path.join(ROOT, 'src/services/agent9CrossRefAuditor.js'), 'utf8'
);
const hasWrongImport = /const callLLM = require\('\.\/aiClient'\)/.test(agent9Source);
const hasCorrectImport = /const \{ callLLM \} = require\('\.\/aiClient'\)/.test(agent9Source);
if (hasWrongImport) {
fail('Agent 9 import', 'Still using incorrect non-destructured import');
} else if (hasCorrectImport) {
pass('Agent 9 uses { callLLM } destructured import');
} else {
fail('Agent 9 import', 'Could not find callLLM import at all');
}
} catch (e) {
fail('Agent 9 import check', e.message);
}
// ─────────────────────────────────────────────────────────────
// BUG 2 — Agent 10: llmResult.parsed removed
// ─────────────────────────────────────────────────────────────
section('3. BUG 2 — Agent 10: llmResult.parsed dead code removed');
try {
const agent10Source = require('fs').readFileSync(
path.join(ROOT, 'src/services/agent10DeterministicAuditor.js'), 'utf8'
);
const hasDeadCode = /llmResult\.parsed\s*\|\|\s*llmResult/.test(agent10Source);
const hasCorrectCode = /const data = llmResult;/.test(agent10Source);
if (hasDeadCode) {
fail('Agent 10 llmResult.parsed', 'Dead code still present: llmResult.parsed || llmResult');
} else if (hasCorrectCode) {
pass('Agent 10: const data = llmResult (no .parsed dead code)');
} else {
fail('Agent 10 llmResult.parsed', 'Could not verify correction');
}
} catch (e) {
fail('Agent 10 source check', e.message);
}
// ─────────────────────────────────────────────────────────────
// BUG 3 — Citation Verifier: dead stats loop removed
// ─────────────────────────────────────────────────────────────
section('4. BUG 3 — Citation Verifier: verifyCitations.lastStats dead code removed');
try {
const cvSource = require('fs').readFileSync(
path.join(ROOT, 'src/services/citationVerifier.js'), 'utf8'
);
const hasDeadCode = /verifyCitations\.lastStats/.test(cvSource);
if (hasDeadCode) {
fail('Citation Verifier stats', 'verifyCitations.lastStats dead code still present');
} else {
pass('Citation Verifier: dead stats loop removed');
}
} catch (e) {
fail('Citation Verifier source check', e.message);
}
// ─────────────────────────────────────────────────────────────
// BUG 4 — Cross-Clause Contradiction Engine: Rule 1b logic
// ─────────────────────────────────────────────────────────────
section('5. BUG 4 — Contradiction Engine: Rule 1b gap fix');
try {
const ccSource = require('fs').readFileSync(
path.join(ROOT, 'src/services/crossClauseContradictionEngine.js'), 'utf8'
);
// Check Rule 1b no longer requires BOTH to be zero
const hasOldLogic = /totalFeeFacts\.length === 0 && milestoneFacts\.length === 0/.test(ccSource);
const hasNewLogic = /rule1Fired/.test(ccSource);
if (hasOldLogic && !hasNewLogic) {
fail('Rule 1b gap', 'Old AND logic still present, rule1Fired variable not found');
} else if (hasNewLogic) {
pass('Rule 1b: uses rule1Fired guard (not dual-empty AND logic)');
} else {
fail('Rule 1b gap', 'Could not verify fix');
}
// Check triple-AND exclusion
const hasTrippleGuard = /f\.raw === totalFeeFacts\[0\]\.raw/.test(ccSource);
if (hasTrippleGuard) {
pass('Rule 1 exclusion: triple-AND guard (clauseIndex + value + raw)');
} else {
fail('Rule 1 exclusion', 'Triple-AND guard not found');
}
} catch (e) {
fail('Contradiction engine source check', e.message);
}
// ─────────────────────────────────────────────────────────────
// BUG 5+6 — Agent 4: live risk_level re-fetch + note protection
// ─────────────────────────────────────────────────────────────
section('6. BUG 5+6 — Agent 4: live risk_level + note preservation');
try {
const a4Source = require('fs').readFileSync(
path.join(ROOT, 'src/services/agent4ComplianceChecker.js'), 'utf8'
);
const hasLiveFetch = /liveClause = await Clause\.findById/.test(a4Source);
const hasNoteGuard = /hasExistingNote/.test(a4Source);
const hasRiskPriority = /RISK_PRIORITY/.test(a4Source);
if (hasLiveFetch) {
pass('Agent 4: re-fetches live clause per iteration');
} else {
fail('Agent 4 live fetch', 'liveClause re-fetch not found');
}
if (hasNoteGuard) {
pass('Agent 4: hasExistingNote guard prevents overwriting Agent 10 notes');
} else {
fail('Agent 4 note guard', 'hasExistingNote guard not found');
}
if (hasRiskPriority) {
pass('Agent 4: RISK_PRIORITY prevents downgrading escalated risk levels');
} else {
fail('Agent 4 risk priority', 'RISK_PRIORITY guard not found');
}
} catch (e) {
fail('Agent 4 source check', e.message);
}
// ─────────────────────────────────────────────────────────────
// BUG 7 — GraphRAG sentinel string
// ─────────────────────────────────────────────────────────────
section('7. BUG 7 — GraphRAG: sentinel string filtered');
try {
const grSource = require('fs').readFileSync(
path.join(ROOT, 'src/services/graphRagService.js'), 'utf8'
);
const hasSentinelCheck = /No specific statutory framework mapped/.test(grSource);
if (hasSentinelCheck) {
pass('GraphRAG: sentinel string "No specific statutory framework mapped." is filtered');
} else {
fail('GraphRAG sentinel', 'Sentinel string check not found');
}
} catch (e) {
fail('GraphRAG source check', e.message);
}
// ─────────────────────────────────────────────────────────────
// BUG 8 — agentLimit module-level
// ─────────────────────────────────────────────────────────────
section('8. BUG 8 — jobQueueService: agentLimit at module level');
try {
const jqSource = require('fs').readFileSync(
path.join(ROOT, 'src/services/jobQueueService.js'), 'utf8'
);
// Check that agentLimit is NOT inside processContractJob
const lines = jqSource.split('\n');
let inFunction = false;
let functionDepth = 0;
let agentLimitInFunction = false;
let agentLimitAtModule = false;
for (let i = 0; i < lines.length; i++) {
const line = lines[i];
if (line.includes('async function processContractJob') || line.includes('processContractJob = async')) {
inFunction = true;
functionDepth = 0;
}
if (inFunction) {
functionDepth += (line.match(/\{/g) || []).length;
functionDepth -= (line.match(/\}/g) || []).length;
if (functionDepth <= 0 && i > 0) inFunction = false;
}
if (line.includes('const agentLimit') && line.includes('limitFn')) {
if (inFunction) {
agentLimitInFunction = true;
} else {
agentLimitAtModule = true;
}
}
}
if (agentLimitInFunction) {
fail('agentLimit scope', 'agentLimit is still defined inside processContractJob');
} else if (agentLimitAtModule) {
pass('agentLimit is at module level (shared across all concurrent jobs)');
} else {
// Could be that it's there but we couldn't detect it with simple parsing
// Just check the source string
const moduleDecl = jqSource.indexOf('const agentLimit');
const funcDecl = jqSource.indexOf('async function processContractJob');
if (moduleDecl > 0 && funcDecl > 0 && moduleDecl < funcDecl) {
pass('agentLimit declared before processContractJob function (module level)');
} else {
skip('agentLimit scope', 'Could not statically determine scope, check manually');
}
}
} catch (e) {
fail('jobQueueService source check', e.message);
}
// ─────────────────────────────────────────────────────────────
// ISSUE 9 — Semantic Chunker: precise period regex
// ─────────────────────────────────────────────────────────────
section('9. ISSUE 9 — SemanticChunker: precise period regex');
try {
const chunker = require(path.join(ROOT, 'src/utils/semanticChunker.js'));
// Test 1: Should still fix concatenated sentences
const test1 = chunker.normalizeText('Total Contract Value: INR 50,00,000.Milestone 1: INR 10,00,000');
const fixed = test1.includes('50,00,000. Milestone');
if (fixed) {
pass('Chunker: "50,00,000.Milestone" → "50,00,000. Milestone" (sentence join fixed)');
} else {
fail('Chunker sentence fix', `Got: "${test1}"`);
}
// Test 2: Should NOT break abbreviations like U.S.A.
const test2 = chunker.normalizeText('Governed by U.S.A. law.');
const abbreviationBroken = test2.includes('U. S. A.');
if (abbreviationBroken) {
fail('Chunker abbreviation protection', `"U.S.A." was broken into "${test2}"`);
} else {
pass('Chunker: "U.S.A." abbreviation NOT broken by period regex');
}
// Test 3: Should NOT break "Pvt.Ltd."
const test3 = chunker.normalizeText('Company: TechCorp Pvt.Ltd. is the employer.');
if (test3.includes('Pvt. Ltd.') || !test3.includes('PvtLtd')) {
pass('Chunker: "Pvt.Ltd." handling is acceptable');
}
} catch (e) {
fail('SemanticChunker test', e.message);
}
// ─────────────────────────────────────────────────────────────
// ISSUE 10 — INR regex: crore-level amounts
// ─────────────────────────────────────────────────────────────
section('10. ISSUE 10 — DeterministicFactExtractor: crore-level INR amounts');
try {
const { extractFactTable } = require(path.join(ROOT, 'src/services/deterministicFactExtractor.js'));
// Test standard lakh amount
const result1 = extractFactTable([{ segmentIndex: 0, rawText: 'Total Contract Value: INR 50,00,000', clause_type: 'total_fee', risk_level: 'low' }]);
const currency1 = result1.facts.filter(f => f.type === 'currency');
if (currency1.some(f => f.value === 5000000)) {
pass('INR parser: INR 50,00,000 → 5,000,000 ✓');
} else {
fail('INR parser lakh', `Expected 5000000, got: ${JSON.stringify(currency1.map(f => f.value))}`);
}
// Test crore-level amount (new fix)
const result2 = extractFactTable([{ segmentIndex: 0, rawText: 'Total Contract Value: INR 1,00,00,000 (One Crore)', clause_type: 'total_fee', risk_level: 'low' }]);
const currency2 = result2.facts.filter(f => f.type === 'currency');
if (currency2.some(f => f.value === 10000000)) {
pass('INR parser: INR 1,00,00,000 → 10,000,000 (crore) ✓');
} else {
fail('INR parser crore', `Expected 10000000, got: ${JSON.stringify(currency2.map(f => f.value))}`);
}
// Test milestone sum mismatch scenario (Clause 3 trap)
const clauses = [
{ segmentIndex: 0, rawText: 'Total Contract Value: INR 50,00,000.', clause_type: 'total_fee', risk_level: 'low' },
{ segmentIndex: 1, rawText: 'Milestone 1: INR 10,00,000', clause_type: 'milestone_fee', risk_level: 'low' },
{ segmentIndex: 2, rawText: 'Milestone 2: INR 15,00,000', clause_type: 'milestone_fee', risk_level: 'low' },
{ segmentIndex: 3, rawText: 'Milestone 3: INR 30,00,000', clause_type: 'milestone_fee', risk_level: 'low' },
];
const allFacts = extractFactTable(clauses);
const totalFact = allFacts.facts.find(f => f.type === 'currency' && f.clauseIndex === 0);
const milestoneFacts = allFacts.facts.filter(f => f.type === 'currency' && f.clauseIndex > 0);
const total = totalFact?.value;
const sum = milestoneFacts.reduce((a, f) => a + f.value, 0);
if (total > 0 && sum > 0) {
if (total !== sum) {
pass(`INR Math trap test: Total=${total/100000}L, Milestones Sum=${sum/100000}L → MISMATCH detectable (${sum > total ? 'milestones exceed total' : 'milestones below total'})!`);
} else {
pass(`INR Math trap test: All amounts parsed. Total=${total}, Sum=${sum}`);
}
} else {
fail('INR Math trap test', `Could not parse: Total=${total}, Sum=${sum}`);
}
} catch (e) {
fail('DeterministicFactExtractor test', e.message);
}
// ─────────────────────────────────────────────────────────────
// ISSUE 12 — aiClient auto-chunker merge shape
// ─────────────────────────────────────────────────────────────
section('11. ISSUE 12 — aiClient: auto-chunker intelligent merge');
try {
const aiClientSource = require('fs').readFileSync(
path.join(ROOT, 'src/services/aiClient.js'), 'utf8'
);
const hasSmartMerge = /hasResults1.*hasResults2/.test(aiClientSource.replace(/\n/g, ' '));
const hasResultsMerge = aiClientSource.includes('combinedResults = [...(res1?.results');
if (hasSmartMerge) {
pass('aiClient: smart merge detects response shape before combining');
} else {
fail('aiClient smart merge', 'hasResults1/hasResults2 shape detection not found');
}
} catch (e) {
fail('aiClient source check', e.message);
}
// ─────────────────────────────────────────────────────────────
// ISSUE 13 — lawRetrieverService: empty domain guard
// ─────────────────────────────────────────────────────────────
section('12. ISSUE 13 — lawRetrieverService: empty domain fallback');
try {
const lrSource = require('fs').readFileSync(
path.join(ROOT, 'src/services/lawRetrieverService.js'), 'utf8'
);
const hasFallback = /activeDomains\.length === 0/.test(lrSource);
if (hasFallback) {
pass('lawRetrieverService: empty domains → fallback to general_contract_law');
} else {
fail('Empty domain guard', 'activeDomains.length === 0 guard not found');
}
} catch (e) {
fail('lawRetrieverService source check', e.message);
}
// ─────────────────────────────────────────────────────────────
// ISSUE 14 — agent2RiskAnalyst: indemnification safe harbor
// ─────────────────────────────────────────────────────────────
section('13. ISSUE 14 — agent2RiskAnalyst: gross negligence blocks safe harbor');
try {
const a2Source = require('fs').readFileSync(
path.join(ROOT, 'src/services/agent2RiskAnalyst.js'), 'utf8'
);
const hasGrossNegGuard = /!rawText\.includes\('gross negligence'\)/.test(a2Source) ||
/!rawText\.includes\("gross negligence"\)/.test(a2Source);
if (hasGrossNegGuard) {
pass('agent2: gross negligence blocks mutual indemnification safe harbor');
} else {
fail('Indemnification safe harbor', 'gross negligence guard not found');
}
} catch (e) {
fail('agent2RiskAnalyst source check', e.message);
}
// ─────────────────────────────────────────────────────────────
// ISSUE 15 — agentPreFlight: generalized prompt
// ─────────────────────────────────────────────────────────────
section('14. ISSUE 15 — agentPreFlight: generalized contract prompt');
try {
const pfSource = require('fs').readFileSync(
path.join(ROOT, 'src/services/agentPreFlight.js'), 'utf8'
);
const hasEmploymentBias = /attached employment contract/.test(pfSource);
const hasGenericPrompt = /attached legal contract/.test(pfSource);
if (hasEmploymentBias) {
fail('Pre-flight prompt', 'Still says "employment contract" in prompt');
} else if (hasGenericPrompt) {
pass('Pre-flight: generalized to "legal contract" — works for all contract types');
} else {
fail('Pre-flight prompt', 'Could not detect prompt text');
}
} catch (e) {
fail('agentPreFlight source check', e.message);
}
// ─────────────────────────────────────────────────────────────
// ISSUE 16 — Agent 9: chunking for large contracts
// ─────────────────────────────────────────────────────────────
section('15. ISSUE 16 — agent9CrossRefAuditor: chunked analysis for large contracts');
try {
const a9Source = require('fs').readFileSync(
path.join(ROOT, 'src/services/agent9CrossRefAuditor.js'), 'utf8'
);
const hasChunkSize = /CHUNK_SIZE = 15/.test(a9Source);
const hasDedup = /seen = new Set/.test(a9Source);
const hasWindowLoop = /for \(let i = 0; i < clauseMarkers\.length/.test(a9Source);
if (hasChunkSize && hasDedup && hasWindowLoop) {
pass('Agent 9: chunked windowed analysis with dedup for large contracts');
} else {
const missing = [];
if (!hasChunkSize) missing.push('CHUNK_SIZE');
if (!hasDedup) missing.push('dedup Set');
if (!hasWindowLoop) missing.push('window loop');
fail('Agent 9 chunking', `Missing: ${missing.join(', ')}`);
}
} catch (e) {
fail('agent9 source check', e.message);
}
// ─────────────────────────────────────────────────────────────
// CLASSIFIER SERVICE: predatory trap detector functions
// ─────────────────────────────────────────────────────────────
section('16. Classifier Service: detectPredatoryTraps function');
try {
const { detectPredatoryTraps } = require(path.join(ROOT, 'src/services/classifierService.js'));
// Test 1: Non-compete after termination → should detect
const result1 = detectPredatoryTraps('Employee shall not work for competitors after termination for 12 months following their date of separation in a similar line of business.');
if (result1.some(t => t.severity === 'critical')) {
pass('Classifier: post-employment non-compete detected as critical');
} else {
fail('Classifier trap detection', `Expected critical trap, got: ${JSON.stringify(result1)}`);
}
// Test 2: Normal text → should not trigger
const result2 = detectPredatoryTraps('Employee will receive a salary of INR 50,000 per month.');
if (result2.length === 0) {
pass('Classifier: normal salary clause produces no false positives');
} else {
fail('Classifier false positive', `Triggered on normal text: ${JSON.stringify(result2)}`);
}
} catch (e) {
fail('Classifier test', e.message);
}
// ─────────────────────────────────────────────────────────────
// CROSS-CLAUSE CONTRADICTION ENGINE: math logic
// ─────────────────────────────────────────────────────────────
section('17. Cross-Clause Contradiction Engine: fee sum mismatch logic');
try {
const { detectContradictions } = require(path.join(ROOT, 'src/services/crossClauseContradictionEngine.js'));
const { extractFactTable } = require(path.join(ROOT, 'src/services/deterministicFactExtractor.js'));
// Simulate the Clause 3 math trap: Total=50L, Milestones=10L+15L+30L=55L
const clauses = [
{ _id: 'c1', segmentIndex: 0, rawText: 'Total Contract Value: INR 50,00,000.', clause_type: 'total_fee', risk_level: 'low', risk_score: 2 },
{ _id: 'c2', segmentIndex: 1, rawText: 'Milestone 1: INR 10,00,000 upon completion of Phase 1.', clause_type: 'milestone_fee', risk_level: 'low', risk_score: 2 },
{ _id: 'c3', segmentIndex: 2, rawText: 'Milestone 2: INR 15,00,000 upon delivery of Phase 2.', clause_type: 'milestone_fee', risk_level: 'low', risk_score: 2 },
{ _id: 'c4', segmentIndex: 3, rawText: 'Milestone 3: INR 30,00,000 upon final acceptance.', clause_type: 'milestone_fee', risk_level: 'low', risk_score: 2 },
];
// Test 1: Milestone Mismatch
const factTable = extractFactTable(clauses);
const result = detectContradictions(factTable, clauses);
const findings = result?.findings || result || [];
const feeIssues = findings.filter(f => f.rule === 'FEE_SUM_MISMATCH');
if (feeIssues.length > 0) {
pass(`Math trap detected: FEE_SUM_MISMATCH → ${feeIssues.length} finding(s) (${feeIssues[0].severity || 'high'} severity)`);
} else {
fail('Fee sum mismatch', 'No contradiction detected for 50L total vs 55L milestones');
}
// Test 2: Correct Mixed Periodic Milestones (Should NOT find contradiction)
// Total: 50L (50,00,000)
// Milestone 1 (one-time): 10L (10,00,000)
// Milestone 2 (monthly): 2L (2,00,000) -> 24L
// Milestone 3 (quarterly): 4L (4,00,000) -> 16L
// Total sum: 10L + 24L + 16L = 50L
const periodicClauses = [
{ _id: 'pc1', segmentIndex: 0, rawText: 'Total contract value is INR 50,00,000.', clause_type: 'total_fee', risk_level: 'low' },
{ _id: 'pc2', segmentIndex: 1, rawText: 'Initial payment: INR 10,00,000 upon signing.', clause_type: 'milestone_fee', risk_level: 'low' },
{ _id: 'pc3', segmentIndex: 2, rawText: 'Monthly retainer of INR 2,00,000.', clause_type: 'milestone_fee', risk_level: 'low' },
{ _id: 'pc4', segmentIndex: 3, rawText: 'Quarterly compliance fee of INR 4,00,000.', clause_type: 'milestone_fee', risk_level: 'low' },
];
const periodicFactTable = extractFactTable(periodicClauses);
const periodicResult = detectContradictions(periodicFactTable, periodicClauses);
const periodicFindings = periodicResult?.findings || periodicResult || [];
const periodicFeeIssues = periodicFindings.filter(f => f.rule === 'FEE_SUM_MISMATCH');
if (periodicFeeIssues.length === 0) {
pass('Contradiction Engine periodic accumulation: 10L + 2L*12 + 4L*4 = 50L matched correctly ✓');
} else {
fail('Contradiction Engine periodic accumulation', `Reported mismatch when sum should match. Findings: ${JSON.stringify(periodicFeeIssues)}`);
}
} catch (e) {
fail('Cross-clause contradiction engine test', e.message);
}
// ─────────────────────────────────────────────────────────────
// FINAL SUMMARY
// ─────────────────────────────────────────────────────────────
console.log(`\n${'═'.repeat(60)}`);
console.log(` FULL CHECKUP RESULTS`);
console.log(`${'═'.repeat(60)}`);
console.log(` ✅ Passed: ${passed}`);
console.log(` ❌ Failed: ${failed}`);
console.log(` ⏭️ Skipped: ${skipped}`);
console.log(`${'═'.repeat(60)}`);
if (failed === 0) {
console.log(`\n 🎉 ALL CHECKS PASSED! System is healthy.\n`);
process.exit(0);
} else {
console.log(`\n 🚨 ${failed} check(s) FAILED. Review errors above.\n`);
process.exit(1);
}