@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
|
@@ -13,6 +13,7 @@ const {
|
|
|
13
13
|
test('payload-guard accepts valid hunter payload and rejects malformed payload', () => {
|
|
14
14
|
const sandbox = makeSandbox('payload-guard-');
|
|
15
15
|
const guardScript = resolveSkillScript('payload-guard.cjs');
|
|
16
|
+
const schemaRuntime = require(resolveSkillScript('schema-runtime.cjs'));
|
|
16
17
|
const validPayloadPath = path.join(sandbox, 'valid.json');
|
|
17
18
|
const invalidPayloadPath = path.join(sandbox, 'invalid.json');
|
|
18
19
|
|
|
@@ -21,7 +22,7 @@ test('payload-guard accepts valid hunter payload and rejects malformed payload',
|
|
|
21
22
|
targetFiles: ['src/a.ts'],
|
|
22
23
|
riskMap: {},
|
|
23
24
|
techStack: { framework: 'express' },
|
|
24
|
-
outputSchema:
|
|
25
|
+
outputSchema: schemaRuntime.createSchemaRef('findings')
|
|
25
26
|
});
|
|
26
27
|
|
|
27
28
|
const valid = runJson('node', [guardScript, 'validate', 'hunter', validPayloadPath]);
|
|
@@ -31,11 +32,162 @@ test('payload-guard accepts valid hunter payload and rejects malformed payload',
|
|
|
31
32
|
writeJson(invalidPayloadPath, {
|
|
32
33
|
skillDir: 'relative/path',
|
|
33
34
|
targetFiles: [],
|
|
34
|
-
outputSchema:
|
|
35
|
+
outputSchema: { artifact: 'findings', schemaVersion: 999, schemaFile: 'schemas/findings.schema.json' }
|
|
35
36
|
});
|
|
36
37
|
|
|
37
38
|
const invalid = runRaw('node', [guardScript, 'validate', 'hunter', invalidPayloadPath]);
|
|
38
39
|
assert.notEqual(invalid.status, 0);
|
|
39
40
|
const output = `${invalid.stdout || ''}\n${invalid.stderr || ''}`;
|
|
40
41
|
assert.match(output, /Missing required field: riskMap/);
|
|
42
|
+
assert.match(output, /schema version 1/);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
test('schema-validate validates example findings fixtures', () => {
|
|
46
|
+
const validatorScript = resolveSkillScript('schema-validate.cjs');
|
|
47
|
+
const validPath = resolveSkillScript('..', 'schemas', 'examples', 'findings.valid.json');
|
|
48
|
+
const invalidPath = resolveSkillScript('..', 'schemas', 'examples', 'findings.invalid.json');
|
|
49
|
+
|
|
50
|
+
const valid = runJson('node', [validatorScript, 'findings', validPath]);
|
|
51
|
+
assert.equal(valid.ok, true);
|
|
52
|
+
|
|
53
|
+
const invalid = runRaw('node', [validatorScript, 'findings', invalidPath]);
|
|
54
|
+
assert.notEqual(invalid.status, 0);
|
|
55
|
+
assert.match(`${invalid.stdout}${invalid.stderr}`, /\$\[0\]\.claim is required/);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
test('schema-validate accepts valid skeptic, referee, fix-report, fix-strategy, and fix-plan artifacts', () => {
|
|
59
|
+
const sandbox = makeSandbox('schema-validate-more-');
|
|
60
|
+
const validatorScript = resolveSkillScript('schema-validate.cjs');
|
|
61
|
+
const skepticPath = path.join(sandbox, 'skeptic.json');
|
|
62
|
+
const refereePath = path.join(sandbox, 'referee.json');
|
|
63
|
+
const fixReportPath = path.join(sandbox, 'fix-report.json');
|
|
64
|
+
const fixStrategyPath = path.join(sandbox, 'fix-strategy.json');
|
|
65
|
+
const fixPlanPath = path.join(sandbox, 'fix-plan.json');
|
|
66
|
+
|
|
67
|
+
writeJson(skepticPath, [
|
|
68
|
+
{
|
|
69
|
+
bugId: 'BUG-1',
|
|
70
|
+
response: 'ACCEPT',
|
|
71
|
+
analysisSummary: 'The finding holds after re-reading the code.'
|
|
72
|
+
}
|
|
73
|
+
]);
|
|
74
|
+
writeJson(refereePath, [
|
|
75
|
+
{
|
|
76
|
+
bugId: 'BUG-1',
|
|
77
|
+
verdict: 'REAL_BUG',
|
|
78
|
+
trueSeverity: 'Critical',
|
|
79
|
+
confidenceScore: 95,
|
|
80
|
+
confidenceLabel: 'high',
|
|
81
|
+
verificationMode: 'INDEPENDENTLY_VERIFIED',
|
|
82
|
+
analysisSummary: 'Confirmed by direct code trace.'
|
|
83
|
+
}
|
|
84
|
+
]);
|
|
85
|
+
writeJson(fixReportPath, {
|
|
86
|
+
version: '3.0.0',
|
|
87
|
+
fix_branch: 'bug-hunter-fix-20260311-200000',
|
|
88
|
+
base_commit: 'abc123',
|
|
89
|
+
dry_run: false,
|
|
90
|
+
circuit_breaker_tripped: false,
|
|
91
|
+
phase2_timeout_hit: false,
|
|
92
|
+
fixes: [
|
|
93
|
+
{
|
|
94
|
+
bugId: 'BUG-1',
|
|
95
|
+
severity: 'CRITICAL',
|
|
96
|
+
status: 'FIXED',
|
|
97
|
+
files: ['src/a.ts'],
|
|
98
|
+
lines: '10-12',
|
|
99
|
+
commit: 'def456',
|
|
100
|
+
description: 'Parameterized the query.'
|
|
101
|
+
}
|
|
102
|
+
],
|
|
103
|
+
verification: {
|
|
104
|
+
baseline_pass: 10,
|
|
105
|
+
baseline_fail: 1,
|
|
106
|
+
flaky_tests: 0,
|
|
107
|
+
final_pass: 11,
|
|
108
|
+
final_fail: 0,
|
|
109
|
+
new_failures: 0,
|
|
110
|
+
resolved_failures: 1,
|
|
111
|
+
typecheck_pass: true,
|
|
112
|
+
build_pass: true,
|
|
113
|
+
fixer_bugs_found: 0
|
|
114
|
+
},
|
|
115
|
+
summary: {
|
|
116
|
+
total_confirmed: 1,
|
|
117
|
+
eligible: 1,
|
|
118
|
+
manual_review: 0,
|
|
119
|
+
fixed: 1,
|
|
120
|
+
fix_reverted: 0,
|
|
121
|
+
fix_failed: 0,
|
|
122
|
+
skipped: 0,
|
|
123
|
+
fixer_bug: 0,
|
|
124
|
+
partial: 0
|
|
125
|
+
}
|
|
126
|
+
});
|
|
127
|
+
writeJson(fixStrategyPath, {
|
|
128
|
+
version: '3.1.0',
|
|
129
|
+
generatedAt: '2026-03-12T00:00:00.000Z',
|
|
130
|
+
confidenceThreshold: 75,
|
|
131
|
+
summary: {
|
|
132
|
+
confirmed: 1,
|
|
133
|
+
safeAutofix: 1,
|
|
134
|
+
manualReview: 0,
|
|
135
|
+
largerRefactor: 0,
|
|
136
|
+
architecturalRemediation: 0,
|
|
137
|
+
canaryCandidates: 1,
|
|
138
|
+
rolloutCandidates: 0
|
|
139
|
+
},
|
|
140
|
+
clusters: [
|
|
141
|
+
{
|
|
142
|
+
clusterId: 'cluster-1',
|
|
143
|
+
strategy: 'safe-autofix',
|
|
144
|
+
executionStage: 'canary',
|
|
145
|
+
autofixEligible: true,
|
|
146
|
+
bugIds: ['BUG-1'],
|
|
147
|
+
files: ['src/a.ts'],
|
|
148
|
+
maxSeverity: 'CRITICAL',
|
|
149
|
+
summary: '1 bug(s) in src classified as safe-autofix.',
|
|
150
|
+
recommendedAction: 'Proceed through the guarded fix pipeline with canary verification and rollback safety.',
|
|
151
|
+
reasons: ['Finding is localized enough for a guarded surgical fix.']
|
|
152
|
+
}
|
|
153
|
+
]
|
|
154
|
+
});
|
|
155
|
+
writeJson(fixPlanPath, {
|
|
156
|
+
generatedAt: '2026-03-12T00:00:00.000Z',
|
|
157
|
+
confidenceThreshold: 75,
|
|
158
|
+
canarySize: 1,
|
|
159
|
+
totals: {
|
|
160
|
+
findings: 1,
|
|
161
|
+
eligible: 1,
|
|
162
|
+
canary: 1,
|
|
163
|
+
rollout: 0,
|
|
164
|
+
manualReview: 0
|
|
165
|
+
},
|
|
166
|
+
canary: [
|
|
167
|
+
{
|
|
168
|
+
bugId: 'BUG-1',
|
|
169
|
+
severity: 'Critical',
|
|
170
|
+
category: 'logic',
|
|
171
|
+
file: 'src/a.ts',
|
|
172
|
+
lines: '10-12',
|
|
173
|
+
claim: 'x',
|
|
174
|
+
evidence: 'src/a.ts:10-12 evidence',
|
|
175
|
+
runtimeTrigger: 'Call x()',
|
|
176
|
+
crossReferences: ['Single file'],
|
|
177
|
+
confidenceScore: 95,
|
|
178
|
+
strategy: 'safe-autofix',
|
|
179
|
+
executionStage: 'canary',
|
|
180
|
+
autofixEligible: true,
|
|
181
|
+
reason: 'Finding is localized enough for a guarded surgical fix.'
|
|
182
|
+
}
|
|
183
|
+
],
|
|
184
|
+
rollout: [],
|
|
185
|
+
manualReview: []
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
assert.equal(runJson('node', [validatorScript, 'skeptic', skepticPath]).ok, true);
|
|
189
|
+
assert.equal(runJson('node', [validatorScript, 'referee', refereePath]).ok, true);
|
|
190
|
+
assert.equal(runJson('node', [validatorScript, 'fix-report', fixReportPath]).ok, true);
|
|
191
|
+
assert.equal(runJson('node', [validatorScript, 'fix-strategy', fixStrategyPath]).ok, true);
|
|
192
|
+
assert.equal(runJson('node', [validatorScript, 'fix-plan', fixPlanPath]).ok, true);
|
|
41
193
|
});
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
const assert = require('node:assert/strict');
|
|
2
|
+
const fs = require('fs');
|
|
3
|
+
const path = require('path');
|
|
4
|
+
const test = require('node:test');
|
|
5
|
+
|
|
6
|
+
const {
|
|
7
|
+
makeSandbox,
|
|
8
|
+
resolveSkillScript,
|
|
9
|
+
runJson,
|
|
10
|
+
runRaw
|
|
11
|
+
} = require('./test-utils.cjs');
|
|
12
|
+
|
|
13
|
+
function writeExecutable(filePath, content) {
|
|
14
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
15
|
+
fs.writeFileSync(filePath, content, 'utf8');
|
|
16
|
+
fs.chmodSync(filePath, 0o755);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
test('pr-scope resolves the current PR via gh metadata', () => {
|
|
20
|
+
const sandbox = makeSandbox('pr-scope-current-');
|
|
21
|
+
const script = resolveSkillScript('pr-scope.cjs');
|
|
22
|
+
const ghPath = path.join(sandbox, 'gh-mock.cjs');
|
|
23
|
+
|
|
24
|
+
writeExecutable(ghPath, `#!/usr/bin/env node
|
|
25
|
+
const args = process.argv.slice(2);
|
|
26
|
+
if (args[0] === 'pr' && args[1] === 'view') {
|
|
27
|
+
process.stdout.write(JSON.stringify({ number: 42, title: 'Fix auth flow', headRefName: 'feature/auth', baseRefName: 'main', url: 'https://example.test/pr/42' }));
|
|
28
|
+
process.exit(0);
|
|
29
|
+
}
|
|
30
|
+
if (args[0] === 'pr' && args[1] === 'diff') {
|
|
31
|
+
process.stdout.write('src/auth.ts\\nsrc/session.ts\\n');
|
|
32
|
+
process.exit(0);
|
|
33
|
+
}
|
|
34
|
+
process.exit(1);
|
|
35
|
+
`);
|
|
36
|
+
|
|
37
|
+
const result = runJson('node', [script, 'resolve', 'current', '--repo-root', sandbox, '--gh-bin', ghPath]);
|
|
38
|
+
assert.equal(result.ok, true);
|
|
39
|
+
assert.equal(result.source, 'gh');
|
|
40
|
+
assert.equal(result.pr.number, 42);
|
|
41
|
+
assert.deepEqual(result.changedFiles, ['src/auth.ts', 'src/session.ts']);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
test('pr-scope falls back to git when current PR metadata is unavailable', () => {
|
|
45
|
+
const sandbox = makeSandbox('pr-scope-fallback-');
|
|
46
|
+
const script = resolveSkillScript('pr-scope.cjs');
|
|
47
|
+
const ghPath = path.join(sandbox, 'gh-fail.cjs');
|
|
48
|
+
const gitPath = path.join(sandbox, 'git-mock.cjs');
|
|
49
|
+
|
|
50
|
+
writeExecutable(ghPath, `#!/usr/bin/env node
|
|
51
|
+
process.stderr.write('gh unavailable');
|
|
52
|
+
process.exit(1);
|
|
53
|
+
`);
|
|
54
|
+
|
|
55
|
+
writeExecutable(gitPath, `#!/usr/bin/env node
|
|
56
|
+
const args = process.argv.slice(2);
|
|
57
|
+
if (args[0] === 'rev-parse') {
|
|
58
|
+
process.stdout.write('feature/local\\n');
|
|
59
|
+
process.exit(0);
|
|
60
|
+
}
|
|
61
|
+
if (args[0] === 'diff') {
|
|
62
|
+
process.stdout.write('src/local.ts\\n');
|
|
63
|
+
process.exit(0);
|
|
64
|
+
}
|
|
65
|
+
process.exit(1);
|
|
66
|
+
`);
|
|
67
|
+
|
|
68
|
+
const result = runJson('node', [
|
|
69
|
+
script,
|
|
70
|
+
'resolve',
|
|
71
|
+
'current',
|
|
72
|
+
'--repo-root',
|
|
73
|
+
sandbox,
|
|
74
|
+
'--gh-bin',
|
|
75
|
+
ghPath,
|
|
76
|
+
'--git-bin',
|
|
77
|
+
gitPath,
|
|
78
|
+
'--base',
|
|
79
|
+
'develop'
|
|
80
|
+
]);
|
|
81
|
+
assert.equal(result.ok, true);
|
|
82
|
+
assert.equal(result.source, 'git');
|
|
83
|
+
assert.equal(result.pr.headRefName, 'feature/local');
|
|
84
|
+
assert.equal(result.pr.baseRefName, 'develop');
|
|
85
|
+
assert.deepEqual(result.changedFiles, ['src/local.ts']);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
test('pr-scope resolves the most recent PR via gh list + diff', () => {
|
|
89
|
+
const sandbox = makeSandbox('pr-scope-recent-');
|
|
90
|
+
const script = resolveSkillScript('pr-scope.cjs');
|
|
91
|
+
const ghPath = path.join(sandbox, 'gh-recent.cjs');
|
|
92
|
+
|
|
93
|
+
writeExecutable(ghPath, `#!/usr/bin/env node
|
|
94
|
+
const args = process.argv.slice(2);
|
|
95
|
+
if (args[0] === 'pr' && args[1] === 'list') {
|
|
96
|
+
process.stdout.write(JSON.stringify([{ number: 7, title: 'Recent PR', headRefName: 'feature/recent', baseRefName: 'main', url: 'https://example.test/pr/7' }]));
|
|
97
|
+
process.exit(0);
|
|
98
|
+
}
|
|
99
|
+
if (args[0] === 'pr' && args[1] === 'diff') {
|
|
100
|
+
process.stdout.write('src/recent.ts\\n');
|
|
101
|
+
process.exit(0);
|
|
102
|
+
}
|
|
103
|
+
process.exit(1);
|
|
104
|
+
`);
|
|
105
|
+
|
|
106
|
+
const result = runJson('node', [script, 'resolve', 'recent', '--repo-root', sandbox, '--gh-bin', ghPath]);
|
|
107
|
+
assert.equal(result.ok, true);
|
|
108
|
+
assert.equal(result.source, 'gh');
|
|
109
|
+
assert.equal(result.pr.number, 7);
|
|
110
|
+
assert.deepEqual(result.changedFiles, ['src/recent.ts']);
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
test('pr-scope uses the discovered default branch for current-branch fallback', () => {
|
|
114
|
+
const sandbox = makeSandbox('pr-scope-default-branch-');
|
|
115
|
+
const script = resolveSkillScript('pr-scope.cjs');
|
|
116
|
+
const ghPath = path.join(sandbox, 'gh-fail.cjs');
|
|
117
|
+
const gitPath = path.join(sandbox, 'git-default.cjs');
|
|
118
|
+
|
|
119
|
+
writeExecutable(ghPath, `#!/usr/bin/env node
|
|
120
|
+
process.stderr.write('gh unavailable');
|
|
121
|
+
process.exit(1);
|
|
122
|
+
`);
|
|
123
|
+
|
|
124
|
+
writeExecutable(gitPath, `#!/usr/bin/env node
|
|
125
|
+
const args = process.argv.slice(2);
|
|
126
|
+
if (args[0] === 'rev-parse' && args[1] === '--abbrev-ref') {
|
|
127
|
+
process.stdout.write('feature/local\\n');
|
|
128
|
+
process.exit(0);
|
|
129
|
+
}
|
|
130
|
+
if (args[0] === 'symbolic-ref') {
|
|
131
|
+
process.stdout.write('refs/remotes/origin/trunk\\n');
|
|
132
|
+
process.exit(0);
|
|
133
|
+
}
|
|
134
|
+
if (args[0] === 'diff' && args[2] === 'origin/trunk...feature/local') {
|
|
135
|
+
process.stdout.write('src/from-trunk.ts\\n');
|
|
136
|
+
process.exit(0);
|
|
137
|
+
}
|
|
138
|
+
process.stderr.write('unexpected command: ' + args.join(' '));
|
|
139
|
+
process.exit(1);
|
|
140
|
+
`);
|
|
141
|
+
|
|
142
|
+
const result = runJson('node', [
|
|
143
|
+
script,
|
|
144
|
+
'resolve',
|
|
145
|
+
'current',
|
|
146
|
+
'--repo-root',
|
|
147
|
+
sandbox,
|
|
148
|
+
'--gh-bin',
|
|
149
|
+
ghPath,
|
|
150
|
+
'--git-bin',
|
|
151
|
+
gitPath
|
|
152
|
+
]);
|
|
153
|
+
assert.equal(result.ok, true);
|
|
154
|
+
assert.equal(result.source, 'git');
|
|
155
|
+
assert.equal(result.pr.baseRefName, 'trunk');
|
|
156
|
+
assert.deepEqual(result.changedFiles, ['src/from-trunk.ts']);
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
test('pr-scope fails current-branch fallback when no trustworthy base branch is available', () => {
|
|
160
|
+
const sandbox = makeSandbox('pr-scope-no-base-');
|
|
161
|
+
const script = resolveSkillScript('pr-scope.cjs');
|
|
162
|
+
const ghPath = path.join(sandbox, 'gh-fail.cjs');
|
|
163
|
+
const gitPath = path.join(sandbox, 'git-partial.cjs');
|
|
164
|
+
|
|
165
|
+
writeExecutable(ghPath, `#!/usr/bin/env node
|
|
166
|
+
process.stderr.write('gh unavailable');
|
|
167
|
+
process.exit(1);
|
|
168
|
+
`);
|
|
169
|
+
|
|
170
|
+
writeExecutable(gitPath, `#!/usr/bin/env node
|
|
171
|
+
const args = process.argv.slice(2);
|
|
172
|
+
if (args[0] === 'rev-parse' && args[1] === '--abbrev-ref') {
|
|
173
|
+
process.stdout.write('feature/local\\n');
|
|
174
|
+
process.exit(0);
|
|
175
|
+
}
|
|
176
|
+
process.stderr.write('missing default branch');
|
|
177
|
+
process.exit(1);
|
|
178
|
+
`);
|
|
179
|
+
|
|
180
|
+
const result = runRaw('node', [
|
|
181
|
+
script,
|
|
182
|
+
'resolve',
|
|
183
|
+
'current',
|
|
184
|
+
'--repo-root',
|
|
185
|
+
sandbox,
|
|
186
|
+
'--gh-bin',
|
|
187
|
+
ghPath,
|
|
188
|
+
'--git-bin',
|
|
189
|
+
gitPath
|
|
190
|
+
], {
|
|
191
|
+
encoding: 'utf8'
|
|
192
|
+
});
|
|
193
|
+
assert.notEqual(result.status, 0);
|
|
194
|
+
assert.match(`${result.stdout || ''}${result.stderr || ''}`, /base branch|default branch|missing default branch/i);
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
test('pr-scope fails for numbered PRs when gh metadata cannot be resolved', () => {
|
|
198
|
+
const sandbox = makeSandbox('pr-scope-numbered-');
|
|
199
|
+
const script = resolveSkillScript('pr-scope.cjs');
|
|
200
|
+
const ghPath = path.join(sandbox, 'gh-fail.cjs');
|
|
201
|
+
|
|
202
|
+
writeExecutable(ghPath, `#!/usr/bin/env node
|
|
203
|
+
process.stderr.write('not found');
|
|
204
|
+
process.exit(1);
|
|
205
|
+
`);
|
|
206
|
+
|
|
207
|
+
const result = runRaw('node', [script, 'resolve', '123', '--repo-root', sandbox, '--gh-bin', ghPath], {
|
|
208
|
+
encoding: 'utf8'
|
|
209
|
+
});
|
|
210
|
+
assert.notEqual(result.status, 0);
|
|
211
|
+
assert.match(`${result.stdout || ''}${result.stderr || ''}`, /not found/);
|
|
212
|
+
});
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
const assert = require('node:assert/strict');
|
|
2
|
+
const fs = require('fs');
|
|
3
|
+
const path = require('path');
|
|
4
|
+
const test = require('node:test');
|
|
5
|
+
|
|
6
|
+
const {
|
|
7
|
+
makeSandbox,
|
|
8
|
+
resolveSkillScript,
|
|
9
|
+
runRaw,
|
|
10
|
+
writeJson
|
|
11
|
+
} = require('./test-utils.cjs');
|
|
12
|
+
|
|
13
|
+
test('render-report renders a markdown summary from findings and referee JSON', () => {
|
|
14
|
+
const sandbox = makeSandbox('render-report-');
|
|
15
|
+
const script = resolveSkillScript('render-report.cjs');
|
|
16
|
+
const findingsPath = path.join(sandbox, 'findings.json');
|
|
17
|
+
const refereePath = path.join(sandbox, 'referee.json');
|
|
18
|
+
|
|
19
|
+
writeJson(findingsPath, [
|
|
20
|
+
{
|
|
21
|
+
bugId: 'BUG-1',
|
|
22
|
+
severity: 'Critical',
|
|
23
|
+
category: 'security',
|
|
24
|
+
file: 'src/api.ts',
|
|
25
|
+
lines: '10-12',
|
|
26
|
+
claim: 'User input reaches an unsafe sink',
|
|
27
|
+
evidence: 'src/api.ts:10-12 ...',
|
|
28
|
+
runtimeTrigger: 'POST /api with attacker input',
|
|
29
|
+
crossReferences: ['Single file'],
|
|
30
|
+
confidenceScore: 90
|
|
31
|
+
}
|
|
32
|
+
]);
|
|
33
|
+
|
|
34
|
+
writeJson(refereePath, [
|
|
35
|
+
{
|
|
36
|
+
bugId: 'BUG-1',
|
|
37
|
+
verdict: 'REAL_BUG',
|
|
38
|
+
trueSeverity: 'Critical',
|
|
39
|
+
confidenceScore: 91,
|
|
40
|
+
confidenceLabel: 'high',
|
|
41
|
+
verificationMode: 'INDEPENDENTLY_VERIFIED',
|
|
42
|
+
analysisSummary: 'Confirmed by tracing the sink.'
|
|
43
|
+
}
|
|
44
|
+
]);
|
|
45
|
+
|
|
46
|
+
const result = runRaw('node', [script, 'report', findingsPath, refereePath], {
|
|
47
|
+
encoding: 'utf8'
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
assert.equal(result.status, 0);
|
|
51
|
+
assert.match(result.stdout, /# Bug Hunter Report/);
|
|
52
|
+
assert.match(result.stdout, /BUG-1 \| Critical \| src\/api.ts/);
|
|
53
|
+
assert.match(result.stdout, /Confirmed by tracing the sink/);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
test('render-report renders coverage markdown from coverage JSON', () => {
|
|
57
|
+
const sandbox = makeSandbox('render-coverage-');
|
|
58
|
+
const script = resolveSkillScript('render-report.cjs');
|
|
59
|
+
const coveragePath = path.join(sandbox, 'coverage.json');
|
|
60
|
+
|
|
61
|
+
writeJson(coveragePath, {
|
|
62
|
+
schemaVersion: 1,
|
|
63
|
+
iteration: 2,
|
|
64
|
+
status: 'COMPLETE',
|
|
65
|
+
files: [{ path: 'src/a.ts', status: 'done' }],
|
|
66
|
+
bugs: [{ bugId: 'BUG-1', severity: 'Low', file: 'src/a.ts', claim: 'example' }],
|
|
67
|
+
fixes: [{ bugId: 'BUG-1', status: 'MANUAL_REVIEW' }]
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
const result = runRaw('node', [script, 'coverage', coveragePath], {
|
|
71
|
+
encoding: 'utf8'
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
assert.equal(result.status, 0);
|
|
75
|
+
assert.match(result.stdout, /# Bug Hunter Coverage/);
|
|
76
|
+
assert.match(result.stdout, /done \| src\/a.ts/);
|
|
77
|
+
assert.match(result.stdout, /BUG-1 \| Low \| src\/a.ts \| example/);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
test('render-report renders a markdown summary from fix-report JSON', () => {
|
|
81
|
+
const sandbox = makeSandbox('render-fix-report-');
|
|
82
|
+
const script = resolveSkillScript('render-report.cjs');
|
|
83
|
+
const fixReportPath = path.join(sandbox, 'fix-report.json');
|
|
84
|
+
|
|
85
|
+
writeJson(fixReportPath, {
|
|
86
|
+
version: '3.0.0',
|
|
87
|
+
fix_branch: 'bug-hunter-fix-20260311-200000',
|
|
88
|
+
base_commit: 'abc123',
|
|
89
|
+
dry_run: false,
|
|
90
|
+
circuit_breaker_tripped: false,
|
|
91
|
+
phase2_timeout_hit: false,
|
|
92
|
+
fixes: [
|
|
93
|
+
{
|
|
94
|
+
bugId: 'BUG-1',
|
|
95
|
+
severity: 'CRITICAL',
|
|
96
|
+
status: 'FIXED',
|
|
97
|
+
files: ['src/a.ts'],
|
|
98
|
+
lines: '10-12',
|
|
99
|
+
commit: 'def456',
|
|
100
|
+
description: 'Parameterized the query.'
|
|
101
|
+
}
|
|
102
|
+
],
|
|
103
|
+
verification: {
|
|
104
|
+
baseline_pass: 10,
|
|
105
|
+
baseline_fail: 1,
|
|
106
|
+
flaky_tests: 0,
|
|
107
|
+
final_pass: 11,
|
|
108
|
+
final_fail: 0,
|
|
109
|
+
new_failures: 0,
|
|
110
|
+
resolved_failures: 1,
|
|
111
|
+
typecheck_pass: true,
|
|
112
|
+
build_pass: true,
|
|
113
|
+
fixer_bugs_found: 0
|
|
114
|
+
},
|
|
115
|
+
summary: {
|
|
116
|
+
total_confirmed: 1,
|
|
117
|
+
eligible: 1,
|
|
118
|
+
manual_review: 0,
|
|
119
|
+
fixed: 1,
|
|
120
|
+
fix_reverted: 0,
|
|
121
|
+
fix_failed: 0,
|
|
122
|
+
skipped: 0,
|
|
123
|
+
fixer_bug: 0,
|
|
124
|
+
partial: 0
|
|
125
|
+
}
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
const result = runRaw('node', [script, 'fix-report', fixReportPath], {
|
|
129
|
+
encoding: 'utf8'
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
assert.equal(result.status, 0);
|
|
133
|
+
assert.match(result.stdout, /# Fix Report/);
|
|
134
|
+
assert.match(result.stdout, /BUG-1 \| FIXED \| CRITICAL/);
|
|
135
|
+
assert.match(result.stdout, /Parameterized the query/);
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
test('render-report renders a markdown summary from fix-strategy JSON', () => {
|
|
139
|
+
const sandbox = makeSandbox('render-fix-strategy-');
|
|
140
|
+
const script = resolveSkillScript('render-report.cjs');
|
|
141
|
+
const fixStrategyPath = path.join(sandbox, 'fix-strategy.json');
|
|
142
|
+
|
|
143
|
+
writeJson(fixStrategyPath, {
|
|
144
|
+
version: '3.1.0',
|
|
145
|
+
generatedAt: '2026-03-12T00:00:00.000Z',
|
|
146
|
+
confidenceThreshold: 75,
|
|
147
|
+
summary: {
|
|
148
|
+
confirmed: 2,
|
|
149
|
+
safeAutofix: 1,
|
|
150
|
+
manualReview: 1,
|
|
151
|
+
largerRefactor: 0,
|
|
152
|
+
architecturalRemediation: 0,
|
|
153
|
+
canaryCandidates: 1,
|
|
154
|
+
rolloutCandidates: 0
|
|
155
|
+
},
|
|
156
|
+
clusters: [
|
|
157
|
+
{
|
|
158
|
+
clusterId: 'cluster-1',
|
|
159
|
+
strategy: 'safe-autofix',
|
|
160
|
+
executionStage: 'canary',
|
|
161
|
+
autofixEligible: true,
|
|
162
|
+
bugIds: ['BUG-1'],
|
|
163
|
+
files: ['src/a.ts'],
|
|
164
|
+
maxSeverity: 'CRITICAL',
|
|
165
|
+
summary: '1 bug(s) in src classified as safe-autofix.',
|
|
166
|
+
recommendedAction: 'Proceed through the guarded fix pipeline with canary verification and rollback safety.',
|
|
167
|
+
reasons: ['Finding is localized enough for a guarded surgical fix.']
|
|
168
|
+
}
|
|
169
|
+
]
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
const result = runRaw('node', [script, 'fix-strategy', fixStrategyPath], {
|
|
173
|
+
encoding: 'utf8'
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
assert.equal(result.status, 0);
|
|
177
|
+
assert.match(result.stdout, /# Fix Strategy/);
|
|
178
|
+
assert.match(result.stdout, /cluster-1 \| safe-autofix \| canary/);
|
|
179
|
+
assert.match(result.stdout, /guarded fix pipeline/);
|
|
180
|
+
});
|