@codexstar/bug-hunter 3.0.0 → 3.0.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +149 -83
- package/README.md +150 -15
- package/SKILL.md +94 -27
- package/agents/openai.yaml +4 -0
- package/bin/bug-hunter +9 -3
- package/docs/images/2026-03-12-fix-plan-rollout.png +0 -0
- package/docs/images/2026-03-12-hero-bug-hunter-overview.png +0 -0
- package/docs/images/2026-03-12-machine-readable-artifacts.png +0 -0
- package/docs/images/2026-03-12-pr-review-flow.png +0 -0
- package/docs/images/2026-03-12-security-pack.png +0 -0
- package/docs/images/adversarial-debate.png +0 -0
- package/docs/images/doc-verify-fix-plan.png +0 -0
- package/docs/images/hero.png +0 -0
- package/docs/images/pipeline-overview.png +0 -0
- package/docs/images/security-finding-card.png +0 -0
- package/docs/plans/2026-03-11-structured-output-migration-plan.md +288 -0
- package/docs/plans/2026-03-12-audit-bug-fixes-surgical-plan.md +193 -0
- package/docs/plans/2026-03-12-enterprise-security-pack-e2e-plan.md +59 -0
- package/docs/plans/2026-03-12-local-security-skills-integration-plan.md +39 -0
- package/docs/plans/2026-03-12-pr-review-strategic-fix-flow.md +78 -0
- package/evals/evals.json +366 -102
- package/modes/extended.md +2 -2
- package/modes/fix-loop.md +30 -30
- package/modes/fix-pipeline.md +32 -6
- package/modes/large-codebase.md +14 -15
- package/modes/local-sequential.md +44 -20
- package/modes/loop.md +56 -56
- package/modes/parallel.md +3 -3
- package/modes/scaled.md +2 -2
- package/modes/single-file.md +3 -3
- package/modes/small.md +11 -11
- package/package.json +11 -1
- package/prompts/fixer.md +37 -23
- package/prompts/hunter.md +39 -20
- package/prompts/referee.md +34 -20
- package/prompts/skeptic.md +25 -22
- package/schemas/coverage.schema.json +67 -0
- package/schemas/examples/findings.invalid.json +13 -0
- package/schemas/examples/findings.valid.json +17 -0
- package/schemas/findings.schema.json +76 -0
- package/schemas/fix-plan.schema.json +94 -0
- package/schemas/fix-report.schema.json +105 -0
- package/schemas/fix-strategy.schema.json +99 -0
- package/schemas/recon.schema.json +31 -0
- package/schemas/referee.schema.json +46 -0
- package/schemas/shared.schema.json +51 -0
- package/schemas/skeptic.schema.json +21 -0
- package/scripts/bug-hunter-state.cjs +35 -12
- package/scripts/code-index.cjs +11 -4
- package/scripts/fix-lock.cjs +95 -25
- package/scripts/payload-guard.cjs +24 -10
- package/scripts/pr-scope.cjs +181 -0
- package/scripts/prepublish-guard.cjs +82 -0
- package/scripts/render-report.cjs +346 -0
- package/scripts/run-bug-hunter.cjs +669 -33
- package/scripts/schema-runtime.cjs +273 -0
- package/scripts/schema-validate.cjs +40 -0
- package/scripts/tests/bug-hunter-state.test.cjs +68 -3
- package/scripts/tests/code-index.test.cjs +15 -0
- package/scripts/tests/fix-lock.test.cjs +60 -2
- package/scripts/tests/fixtures/flaky-worker.cjs +6 -1
- package/scripts/tests/fixtures/low-confidence-worker.cjs +8 -2
- package/scripts/tests/fixtures/success-worker.cjs +6 -1
- package/scripts/tests/payload-guard.test.cjs +154 -2
- package/scripts/tests/pr-scope.test.cjs +212 -0
- package/scripts/tests/render-report.test.cjs +180 -0
- package/scripts/tests/run-bug-hunter.test.cjs +686 -2
- package/scripts/tests/security-skills-integration.test.cjs +29 -0
- package/scripts/tests/skills-packaging.test.cjs +30 -0
- package/scripts/tests/worktree-harvest.test.cjs +67 -1
- package/scripts/worktree-harvest.cjs +62 -9
- package/skills/README.md +19 -0
- package/skills/commit-security-scan/SKILL.md +63 -0
- package/skills/security-review/SKILL.md +57 -0
- package/skills/threat-model-generation/SKILL.md +47 -0
- package/skills/vulnerability-validation/SKILL.md +59 -0
- package/templates/subagent-wrapper.md +12 -3
- package/modes/_dispatch.md +0 -121
|
@@ -8,6 +8,7 @@ const {
|
|
|
8
8
|
readJson,
|
|
9
9
|
resolveSkillScript,
|
|
10
10
|
runJson,
|
|
11
|
+
runRaw,
|
|
11
12
|
writeJson
|
|
12
13
|
} = require('./test-utils.cjs');
|
|
13
14
|
|
|
@@ -31,19 +32,41 @@ test('run-bug-hunter preflight tolerates missing optional code-index helper', ()
|
|
|
31
32
|
const runner = resolveSkillScript('run-bug-hunter.cjs');
|
|
32
33
|
const optionalSkillDir = path.join(sandbox, 'skill');
|
|
33
34
|
const scriptsDir = path.join(optionalSkillDir, 'scripts');
|
|
35
|
+
const schemasDir = path.join(optionalSkillDir, 'schemas');
|
|
34
36
|
fs.mkdirSync(scriptsDir, { recursive: true });
|
|
37
|
+
fs.mkdirSync(schemasDir, { recursive: true });
|
|
35
38
|
|
|
36
39
|
for (const fileName of [
|
|
37
40
|
'run-bug-hunter.cjs',
|
|
38
41
|
'bug-hunter-state.cjs',
|
|
39
42
|
'payload-guard.cjs',
|
|
43
|
+
'schema-validate.cjs',
|
|
44
|
+
'schema-runtime.cjs',
|
|
45
|
+
'render-report.cjs',
|
|
40
46
|
'fix-lock.cjs',
|
|
41
47
|
'doc-lookup.cjs',
|
|
42
48
|
'context7-api.cjs',
|
|
43
|
-
'delta-mode.cjs'
|
|
49
|
+
'delta-mode.cjs',
|
|
50
|
+
'pr-scope.cjs'
|
|
44
51
|
]) {
|
|
45
52
|
fs.copyFileSync(resolveSkillScript(fileName), path.join(scriptsDir, fileName));
|
|
46
53
|
}
|
|
54
|
+
for (const fileName of [
|
|
55
|
+
'findings.schema.json',
|
|
56
|
+
'skeptic.schema.json',
|
|
57
|
+
'referee.schema.json',
|
|
58
|
+
'coverage.schema.json',
|
|
59
|
+
'fix-report.schema.json',
|
|
60
|
+
'fix-plan.schema.json',
|
|
61
|
+
'fix-strategy.schema.json',
|
|
62
|
+
'recon.schema.json',
|
|
63
|
+
'shared.schema.json'
|
|
64
|
+
]) {
|
|
65
|
+
fs.copyFileSync(
|
|
66
|
+
resolveSkillScript('..', 'schemas', fileName),
|
|
67
|
+
path.join(schemasDir, fileName)
|
|
68
|
+
);
|
|
69
|
+
}
|
|
47
70
|
|
|
48
71
|
const result = runJson('node', [
|
|
49
72
|
path.join(scriptsDir, 'run-bug-hunter.cjs'),
|
|
@@ -151,7 +174,11 @@ test('run-bug-hunter integrates index+delta, fact cards, consistency pass, and f
|
|
|
151
174
|
const seenFilesPath = path.join(sandbox, 'seen-files.json');
|
|
152
175
|
const consistencyReportPath = path.join(sandbox, '.claude', 'bug-hunter-consistency.json');
|
|
153
176
|
const fixPlanPath = path.join(sandbox, '.claude', 'bug-hunter-fix-plan.json');
|
|
177
|
+
const strategyPath = path.join(sandbox, '.claude', 'bug-hunter-fix-strategy.json');
|
|
178
|
+
const strategyMarkdownPath = path.join(sandbox, '.claude', 'bug-hunter-fix-strategy.md');
|
|
154
179
|
const factsPath = path.join(sandbox, '.claude', 'bug-hunter-facts.json');
|
|
180
|
+
const coveragePath = path.join(sandbox, '.claude', 'coverage.json');
|
|
181
|
+
const coverageMarkdownPath = path.join(sandbox, '.claude', 'coverage.md');
|
|
155
182
|
|
|
156
183
|
const changedFile = path.join(sandbox, 'src', 'feature', 'changed.ts');
|
|
157
184
|
const depFile = path.join(sandbox, 'src', 'feature', 'dep.ts');
|
|
@@ -212,6 +239,10 @@ test('run-bug-hunter integrates index+delta, fact cards, consistency pass, and f
|
|
|
212
239
|
consistencyReportPath,
|
|
213
240
|
'--fix-plan-path',
|
|
214
241
|
fixPlanPath,
|
|
242
|
+
'--strategy-path',
|
|
243
|
+
strategyPath,
|
|
244
|
+
'--strategy-markdown-path',
|
|
245
|
+
strategyMarkdownPath,
|
|
215
246
|
'--facts-path',
|
|
216
247
|
factsPath,
|
|
217
248
|
'--use-index',
|
|
@@ -235,7 +266,11 @@ test('run-bug-hunter integrates index+delta, fact cards, consistency pass, and f
|
|
|
235
266
|
assert.equal(result.deltaSummary.selectedCount >= 2, true);
|
|
236
267
|
assert.equal(fs.existsSync(consistencyReportPath), true);
|
|
237
268
|
assert.equal(fs.existsSync(fixPlanPath), true);
|
|
269
|
+
assert.equal(fs.existsSync(strategyPath), true);
|
|
270
|
+
assert.equal(fs.existsSync(strategyMarkdownPath), true);
|
|
238
271
|
assert.equal(fs.existsSync(factsPath), true);
|
|
272
|
+
assert.equal(fs.existsSync(coveragePath), true);
|
|
273
|
+
assert.equal(fs.existsSync(coverageMarkdownPath), true);
|
|
239
274
|
|
|
240
275
|
const seenFiles = readJson(seenFilesPath);
|
|
241
276
|
assert.equal(seenFiles.includes(overlayFile), true);
|
|
@@ -243,12 +278,25 @@ test('run-bug-hunter integrates index+delta, fact cards, consistency pass, and f
|
|
|
243
278
|
const state = readJson(statePath);
|
|
244
279
|
assert.equal(Object.keys(state.factCards || {}).length >= 3, true);
|
|
245
280
|
assert.equal(state.metrics.lowConfidenceFindings >= 1, true);
|
|
281
|
+
assert.equal(state.bugLedger.every((entry) => typeof entry.confidenceScore === 'number'), true);
|
|
246
282
|
|
|
247
283
|
const consistency = readJson(consistencyReportPath);
|
|
248
284
|
assert.equal(consistency.lowConfidenceFindings >= 1, true);
|
|
249
285
|
|
|
250
286
|
const fixPlan = readJson(fixPlanPath);
|
|
251
287
|
assert.equal(fixPlan.totals.manualReview >= 1, true);
|
|
288
|
+
|
|
289
|
+
const fixStrategy = readJson(strategyPath);
|
|
290
|
+
assert.equal(fixStrategy.summary.manualReview >= 1, true);
|
|
291
|
+
assert.equal(Array.isArray(fixStrategy.clusters), true);
|
|
292
|
+
|
|
293
|
+
const strategyMarkdown = fs.readFileSync(strategyMarkdownPath, 'utf8');
|
|
294
|
+
assert.match(strategyMarkdown, /# Fix Strategy/);
|
|
295
|
+
|
|
296
|
+
const coverage = readJson(coveragePath);
|
|
297
|
+
assert.equal(coverage.status, 'COMPLETE');
|
|
298
|
+
assert.equal(Array.isArray(coverage.files), true);
|
|
299
|
+
assert.equal(Array.isArray(coverage.bugs), true);
|
|
252
300
|
});
|
|
253
301
|
|
|
254
302
|
test('run-bug-hunter builds canary fix subset from high-confidence findings', () => {
|
|
@@ -314,6 +362,126 @@ test('run-bug-hunter builds canary fix subset from high-confidence findings', ()
|
|
|
314
362
|
assert.equal(fixPlan.totals.canary, 1);
|
|
315
363
|
});
|
|
316
364
|
|
|
365
|
+
test('run-bug-hunter excludes non-autofix strategy findings from the executable fix plan', () => {
|
|
366
|
+
const sandbox = makeSandbox('run-bug-hunter-strategy-gate-');
|
|
367
|
+
const runner = resolveSkillScript('run-bug-hunter.cjs');
|
|
368
|
+
const skillDir = path.resolve(__dirname, '..', '..');
|
|
369
|
+
const filesJsonPath = path.join(sandbox, 'files.json');
|
|
370
|
+
const statePath = path.join(sandbox, '.claude', 'bug-hunter-state.json');
|
|
371
|
+
const fixPlanPath = path.join(sandbox, '.claude', 'bug-hunter-fix-plan.json');
|
|
372
|
+
const workerPath = path.join(sandbox, 'worker.cjs');
|
|
373
|
+
|
|
374
|
+
const fileA = path.join(sandbox, 'src', 'architecture.ts');
|
|
375
|
+
fs.mkdirSync(path.dirname(fileA), { recursive: true });
|
|
376
|
+
fs.writeFileSync(fileA, 'export const architecture = true;\n', 'utf8');
|
|
377
|
+
writeJson(filesJsonPath, [fileA]);
|
|
378
|
+
|
|
379
|
+
fs.writeFileSync(workerPath, [
|
|
380
|
+
'#!/usr/bin/env node',
|
|
381
|
+
"const fs = require('fs');",
|
|
382
|
+
"const findingsPath = process.argv[process.argv.indexOf('--findings-json') + 1];",
|
|
383
|
+
"const scanPath = process.argv[process.argv.indexOf('--scan-files-json') + 1];",
|
|
384
|
+
"const scanFiles = JSON.parse(fs.readFileSync(scanPath, 'utf8'));",
|
|
385
|
+
"fs.writeFileSync(findingsPath, JSON.stringify([{ bugId: 'BUG-ARCH', severity: 'Critical', category: 'logic', file: scanFiles[0], lines: '1', claim: 'architecture contract violation in orchestration flow', evidence: scanFiles[0] + ':1 architecture evidence', runtimeTrigger: 'Run the orchestrator on this file', crossReferences: ['Single file'], confidenceScore: 98, confidenceLabel: 'high', stride: 'N/A', cwe: 'N/A' }], null, 2));"
|
|
386
|
+
].join('\n'), 'utf8');
|
|
387
|
+
|
|
388
|
+
runJson('node', [
|
|
389
|
+
runner,
|
|
390
|
+
'run',
|
|
391
|
+
'--skill-dir',
|
|
392
|
+
skillDir,
|
|
393
|
+
'--files-json',
|
|
394
|
+
filesJsonPath,
|
|
395
|
+
'--state',
|
|
396
|
+
statePath,
|
|
397
|
+
'--mode',
|
|
398
|
+
'extended',
|
|
399
|
+
'--chunk-size',
|
|
400
|
+
'1',
|
|
401
|
+
'--worker-cmd',
|
|
402
|
+
`node ${workerPath} --chunk-id {chunkId} --scan-files-json {scanFilesJson} --findings-json {findingsJson}`,
|
|
403
|
+
'--timeout-ms',
|
|
404
|
+
'5000',
|
|
405
|
+
'--confidence-threshold',
|
|
406
|
+
'75',
|
|
407
|
+
'--fix-plan-path',
|
|
408
|
+
fixPlanPath,
|
|
409
|
+
'--canary-size',
|
|
410
|
+
'1'
|
|
411
|
+
], {
|
|
412
|
+
cwd: sandbox
|
|
413
|
+
});
|
|
414
|
+
|
|
415
|
+
const fixPlan = readJson(fixPlanPath);
|
|
416
|
+
assert.equal(fixPlan.totals.eligible, 0);
|
|
417
|
+
assert.equal(fixPlan.totals.canary, 0);
|
|
418
|
+
assert.equal(fixPlan.totals.rollout, 0);
|
|
419
|
+
});
|
|
420
|
+
|
|
421
|
+
test('run-bug-hunter downgrades conflicting findings to manual review before fix-plan execution', () => {
|
|
422
|
+
const sandbox = makeSandbox('run-bug-hunter-conflicts-');
|
|
423
|
+
const runner = resolveSkillScript('run-bug-hunter.cjs');
|
|
424
|
+
const skillDir = path.resolve(__dirname, '..', '..');
|
|
425
|
+
const filesJsonPath = path.join(sandbox, 'files.json');
|
|
426
|
+
const statePath = path.join(sandbox, '.claude', 'bug-hunter-state.json');
|
|
427
|
+
const fixPlanPath = path.join(sandbox, '.claude', 'bug-hunter-fix-plan.json');
|
|
428
|
+
const consistencyPath = path.join(sandbox, '.claude', 'consistency.json');
|
|
429
|
+
const workerPath = path.join(sandbox, 'worker.cjs');
|
|
430
|
+
|
|
431
|
+
const fileA = path.join(sandbox, 'src', 'conflict.ts');
|
|
432
|
+
fs.mkdirSync(path.dirname(fileA), { recursive: true });
|
|
433
|
+
fs.writeFileSync(fileA, 'export const conflict = true;\n', 'utf8');
|
|
434
|
+
writeJson(filesJsonPath, [fileA]);
|
|
435
|
+
|
|
436
|
+
fs.writeFileSync(workerPath, [
|
|
437
|
+
'#!/usr/bin/env node',
|
|
438
|
+
"const fs = require('fs');",
|
|
439
|
+
"const findingsPath = process.argv[process.argv.indexOf('--findings-json') + 1];",
|
|
440
|
+
"const scanPath = process.argv[process.argv.indexOf('--scan-files-json') + 1];",
|
|
441
|
+
"const scanFiles = JSON.parse(fs.readFileSync(scanPath, 'utf8'));",
|
|
442
|
+
"fs.writeFileSync(findingsPath, JSON.stringify([",
|
|
443
|
+
" { bugId: 'BUG-1', severity: 'Critical', category: 'logic', file: scanFiles[0], lines: '1', claim: 'first conflicting claim', evidence: scanFiles[0] + ':1 first', runtimeTrigger: 'Trigger first', crossReferences: ['Single file'], confidenceScore: 97, confidenceLabel: 'high', stride: 'N/A', cwe: 'N/A' },",
|
|
444
|
+
" { bugId: 'BUG-2', severity: 'Critical', category: 'logic', file: scanFiles[0], lines: '1', claim: 'second conflicting claim', evidence: scanFiles[0] + ':1 second', runtimeTrigger: 'Trigger second', crossReferences: ['Single file'], confidenceScore: 96, confidenceLabel: 'high', stride: 'N/A', cwe: 'N/A' }",
|
|
445
|
+
"], null, 2));"
|
|
446
|
+
].join('\n'), 'utf8');
|
|
447
|
+
|
|
448
|
+
runJson('node', [
|
|
449
|
+
runner,
|
|
450
|
+
'run',
|
|
451
|
+
'--skill-dir',
|
|
452
|
+
skillDir,
|
|
453
|
+
'--files-json',
|
|
454
|
+
filesJsonPath,
|
|
455
|
+
'--state',
|
|
456
|
+
statePath,
|
|
457
|
+
'--mode',
|
|
458
|
+
'extended',
|
|
459
|
+
'--chunk-size',
|
|
460
|
+
'1',
|
|
461
|
+
'--worker-cmd',
|
|
462
|
+
`node ${workerPath} --chunk-id {chunkId} --scan-files-json {scanFilesJson} --findings-json {findingsJson}`,
|
|
463
|
+
'--timeout-ms',
|
|
464
|
+
'5000',
|
|
465
|
+
'--confidence-threshold',
|
|
466
|
+
'75',
|
|
467
|
+
'--fix-plan-path',
|
|
468
|
+
fixPlanPath,
|
|
469
|
+
'--consistency-report',
|
|
470
|
+
consistencyPath,
|
|
471
|
+
'--canary-size',
|
|
472
|
+
'1'
|
|
473
|
+
], {
|
|
474
|
+
cwd: sandbox
|
|
475
|
+
});
|
|
476
|
+
|
|
477
|
+
const consistency = readJson(consistencyPath);
|
|
478
|
+
assert.equal(consistency.conflicts.length >= 1, true);
|
|
479
|
+
|
|
480
|
+
const fixPlan = readJson(fixPlanPath);
|
|
481
|
+
assert.equal(fixPlan.totals.eligible, 0);
|
|
482
|
+
assert.equal(fixPlan.totals.manualReview, 2);
|
|
483
|
+
});
|
|
484
|
+
|
|
317
485
|
test('run-bug-hunter respects configured delta hops during low-confidence expansion', () => {
|
|
318
486
|
const sandbox = makeSandbox('run-bug-hunter-delta-hops-');
|
|
319
487
|
const runner = resolveSkillScript('run-bug-hunter.cjs');
|
|
@@ -347,7 +515,7 @@ test('run-bug-hunter respects configured delta hops during low-confidence expans
|
|
|
347
515
|
"if (fs.existsSync(seenPath)) seen = JSON.parse(fs.readFileSync(seenPath, 'utf8'));",
|
|
348
516
|
'seen.push(scan);',
|
|
349
517
|
"fs.writeFileSync(seenPath, JSON.stringify(seen));",
|
|
350
|
-
"const findings = scan[0] === changedPath ? [{ file: scan[0], lines: '1', claim: 'low confidence',
|
|
518
|
+
"const findings = scan[0] === changedPath ? [{ bugId: 'BUG-inline', severity: 'Low', category: 'logic', file: scan[0], lines: '1', claim: 'low confidence', evidence: scan[0] + ':1 inline evidence', runtimeTrigger: 'Load the changed file', crossReferences: ['Single file'], confidenceScore: 60 }] : [];",
|
|
351
519
|
"fs.writeFileSync(findingsPath, JSON.stringify(findings));"
|
|
352
520
|
].join('\n'), 'utf8');
|
|
353
521
|
|
|
@@ -401,3 +569,519 @@ test('run-bug-hunter respects configured delta hops during low-confidence expans
|
|
|
401
569
|
const seenFiles = readJson(seenFilesPath).flat();
|
|
402
570
|
assert.equal(seenFiles.includes(twoHopFile), false);
|
|
403
571
|
});
|
|
572
|
+
|
|
573
|
+
test('run-bug-hunter retries malformed findings and records schema errors in the journal', () => {
|
|
574
|
+
const sandbox = makeSandbox('run-bug-hunter-invalid-findings-');
|
|
575
|
+
const runner = resolveSkillScript('run-bug-hunter.cjs');
|
|
576
|
+
const skillDir = path.resolve(__dirname, '..', '..');
|
|
577
|
+
const filesJsonPath = path.join(sandbox, 'files.json');
|
|
578
|
+
const statePath = path.join(sandbox, '.claude', 'bug-hunter-state.json');
|
|
579
|
+
const journalPath = path.join(sandbox, '.claude', 'bug-hunter-run.log');
|
|
580
|
+
const attemptsFile = path.join(sandbox, 'attempts.json');
|
|
581
|
+
|
|
582
|
+
const sourceFile = path.join(sandbox, 'src', 'a.ts');
|
|
583
|
+
fs.mkdirSync(path.dirname(sourceFile), { recursive: true });
|
|
584
|
+
fs.writeFileSync(sourceFile, 'export const a = 1;\n', 'utf8');
|
|
585
|
+
writeJson(filesJsonPath, [sourceFile]);
|
|
586
|
+
|
|
587
|
+
const workerPath = path.join(sandbox, 'invalid-then-valid-worker.cjs');
|
|
588
|
+
fs.writeFileSync(workerPath, [
|
|
589
|
+
'#!/usr/bin/env node',
|
|
590
|
+
"const fs = require('fs');",
|
|
591
|
+
"const path = require('path');",
|
|
592
|
+
"const args = process.argv;",
|
|
593
|
+
"const chunkId = args[args.indexOf('--chunk-id') + 1];",
|
|
594
|
+
"const findingsPath = args[args.indexOf('--findings-json') + 1];",
|
|
595
|
+
"const attemptsPath = args[args.indexOf('--attempts-file') + 1];",
|
|
596
|
+
'let attempts = {};',
|
|
597
|
+
"if (fs.existsSync(attemptsPath)) attempts = JSON.parse(fs.readFileSync(attemptsPath, 'utf8'));",
|
|
598
|
+
"attempts[chunkId] = (attempts[chunkId] || 0) + 1;",
|
|
599
|
+
"fs.mkdirSync(path.dirname(attemptsPath), { recursive: true });",
|
|
600
|
+
"fs.writeFileSync(attemptsPath, JSON.stringify(attempts, null, 2));",
|
|
601
|
+
"const payload = attempts[chunkId] === 1",
|
|
602
|
+
" ? [{ bugId: 'BUG-1', severity: 'Low', category: 'logic', file: 'src/a.ts', lines: '1', evidence: 'src/a.ts:1 evidence', runtimeTrigger: 'Call a()', crossReferences: ['Single file'], confidenceScore: 60 }]",
|
|
603
|
+
" : [{ bugId: 'BUG-1', severity: 'Low', category: 'logic', file: 'src/a.ts', lines: '1', claim: 'valid after retry', evidence: 'src/a.ts:1 evidence', runtimeTrigger: 'Call a()', crossReferences: ['Single file'], confidenceScore: 60 }];",
|
|
604
|
+
"fs.writeFileSync(findingsPath, JSON.stringify(payload, null, 2));"
|
|
605
|
+
].join('\n'), 'utf8');
|
|
606
|
+
|
|
607
|
+
const workerTemplate = [
|
|
608
|
+
'node',
|
|
609
|
+
workerPath,
|
|
610
|
+
'--chunk-id',
|
|
611
|
+
'{chunkId}',
|
|
612
|
+
'--findings-json',
|
|
613
|
+
'{findingsJson}',
|
|
614
|
+
'--attempts-file',
|
|
615
|
+
attemptsFile
|
|
616
|
+
].join(' ');
|
|
617
|
+
|
|
618
|
+
const result = runJson('node', [
|
|
619
|
+
runner,
|
|
620
|
+
'run',
|
|
621
|
+
'--skill-dir',
|
|
622
|
+
skillDir,
|
|
623
|
+
'--files-json',
|
|
624
|
+
filesJsonPath,
|
|
625
|
+
'--state',
|
|
626
|
+
statePath,
|
|
627
|
+
'--chunk-size',
|
|
628
|
+
'1',
|
|
629
|
+
'--worker-cmd',
|
|
630
|
+
workerTemplate,
|
|
631
|
+
'--timeout-ms',
|
|
632
|
+
'5000',
|
|
633
|
+
'--max-retries',
|
|
634
|
+
'1',
|
|
635
|
+
'--backoff-ms',
|
|
636
|
+
'10',
|
|
637
|
+
'--journal-path',
|
|
638
|
+
journalPath
|
|
639
|
+
], {
|
|
640
|
+
cwd: sandbox
|
|
641
|
+
});
|
|
642
|
+
|
|
643
|
+
assert.equal(result.ok, true);
|
|
644
|
+
const attempts = readJson(attemptsFile);
|
|
645
|
+
assert.equal(attempts['chunk-1'], 2);
|
|
646
|
+
const journal = fs.readFileSync(journalPath, 'utf8');
|
|
647
|
+
assert.match(journal, /attempt-post-check-failed/);
|
|
648
|
+
assert.match(journal, /\$\[0\]\.claim is required/);
|
|
649
|
+
});
|
|
650
|
+
|
|
651
|
+
test('run-bug-hunter clears stale findings artifacts before retrying a chunk', () => {
|
|
652
|
+
const sandbox = makeSandbox('run-bug-hunter-stale-artifact-');
|
|
653
|
+
const runner = resolveSkillScript('run-bug-hunter.cjs');
|
|
654
|
+
const skillDir = path.resolve(__dirname, '..', '..');
|
|
655
|
+
const filesJsonPath = path.join(sandbox, 'files.json');
|
|
656
|
+
const statePath = path.join(sandbox, '.claude', 'bug-hunter-state.json');
|
|
657
|
+
const journalPath = path.join(sandbox, '.claude', 'bug-hunter-run.log');
|
|
658
|
+
const attemptsFile = path.join(sandbox, 'attempts.json');
|
|
659
|
+
const sourceFile = path.join(sandbox, 'src', 'a.ts');
|
|
660
|
+
|
|
661
|
+
fs.mkdirSync(path.dirname(sourceFile), { recursive: true });
|
|
662
|
+
fs.writeFileSync(sourceFile, 'export const a = 1;\n', 'utf8');
|
|
663
|
+
writeJson(filesJsonPath, [sourceFile]);
|
|
664
|
+
|
|
665
|
+
const workerPath = path.join(sandbox, 'stale-artifact-worker.cjs');
|
|
666
|
+
fs.writeFileSync(workerPath, [
|
|
667
|
+
'#!/usr/bin/env node',
|
|
668
|
+
"const fs = require('fs');",
|
|
669
|
+
"const path = require('path');",
|
|
670
|
+
"const args = process.argv;",
|
|
671
|
+
"const chunkId = args[args.indexOf('--chunk-id') + 1];",
|
|
672
|
+
"const findingsPath = args[args.indexOf('--findings-json') + 1];",
|
|
673
|
+
"const attemptsPath = args[args.indexOf('--attempts-file') + 1];",
|
|
674
|
+
'let attempts = {};',
|
|
675
|
+
"if (fs.existsSync(attemptsPath)) attempts = JSON.parse(fs.readFileSync(attemptsPath, 'utf8'));",
|
|
676
|
+
"attempts[chunkId] = (attempts[chunkId] || 0) + 1;",
|
|
677
|
+
"fs.mkdirSync(path.dirname(attemptsPath), { recursive: true });",
|
|
678
|
+
"fs.writeFileSync(attemptsPath, JSON.stringify(attempts, null, 2));",
|
|
679
|
+
'if (attempts[chunkId] === 1) {',
|
|
680
|
+
" fs.writeFileSync(findingsPath, JSON.stringify([{ bugId: 'BUG-stale', severity: 'Low', category: 'logic', file: 'src/a.ts', lines: '1', claim: 'stale artifact', evidence: 'src/a.ts:1 evidence', runtimeTrigger: 'Call a()', crossReferences: ['Single file'], confidenceScore: 60 }], null, 2));",
|
|
681
|
+
' process.exit(1);',
|
|
682
|
+
'}',
|
|
683
|
+
'process.exit(0);'
|
|
684
|
+
].join('\n'), 'utf8');
|
|
685
|
+
|
|
686
|
+
const workerTemplate = [
|
|
687
|
+
'node',
|
|
688
|
+
workerPath,
|
|
689
|
+
'--chunk-id',
|
|
690
|
+
'{chunkId}',
|
|
691
|
+
'--findings-json',
|
|
692
|
+
'{findingsJson}',
|
|
693
|
+
'--attempts-file',
|
|
694
|
+
attemptsFile
|
|
695
|
+
].join(' ');
|
|
696
|
+
|
|
697
|
+
const result = runJson('node', [
|
|
698
|
+
runner,
|
|
699
|
+
'run',
|
|
700
|
+
'--skill-dir',
|
|
701
|
+
skillDir,
|
|
702
|
+
'--files-json',
|
|
703
|
+
filesJsonPath,
|
|
704
|
+
'--state',
|
|
705
|
+
statePath,
|
|
706
|
+
'--chunk-size',
|
|
707
|
+
'1',
|
|
708
|
+
'--worker-cmd',
|
|
709
|
+
workerTemplate,
|
|
710
|
+
'--timeout-ms',
|
|
711
|
+
'5000',
|
|
712
|
+
'--max-retries',
|
|
713
|
+
'1',
|
|
714
|
+
'--backoff-ms',
|
|
715
|
+
'10',
|
|
716
|
+
'--journal-path',
|
|
717
|
+
journalPath
|
|
718
|
+
], {
|
|
719
|
+
cwd: sandbox
|
|
720
|
+
});
|
|
721
|
+
|
|
722
|
+
assert.equal(result.ok, true);
|
|
723
|
+
const attempts = readJson(attemptsFile);
|
|
724
|
+
assert.equal(attempts['chunk-1'], 2);
|
|
725
|
+
const state = readJson(statePath);
|
|
726
|
+
assert.equal(state.chunks[0].status, 'failed');
|
|
727
|
+
});
|
|
728
|
+
|
|
729
|
+
test('run-bug-hunter handles worker paths containing spaces', () => {
|
|
730
|
+
const sandbox = makeSandbox('run-bug-hunter-space-path-');
|
|
731
|
+
const runner = resolveSkillScript('run-bug-hunter.cjs');
|
|
732
|
+
const skillDir = path.resolve(__dirname, '..', '..');
|
|
733
|
+
const filesJsonPath = path.join(sandbox, 'files.json');
|
|
734
|
+
const statePath = path.join(sandbox, '.claude', 'bug-hunter-state.json');
|
|
735
|
+
const workerPath = path.join(sandbox, 'worker script.cjs');
|
|
736
|
+
const sourceFile = path.join(sandbox, 'src', 'dir with space', 'a.ts');
|
|
737
|
+
|
|
738
|
+
fs.mkdirSync(path.dirname(sourceFile), { recursive: true });
|
|
739
|
+
fs.writeFileSync(sourceFile, 'export const a = 1;\n', 'utf8');
|
|
740
|
+
writeJson(filesJsonPath, [sourceFile]);
|
|
741
|
+
|
|
742
|
+
fs.writeFileSync(workerPath, [
|
|
743
|
+
'#!/usr/bin/env node',
|
|
744
|
+
"const fs = require('fs');",
|
|
745
|
+
"const args = process.argv;",
|
|
746
|
+
"const findingsPath = args[args.indexOf('--findings-json') + 1];",
|
|
747
|
+
"const scanFilesJson = args[args.indexOf('--scan-files-json') + 1];",
|
|
748
|
+
"const scanFiles = JSON.parse(fs.readFileSync(scanFilesJson, 'utf8'));",
|
|
749
|
+
"fs.writeFileSync(findingsPath, JSON.stringify([{ bugId: 'BUG-space', severity: 'Low', category: 'logic', file: scanFiles[0], lines: '1', claim: 'space path works', evidence: scanFiles[0] + ':1 evidence', runtimeTrigger: 'Call a()', crossReferences: ['Single file'], confidenceScore: 60 }], null, 2));"
|
|
750
|
+
].join('\n'), 'utf8');
|
|
751
|
+
|
|
752
|
+
const workerTemplate = [
|
|
753
|
+
'node',
|
|
754
|
+
workerPath,
|
|
755
|
+
'--chunk-id',
|
|
756
|
+
'{chunkId}',
|
|
757
|
+
'--scan-files-json',
|
|
758
|
+
'{scanFilesJson}',
|
|
759
|
+
'--findings-json',
|
|
760
|
+
'{findingsJson}'
|
|
761
|
+
].join(' ');
|
|
762
|
+
|
|
763
|
+
const result = runJson('node', [
|
|
764
|
+
runner,
|
|
765
|
+
'run',
|
|
766
|
+
'--skill-dir',
|
|
767
|
+
skillDir,
|
|
768
|
+
'--files-json',
|
|
769
|
+
filesJsonPath,
|
|
770
|
+
'--state',
|
|
771
|
+
statePath,
|
|
772
|
+
'--chunk-size',
|
|
773
|
+
'1',
|
|
774
|
+
'--worker-cmd',
|
|
775
|
+
workerTemplate,
|
|
776
|
+
'--timeout-ms',
|
|
777
|
+
'5000'
|
|
778
|
+
], {
|
|
779
|
+
cwd: sandbox
|
|
780
|
+
});
|
|
781
|
+
|
|
782
|
+
assert.equal(result.ok, true);
|
|
783
|
+
});
|
|
784
|
+
|
|
785
|
+
test('run-bug-hunter skips fix strategy and fix plan emission when chunks fail', () => {
|
|
786
|
+
const sandbox = makeSandbox('run-bug-hunter-failed-chunks-');
|
|
787
|
+
const runner = resolveSkillScript('run-bug-hunter.cjs');
|
|
788
|
+
const skillDir = path.resolve(__dirname, '..', '..');
|
|
789
|
+
const filesJsonPath = path.join(sandbox, 'files.json');
|
|
790
|
+
const statePath = path.join(sandbox, '.claude', 'bug-hunter-state.json');
|
|
791
|
+
const fixPlanPath = path.join(sandbox, '.claude', 'bug-hunter-fix-plan.json');
|
|
792
|
+
const strategyPath = path.join(sandbox, '.claude', 'bug-hunter-fix-strategy.json');
|
|
793
|
+
const workerPath = path.join(sandbox, 'always-fail-worker.cjs');
|
|
794
|
+
const sourceFile = path.join(sandbox, 'src', 'a.ts');
|
|
795
|
+
|
|
796
|
+
fs.mkdirSync(path.dirname(sourceFile), { recursive: true });
|
|
797
|
+
fs.writeFileSync(sourceFile, 'export const a = 1;\n', 'utf8');
|
|
798
|
+
writeJson(filesJsonPath, [sourceFile]);
|
|
799
|
+
|
|
800
|
+
fs.writeFileSync(workerPath, '#!/usr/bin/env node\nprocess.exit(1);\n', 'utf8');
|
|
801
|
+
|
|
802
|
+
const result = runJson('node', [
|
|
803
|
+
runner,
|
|
804
|
+
'run',
|
|
805
|
+
'--skill-dir',
|
|
806
|
+
skillDir,
|
|
807
|
+
'--files-json',
|
|
808
|
+
filesJsonPath,
|
|
809
|
+
'--state',
|
|
810
|
+
statePath,
|
|
811
|
+
'--chunk-size',
|
|
812
|
+
'1',
|
|
813
|
+
'--worker-cmd',
|
|
814
|
+
`node ${workerPath}`,
|
|
815
|
+
'--timeout-ms',
|
|
816
|
+
'5000',
|
|
817
|
+
'--max-retries',
|
|
818
|
+
'1',
|
|
819
|
+
'--fix-plan-path',
|
|
820
|
+
fixPlanPath,
|
|
821
|
+
'--strategy-path',
|
|
822
|
+
strategyPath
|
|
823
|
+
], {
|
|
824
|
+
cwd: sandbox
|
|
825
|
+
});
|
|
826
|
+
|
|
827
|
+
assert.equal(result.ok, true);
|
|
828
|
+
const state = readJson(statePath);
|
|
829
|
+
assert.equal(state.chunks[0].status, 'failed');
|
|
830
|
+
assert.equal(fs.existsSync(fixPlanPath), false);
|
|
831
|
+
assert.equal(fs.existsSync(strategyPath), false);
|
|
832
|
+
});
|
|
833
|
+
|
|
834
|
+
test('run-bug-hunter fails fast on unknown placeholders in worker templates', () => {
|
|
835
|
+
const sandbox = makeSandbox('run-bug-hunter-bad-template-');
|
|
836
|
+
const runner = resolveSkillScript('run-bug-hunter.cjs');
|
|
837
|
+
const skillDir = path.resolve(__dirname, '..', '..');
|
|
838
|
+
const outputPath = path.join(sandbox, '.bug-hunter', 'skeptic.json');
|
|
839
|
+
|
|
840
|
+
const result = runRaw('node', [
|
|
841
|
+
runner,
|
|
842
|
+
'phase',
|
|
843
|
+
'--skill-dir',
|
|
844
|
+
skillDir,
|
|
845
|
+
'--phase-name',
|
|
846
|
+
'skeptic-phase',
|
|
847
|
+
'--artifact',
|
|
848
|
+
'skeptic',
|
|
849
|
+
'--output-path',
|
|
850
|
+
outputPath,
|
|
851
|
+
'--worker-cmd',
|
|
852
|
+
'node fake-worker --output-path {outputPath} --missing {unknownPlaceholder}',
|
|
853
|
+
'--timeout-ms',
|
|
854
|
+
'5000'
|
|
855
|
+
], {
|
|
856
|
+
cwd: sandbox,
|
|
857
|
+
encoding: 'utf8'
|
|
858
|
+
});
|
|
859
|
+
|
|
860
|
+
assert.notEqual(result.status, 0);
|
|
861
|
+
assert.match(`${result.stdout || ''}${result.stderr || ''}`, /Unknown template placeholder|unknownPlaceholder/);
|
|
862
|
+
});
|
|
863
|
+
|
|
864
|
+
test('run-bug-hunter phase retries invalid skeptic output and renders a markdown companion', () => {
|
|
865
|
+
const sandbox = makeSandbox('run-bug-hunter-phase-skeptic-');
|
|
866
|
+
const runner = resolveSkillScript('run-bug-hunter.cjs');
|
|
867
|
+
const skillDir = path.resolve(__dirname, '..', '..');
|
|
868
|
+
const outputPath = path.join(sandbox, '.bug-hunter', 'skeptic.json');
|
|
869
|
+
const renderOutputPath = path.join(sandbox, '.bug-hunter', 'skeptic.md');
|
|
870
|
+
const journalPath = path.join(sandbox, '.bug-hunter', 'phase.log');
|
|
871
|
+
const attemptsFile = path.join(sandbox, 'attempts.json');
|
|
872
|
+
const workerPath = path.join(sandbox, 'skeptic-worker.cjs');
|
|
873
|
+
|
|
874
|
+
fs.writeFileSync(workerPath, [
|
|
875
|
+
'#!/usr/bin/env node',
|
|
876
|
+
"const fs = require('fs');",
|
|
877
|
+
"const path = require('path');",
|
|
878
|
+
"const args = process.argv;",
|
|
879
|
+
"const outputPath = args[args.indexOf('--output-path') + 1];",
|
|
880
|
+
"const attemptsPath = args[args.indexOf('--attempts-file') + 1];",
|
|
881
|
+
'let attempts = {};',
|
|
882
|
+
"if (fs.existsSync(attemptsPath)) attempts = JSON.parse(fs.readFileSync(attemptsPath, 'utf8'));",
|
|
883
|
+
"attempts.skeptic = (attempts.skeptic || 0) + 1;",
|
|
884
|
+
"fs.mkdirSync(path.dirname(attemptsPath), { recursive: true });",
|
|
885
|
+
"fs.writeFileSync(attemptsPath, JSON.stringify(attempts, null, 2));",
|
|
886
|
+
"const payload = attempts.skeptic === 1",
|
|
887
|
+
" ? [{ bugId: 'BUG-1', response: 'ACCEPT' }]",
|
|
888
|
+
" : [{ bugId: 'BUG-1', response: 'ACCEPT', analysisSummary: 'Validated on retry.' }];",
|
|
889
|
+
"fs.mkdirSync(path.dirname(outputPath), { recursive: true });",
|
|
890
|
+
"fs.writeFileSync(outputPath, JSON.stringify(payload, null, 2));"
|
|
891
|
+
].join('\n'), 'utf8');
|
|
892
|
+
|
|
893
|
+
const workerTemplate = [
|
|
894
|
+
'node',
|
|
895
|
+
workerPath,
|
|
896
|
+
'--output-path',
|
|
897
|
+
'{outputPath}',
|
|
898
|
+
'--attempts-file',
|
|
899
|
+
attemptsFile
|
|
900
|
+
].join(' ');
|
|
901
|
+
|
|
902
|
+
const renderTemplate = [
|
|
903
|
+
'node',
|
|
904
|
+
path.join(skillDir, 'scripts', 'render-report.cjs'),
|
|
905
|
+
'skeptic',
|
|
906
|
+
'{outputPath}',
|
|
907
|
+
'>',
|
|
908
|
+
'{renderOutputPath}'
|
|
909
|
+
].join(' ');
|
|
910
|
+
|
|
911
|
+
const result = runJson('node', [
|
|
912
|
+
runner,
|
|
913
|
+
'phase',
|
|
914
|
+
'--skill-dir',
|
|
915
|
+
skillDir,
|
|
916
|
+
'--phase-name',
|
|
917
|
+
'skeptic-phase',
|
|
918
|
+
'--artifact',
|
|
919
|
+
'skeptic',
|
|
920
|
+
'--output-path',
|
|
921
|
+
outputPath,
|
|
922
|
+
'--render-output-path',
|
|
923
|
+
renderOutputPath,
|
|
924
|
+
'--worker-cmd',
|
|
925
|
+
workerTemplate,
|
|
926
|
+
'--render-cmd',
|
|
927
|
+
renderTemplate,
|
|
928
|
+
'--timeout-ms',
|
|
929
|
+
'5000',
|
|
930
|
+
'--max-retries',
|
|
931
|
+
'1',
|
|
932
|
+
'--backoff-ms',
|
|
933
|
+
'10',
|
|
934
|
+
'--journal-path',
|
|
935
|
+
journalPath
|
|
936
|
+
], {
|
|
937
|
+
cwd: sandbox
|
|
938
|
+
});
|
|
939
|
+
|
|
940
|
+
assert.equal(result.ok, true);
|
|
941
|
+
assert.equal(result.artifact, 'skeptic');
|
|
942
|
+
assert.equal(fs.existsSync(outputPath), true);
|
|
943
|
+
assert.equal(fs.existsSync(renderOutputPath), true);
|
|
944
|
+
|
|
945
|
+
const attempts = readJson(attemptsFile);
|
|
946
|
+
assert.equal(attempts.skeptic, 2);
|
|
947
|
+
|
|
948
|
+
const journal = fs.readFileSync(journalPath, 'utf8');
|
|
949
|
+
assert.match(journal, /attempt-post-check-failed/);
|
|
950
|
+
assert.match(journal, /\$\[0\]\.analysisSummary is required/);
|
|
951
|
+
|
|
952
|
+
const rendered = fs.readFileSync(renderOutputPath, 'utf8');
|
|
953
|
+
assert.match(rendered, /# Skeptic Review/);
|
|
954
|
+
assert.match(rendered, /Validated on retry/);
|
|
955
|
+
});
|
|
956
|
+
|
|
957
|
+
test('run-bug-hunter phase validates referee and fix-report artifacts', () => {
|
|
958
|
+
const sandbox = makeSandbox('run-bug-hunter-phase-multi-');
|
|
959
|
+
const runner = resolveSkillScript('run-bug-hunter.cjs');
|
|
960
|
+
const skillDir = path.resolve(__dirname, '..', '..');
|
|
961
|
+
|
|
962
|
+
const phases = [
|
|
963
|
+
{
|
|
964
|
+
artifact: 'referee',
|
|
965
|
+
invalidBody: "[{\"bugId\":\"BUG-1\",\"verdict\":\"REAL_BUG\"}]",
|
|
966
|
+
validBody: JSON.stringify([
|
|
967
|
+
{
|
|
968
|
+
bugId: 'BUG-1',
|
|
969
|
+
verdict: 'REAL_BUG',
|
|
970
|
+
trueSeverity: 'Critical',
|
|
971
|
+
confidenceScore: 99,
|
|
972
|
+
confidenceLabel: 'high',
|
|
973
|
+
verificationMode: 'INDEPENDENTLY_VERIFIED',
|
|
974
|
+
analysisSummary: 'Confirmed on retry.'
|
|
975
|
+
}
|
|
976
|
+
], null, 2),
|
|
977
|
+
expectedError: '\\$\\[0\\]\\.trueSeverity is required'
|
|
978
|
+
},
|
|
979
|
+
{
|
|
980
|
+
artifact: 'fix-report',
|
|
981
|
+
invalidBody: JSON.stringify({
|
|
982
|
+
version: '3.0.4',
|
|
983
|
+
fix_branch: 'bug-hunter-fix-branch'
|
|
984
|
+
}, null, 2),
|
|
985
|
+
validBody: JSON.stringify({
|
|
986
|
+
version: '3.0.4',
|
|
987
|
+
fix_branch: 'bug-hunter-fix-branch',
|
|
988
|
+
base_commit: 'abc123',
|
|
989
|
+
dry_run: false,
|
|
990
|
+
circuit_breaker_tripped: false,
|
|
991
|
+
phase2_timeout_hit: false,
|
|
992
|
+
fixes: [],
|
|
993
|
+
verification: {
|
|
994
|
+
baseline_pass: 1,
|
|
995
|
+
baseline_fail: 0,
|
|
996
|
+
flaky_tests: 0,
|
|
997
|
+
final_pass: 1,
|
|
998
|
+
final_fail: 0,
|
|
999
|
+
new_failures: 0,
|
|
1000
|
+
resolved_failures: 0,
|
|
1001
|
+
typecheck_pass: true,
|
|
1002
|
+
build_pass: true,
|
|
1003
|
+
fixer_bugs_found: 0
|
|
1004
|
+
},
|
|
1005
|
+
summary: {
|
|
1006
|
+
total_confirmed: 0,
|
|
1007
|
+
eligible: 0,
|
|
1008
|
+
manual_review: 0,
|
|
1009
|
+
fixed: 0,
|
|
1010
|
+
fix_reverted: 0,
|
|
1011
|
+
fix_failed: 0,
|
|
1012
|
+
skipped: 0,
|
|
1013
|
+
fixer_bug: 0,
|
|
1014
|
+
partial: 0
|
|
1015
|
+
}
|
|
1016
|
+
}, null, 2),
|
|
1017
|
+
expectedError: '\\$\\.base_commit is required'
|
|
1018
|
+
}
|
|
1019
|
+
];
|
|
1020
|
+
|
|
1021
|
+
phases.forEach((phase) => {
|
|
1022
|
+
const outputPath = path.join(sandbox, '.bug-hunter', `${phase.artifact}.json`);
|
|
1023
|
+
const journalPath = path.join(sandbox, '.bug-hunter', `${phase.artifact}.log`);
|
|
1024
|
+
const attemptsFile = path.join(sandbox, `${phase.artifact}-attempts.json`);
|
|
1025
|
+
const workerPath = path.join(sandbox, `${phase.artifact}-worker.cjs`);
|
|
1026
|
+
|
|
1027
|
+
fs.writeFileSync(workerPath, [
|
|
1028
|
+
'#!/usr/bin/env node',
|
|
1029
|
+
"const fs = require('fs');",
|
|
1030
|
+
"const path = require('path');",
|
|
1031
|
+
"const args = process.argv;",
|
|
1032
|
+
"const outputPath = args[args.indexOf('--output-path') + 1];",
|
|
1033
|
+
"const attemptsPath = args[args.indexOf('--attempts-file') + 1];",
|
|
1034
|
+
'let attempts = 0;',
|
|
1035
|
+
"if (fs.existsSync(attemptsPath)) attempts = Number(fs.readFileSync(attemptsPath, 'utf8'));",
|
|
1036
|
+
'attempts += 1;',
|
|
1037
|
+
"fs.mkdirSync(path.dirname(attemptsPath), { recursive: true });",
|
|
1038
|
+
"fs.writeFileSync(attemptsPath, String(attempts));",
|
|
1039
|
+
`const invalidBody = ${JSON.stringify(phase.invalidBody)};`,
|
|
1040
|
+
`const validBody = ${JSON.stringify(phase.validBody)};`,
|
|
1041
|
+
"fs.mkdirSync(path.dirname(outputPath), { recursive: true });",
|
|
1042
|
+
"fs.writeFileSync(outputPath, attempts === 1 ? invalidBody : validBody);"
|
|
1043
|
+
].join('\n'), 'utf8');
|
|
1044
|
+
|
|
1045
|
+
const workerTemplate = [
|
|
1046
|
+
'node',
|
|
1047
|
+
workerPath,
|
|
1048
|
+
'--output-path',
|
|
1049
|
+
'{outputPath}',
|
|
1050
|
+
'--attempts-file',
|
|
1051
|
+
attemptsFile
|
|
1052
|
+
].join(' ');
|
|
1053
|
+
|
|
1054
|
+
const result = runJson('node', [
|
|
1055
|
+
runner,
|
|
1056
|
+
'phase',
|
|
1057
|
+
'--skill-dir',
|
|
1058
|
+
skillDir,
|
|
1059
|
+
'--phase-name',
|
|
1060
|
+
`${phase.artifact}-phase`,
|
|
1061
|
+
'--artifact',
|
|
1062
|
+
phase.artifact,
|
|
1063
|
+
'--output-path',
|
|
1064
|
+
outputPath,
|
|
1065
|
+
'--worker-cmd',
|
|
1066
|
+
workerTemplate,
|
|
1067
|
+
'--timeout-ms',
|
|
1068
|
+
'5000',
|
|
1069
|
+
'--max-retries',
|
|
1070
|
+
'1',
|
|
1071
|
+
'--backoff-ms',
|
|
1072
|
+
'10',
|
|
1073
|
+
'--journal-path',
|
|
1074
|
+
journalPath
|
|
1075
|
+
], {
|
|
1076
|
+
cwd: sandbox
|
|
1077
|
+
});
|
|
1078
|
+
|
|
1079
|
+
assert.equal(result.ok, true);
|
|
1080
|
+
assert.equal(result.artifact, phase.artifact);
|
|
1081
|
+
assert.equal(fs.existsSync(outputPath), true);
|
|
1082
|
+
|
|
1083
|
+
const journal = fs.readFileSync(journalPath, 'utf8');
|
|
1084
|
+
assert.match(journal, /attempt-post-check-failed/);
|
|
1085
|
+
assert.match(journal, new RegExp(phase.expectedError));
|
|
1086
|
+
});
|
|
1087
|
+
});
|