@mainahq/core 0.2.0
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/README.md +31 -0
- package/package.json +37 -0
- package/src/ai/__tests__/ai.test.ts +207 -0
- package/src/ai/__tests__/design-approaches.test.ts +192 -0
- package/src/ai/__tests__/spec-questions.test.ts +191 -0
- package/src/ai/__tests__/tiers.test.ts +110 -0
- package/src/ai/commit-msg.ts +28 -0
- package/src/ai/design-approaches.ts +76 -0
- package/src/ai/index.ts +205 -0
- package/src/ai/pr-summary.ts +60 -0
- package/src/ai/spec-questions.ts +74 -0
- package/src/ai/tiers.ts +52 -0
- package/src/ai/try-generate.ts +89 -0
- package/src/ai/validate.ts +66 -0
- package/src/benchmark/__tests__/reporter.test.ts +525 -0
- package/src/benchmark/__tests__/runner.test.ts +113 -0
- package/src/benchmark/__tests__/story-loader.test.ts +152 -0
- package/src/benchmark/reporter.ts +332 -0
- package/src/benchmark/runner.ts +91 -0
- package/src/benchmark/story-loader.ts +88 -0
- package/src/benchmark/types.ts +95 -0
- package/src/cache/__tests__/keys.test.ts +97 -0
- package/src/cache/__tests__/manager.test.ts +312 -0
- package/src/cache/__tests__/ttl.test.ts +94 -0
- package/src/cache/keys.ts +44 -0
- package/src/cache/manager.ts +231 -0
- package/src/cache/ttl.ts +77 -0
- package/src/config/__tests__/config.test.ts +376 -0
- package/src/config/index.ts +198 -0
- package/src/context/__tests__/budget.test.ts +179 -0
- package/src/context/__tests__/engine.test.ts +163 -0
- package/src/context/__tests__/episodic.test.ts +291 -0
- package/src/context/__tests__/relevance.test.ts +323 -0
- package/src/context/__tests__/retrieval.test.ts +143 -0
- package/src/context/__tests__/selector.test.ts +174 -0
- package/src/context/__tests__/semantic.test.ts +252 -0
- package/src/context/__tests__/treesitter.test.ts +229 -0
- package/src/context/__tests__/working.test.ts +236 -0
- package/src/context/budget.ts +130 -0
- package/src/context/engine.ts +394 -0
- package/src/context/episodic.ts +251 -0
- package/src/context/relevance.ts +325 -0
- package/src/context/retrieval.ts +325 -0
- package/src/context/selector.ts +93 -0
- package/src/context/semantic.ts +331 -0
- package/src/context/treesitter.ts +216 -0
- package/src/context/working.ts +192 -0
- package/src/db/__tests__/db.test.ts +151 -0
- package/src/db/index.ts +211 -0
- package/src/db/schema.ts +84 -0
- package/src/design/__tests__/design.test.ts +310 -0
- package/src/design/__tests__/generate-hld-lld.test.ts +109 -0
- package/src/design/__tests__/review.test.ts +561 -0
- package/src/design/index.ts +297 -0
- package/src/design/review.ts +327 -0
- package/src/explain/__tests__/explain.test.ts +173 -0
- package/src/explain/index.ts +181 -0
- package/src/features/__tests__/analyzer.test.ts +358 -0
- package/src/features/__tests__/checklist.test.ts +454 -0
- package/src/features/__tests__/numbering.test.ts +319 -0
- package/src/features/__tests__/quality.test.ts +295 -0
- package/src/features/__tests__/traceability.test.ts +147 -0
- package/src/features/analyzer.ts +445 -0
- package/src/features/checklist.ts +366 -0
- package/src/features/index.ts +18 -0
- package/src/features/numbering.ts +404 -0
- package/src/features/quality.ts +349 -0
- package/src/features/test-stubs.ts +157 -0
- package/src/features/traceability.ts +260 -0
- package/src/feedback/__tests__/async-feedback.test.ts +52 -0
- package/src/feedback/__tests__/collector.test.ts +219 -0
- package/src/feedback/__tests__/compress.test.ts +150 -0
- package/src/feedback/__tests__/preferences.test.ts +169 -0
- package/src/feedback/collector.ts +135 -0
- package/src/feedback/compress.ts +92 -0
- package/src/feedback/preferences.ts +108 -0
- package/src/git/__tests__/git.test.ts +62 -0
- package/src/git/index.ts +110 -0
- package/src/hooks/__tests__/runner.test.ts +266 -0
- package/src/hooks/index.ts +8 -0
- package/src/hooks/runner.ts +130 -0
- package/src/index.ts +356 -0
- package/src/init/__tests__/init.test.ts +228 -0
- package/src/init/index.ts +364 -0
- package/src/language/__tests__/detect.test.ts +77 -0
- package/src/language/__tests__/profile.test.ts +51 -0
- package/src/language/detect.ts +70 -0
- package/src/language/profile.ts +110 -0
- package/src/prompts/__tests__/defaults.test.ts +52 -0
- package/src/prompts/__tests__/engine.test.ts +183 -0
- package/src/prompts/__tests__/evolution-resolve.test.ts +169 -0
- package/src/prompts/__tests__/evolution.test.ts +187 -0
- package/src/prompts/__tests__/loader.test.ts +105 -0
- package/src/prompts/candidates/review-v2.md +55 -0
- package/src/prompts/defaults/ai-review.md +49 -0
- package/src/prompts/defaults/commit.md +30 -0
- package/src/prompts/defaults/context.md +26 -0
- package/src/prompts/defaults/design-approaches.md +57 -0
- package/src/prompts/defaults/design-hld-lld.md +55 -0
- package/src/prompts/defaults/design.md +53 -0
- package/src/prompts/defaults/explain.md +31 -0
- package/src/prompts/defaults/fix.md +32 -0
- package/src/prompts/defaults/index.ts +38 -0
- package/src/prompts/defaults/review.md +41 -0
- package/src/prompts/defaults/spec-questions.md +59 -0
- package/src/prompts/defaults/tests.md +72 -0
- package/src/prompts/engine.ts +137 -0
- package/src/prompts/evolution.ts +409 -0
- package/src/prompts/loader.ts +71 -0
- package/src/review/__tests__/review.test.ts +288 -0
- package/src/review/comprehensive.ts +362 -0
- package/src/review/index.ts +417 -0
- package/src/stats/__tests__/tracker.test.ts +323 -0
- package/src/stats/index.ts +11 -0
- package/src/stats/tracker.ts +492 -0
- package/src/ticket/__tests__/ticket.test.ts +273 -0
- package/src/ticket/index.ts +185 -0
- package/src/utils.ts +87 -0
- package/src/verify/__tests__/ai-review.test.ts +242 -0
- package/src/verify/__tests__/coverage.test.ts +83 -0
- package/src/verify/__tests__/detect.test.ts +175 -0
- package/src/verify/__tests__/diff-filter.test.ts +338 -0
- package/src/verify/__tests__/fix.test.ts +478 -0
- package/src/verify/__tests__/linters/clippy.test.ts +45 -0
- package/src/verify/__tests__/linters/go-vet.test.ts +27 -0
- package/src/verify/__tests__/linters/ruff.test.ts +64 -0
- package/src/verify/__tests__/mutation.test.ts +141 -0
- package/src/verify/__tests__/pipeline.test.ts +553 -0
- package/src/verify/__tests__/proof.test.ts +97 -0
- package/src/verify/__tests__/secretlint.test.ts +190 -0
- package/src/verify/__tests__/semgrep.test.ts +217 -0
- package/src/verify/__tests__/slop.test.ts +366 -0
- package/src/verify/__tests__/sonar.test.ts +113 -0
- package/src/verify/__tests__/syntax-guard.test.ts +227 -0
- package/src/verify/__tests__/trivy.test.ts +191 -0
- package/src/verify/__tests__/visual.test.ts +139 -0
- package/src/verify/ai-review.ts +276 -0
- package/src/verify/coverage.ts +134 -0
- package/src/verify/detect.ts +171 -0
- package/src/verify/diff-filter.ts +183 -0
- package/src/verify/fix.ts +317 -0
- package/src/verify/linters/clippy.ts +52 -0
- package/src/verify/linters/go-vet.ts +32 -0
- package/src/verify/linters/ruff.ts +47 -0
- package/src/verify/mutation.ts +143 -0
- package/src/verify/pipeline.ts +328 -0
- package/src/verify/proof.ts +277 -0
- package/src/verify/secretlint.ts +168 -0
- package/src/verify/semgrep.ts +170 -0
- package/src/verify/slop.ts +493 -0
- package/src/verify/sonar.ts +146 -0
- package/src/verify/syntax-guard.ts +251 -0
- package/src/verify/trivy.ts +161 -0
- package/src/verify/visual.ts +460 -0
- package/src/workflow/__tests__/context.test.ts +110 -0
- package/src/workflow/context.ts +81 -0
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import { describe, expect, it } from "bun:test";
|
|
2
|
+
import { parseStrykerReport, runMutation } from "../mutation";
|
|
3
|
+
|
|
4
|
+
describe("Stryker Mutation Testing", () => {
|
|
5
|
+
describe("parseStrykerReport", () => {
|
|
6
|
+
it("should parse survived mutants into findings", () => {
|
|
7
|
+
const json = JSON.stringify({
|
|
8
|
+
files: {
|
|
9
|
+
"src/app.ts": {
|
|
10
|
+
mutants: [
|
|
11
|
+
{
|
|
12
|
+
id: "1",
|
|
13
|
+
mutatorName: "ConditionalExpression",
|
|
14
|
+
replacement: "false",
|
|
15
|
+
status: "Survived",
|
|
16
|
+
location: {
|
|
17
|
+
start: { line: 10, column: 5 },
|
|
18
|
+
end: { line: 10, column: 20 },
|
|
19
|
+
},
|
|
20
|
+
description: "Replaced x > 0 with false",
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
id: "2",
|
|
24
|
+
mutatorName: "ArithmeticOperator",
|
|
25
|
+
replacement: "-",
|
|
26
|
+
status: "Killed",
|
|
27
|
+
location: {
|
|
28
|
+
start: { line: 15, column: 3 },
|
|
29
|
+
end: { line: 15, column: 10 },
|
|
30
|
+
},
|
|
31
|
+
description: "Replaced + with -",
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
id: "3",
|
|
35
|
+
mutatorName: "StringLiteral",
|
|
36
|
+
replacement: '""',
|
|
37
|
+
status: "Survived",
|
|
38
|
+
location: {
|
|
39
|
+
start: { line: 20, column: 1 },
|
|
40
|
+
end: { line: 20, column: 15 },
|
|
41
|
+
},
|
|
42
|
+
description: 'Replaced "hello" with ""',
|
|
43
|
+
},
|
|
44
|
+
],
|
|
45
|
+
},
|
|
46
|
+
},
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
const findings = parseStrykerReport(json);
|
|
50
|
+
|
|
51
|
+
// Only survived mutants should become findings
|
|
52
|
+
expect(findings).toHaveLength(2);
|
|
53
|
+
expect(findings[0]?.tool).toBe("stryker");
|
|
54
|
+
expect(findings[0]?.file).toBe("src/app.ts");
|
|
55
|
+
expect(findings[0]?.line).toBe(10);
|
|
56
|
+
expect(findings[0]?.severity).toBe("warning");
|
|
57
|
+
expect(findings[0]?.ruleId).toBe("stryker/ConditionalExpression");
|
|
58
|
+
expect(findings[0]?.message).toContain("Survived");
|
|
59
|
+
expect(findings[1]?.line).toBe(20);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it("should handle empty files object", () => {
|
|
63
|
+
const json = JSON.stringify({ files: {} });
|
|
64
|
+
expect(parseStrykerReport(json)).toHaveLength(0);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it("should handle malformed JSON", () => {
|
|
68
|
+
expect(parseStrykerReport("not json")).toHaveLength(0);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it("should handle missing location gracefully", () => {
|
|
72
|
+
const json = JSON.stringify({
|
|
73
|
+
files: {
|
|
74
|
+
"src/app.ts": {
|
|
75
|
+
mutants: [
|
|
76
|
+
{
|
|
77
|
+
id: "1",
|
|
78
|
+
mutatorName: "Test",
|
|
79
|
+
status: "Survived",
|
|
80
|
+
description: "some mutation",
|
|
81
|
+
},
|
|
82
|
+
],
|
|
83
|
+
},
|
|
84
|
+
},
|
|
85
|
+
});
|
|
86
|
+
const findings = parseStrykerReport(json);
|
|
87
|
+
expect(findings).toHaveLength(1);
|
|
88
|
+
expect(findings[0]?.line).toBe(0);
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it("should ignore killed, no coverage, and timeout mutants", () => {
|
|
92
|
+
const json = JSON.stringify({
|
|
93
|
+
files: {
|
|
94
|
+
"src/app.ts": {
|
|
95
|
+
mutants: [
|
|
96
|
+
{
|
|
97
|
+
id: "1",
|
|
98
|
+
mutatorName: "A",
|
|
99
|
+
status: "Killed",
|
|
100
|
+
location: { start: { line: 1 } },
|
|
101
|
+
description: "x",
|
|
102
|
+
},
|
|
103
|
+
{
|
|
104
|
+
id: "2",
|
|
105
|
+
mutatorName: "B",
|
|
106
|
+
status: "NoCoverage",
|
|
107
|
+
location: { start: { line: 2 } },
|
|
108
|
+
description: "x",
|
|
109
|
+
},
|
|
110
|
+
{
|
|
111
|
+
id: "3",
|
|
112
|
+
mutatorName: "C",
|
|
113
|
+
status: "Timeout",
|
|
114
|
+
location: { start: { line: 3 } },
|
|
115
|
+
description: "x",
|
|
116
|
+
},
|
|
117
|
+
{
|
|
118
|
+
id: "4",
|
|
119
|
+
mutatorName: "D",
|
|
120
|
+
status: "Survived",
|
|
121
|
+
location: { start: { line: 4 } },
|
|
122
|
+
description: "x",
|
|
123
|
+
},
|
|
124
|
+
],
|
|
125
|
+
},
|
|
126
|
+
},
|
|
127
|
+
});
|
|
128
|
+
const findings = parseStrykerReport(json);
|
|
129
|
+
expect(findings).toHaveLength(1);
|
|
130
|
+
expect(findings[0]?.line).toBe(4);
|
|
131
|
+
});
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
describe("runMutation", () => {
|
|
135
|
+
it("should skip when stryker is not available", async () => {
|
|
136
|
+
const result = await runMutation({ available: false });
|
|
137
|
+
expect(result.skipped).toBe(true);
|
|
138
|
+
expect(result.findings).toHaveLength(0);
|
|
139
|
+
});
|
|
140
|
+
});
|
|
141
|
+
});
|
|
@@ -0,0 +1,553 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for the Verify Pipeline Orchestrator.
|
|
3
|
+
*
|
|
4
|
+
* Mocks all individual tool modules to test orchestration logic:
|
|
5
|
+
* ordering (syntax first), parallel execution, diff filtering, pass/fail.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { afterAll, beforeEach, describe, expect, it, mock } from "bun:test";
|
|
9
|
+
|
|
10
|
+
import type { DetectedTool } from "../detect";
|
|
11
|
+
import type { DiffFilterResult, Finding } from "../diff-filter";
|
|
12
|
+
import type { SecretlintResult } from "../secretlint";
|
|
13
|
+
import type { SemgrepResult } from "../semgrep";
|
|
14
|
+
import type { SlopResult } from "../slop";
|
|
15
|
+
import type { SyntaxDiagnostic, SyntaxGuardResult } from "../syntax-guard";
|
|
16
|
+
import type { TrivyResult } from "../trivy";
|
|
17
|
+
|
|
18
|
+
// ─── Mock State ────────────────────────────────────────────────────────────
|
|
19
|
+
|
|
20
|
+
// We use manual mock functions tracked via closures.
|
|
21
|
+
// Each test configures the behavior by setting these.
|
|
22
|
+
|
|
23
|
+
let mockSyntaxGuardResult: SyntaxGuardResult = { ok: true, value: undefined };
|
|
24
|
+
let mockDetectedTools: DetectedTool[] = [];
|
|
25
|
+
let mockSlopResult: SlopResult = { findings: [], cached: false };
|
|
26
|
+
let mockSemgrepResult: SemgrepResult = { findings: [], skipped: false };
|
|
27
|
+
let mockTrivyResult: TrivyResult = { findings: [], skipped: false };
|
|
28
|
+
let mockSecretlintResult: SecretlintResult = { findings: [], skipped: false };
|
|
29
|
+
let mockDiffFilterResult: DiffFilterResult = { shown: [], hidden: 0 };
|
|
30
|
+
let mockStagedFiles: string[] = ["src/app.ts"];
|
|
31
|
+
|
|
32
|
+
// Track call order for verifying pipeline sequencing
|
|
33
|
+
let callOrder: string[] = [];
|
|
34
|
+
|
|
35
|
+
// Mock the modules
|
|
36
|
+
mock.module("../syntax-guard", () => ({
|
|
37
|
+
syntaxGuard: async (..._args: unknown[]) => {
|
|
38
|
+
callOrder.push("syntaxGuard");
|
|
39
|
+
return mockSyntaxGuardResult;
|
|
40
|
+
},
|
|
41
|
+
}));
|
|
42
|
+
|
|
43
|
+
mock.module("../detect", () => ({
|
|
44
|
+
detectTools: async () => {
|
|
45
|
+
callOrder.push("detectTools");
|
|
46
|
+
return mockDetectedTools;
|
|
47
|
+
},
|
|
48
|
+
}));
|
|
49
|
+
|
|
50
|
+
mock.module("../slop", () => ({
|
|
51
|
+
detectSlop: async (..._args: unknown[]) => {
|
|
52
|
+
callOrder.push("detectSlop");
|
|
53
|
+
return mockSlopResult;
|
|
54
|
+
},
|
|
55
|
+
}));
|
|
56
|
+
|
|
57
|
+
mock.module("../semgrep", () => ({
|
|
58
|
+
runSemgrep: async (..._args: unknown[]) => {
|
|
59
|
+
callOrder.push("runSemgrep");
|
|
60
|
+
return mockSemgrepResult;
|
|
61
|
+
},
|
|
62
|
+
}));
|
|
63
|
+
|
|
64
|
+
mock.module("../trivy", () => ({
|
|
65
|
+
runTrivy: async (..._args: unknown[]) => {
|
|
66
|
+
callOrder.push("runTrivy");
|
|
67
|
+
return mockTrivyResult;
|
|
68
|
+
},
|
|
69
|
+
}));
|
|
70
|
+
|
|
71
|
+
mock.module("../secretlint", () => ({
|
|
72
|
+
runSecretlint: async (..._args: unknown[]) => {
|
|
73
|
+
callOrder.push("runSecretlint");
|
|
74
|
+
return mockSecretlintResult;
|
|
75
|
+
},
|
|
76
|
+
}));
|
|
77
|
+
|
|
78
|
+
mock.module("../sonar", () => ({
|
|
79
|
+
runSonar: async (..._args: unknown[]) => {
|
|
80
|
+
callOrder.push("runSonar");
|
|
81
|
+
return { findings: [], skipped: true };
|
|
82
|
+
},
|
|
83
|
+
}));
|
|
84
|
+
|
|
85
|
+
mock.module("../mutation", () => ({
|
|
86
|
+
runMutation: async (..._args: unknown[]) => {
|
|
87
|
+
callOrder.push("runMutation");
|
|
88
|
+
return { findings: [], skipped: true };
|
|
89
|
+
},
|
|
90
|
+
}));
|
|
91
|
+
|
|
92
|
+
mock.module("../coverage", () => ({
|
|
93
|
+
runCoverage: async (..._args: unknown[]) => {
|
|
94
|
+
callOrder.push("runCoverage");
|
|
95
|
+
return { findings: [], skipped: true };
|
|
96
|
+
},
|
|
97
|
+
}));
|
|
98
|
+
|
|
99
|
+
mock.module("../diff-filter", () => ({
|
|
100
|
+
filterByDiff: async (findings: Finding[], ..._args: unknown[]) => {
|
|
101
|
+
callOrder.push("filterByDiff");
|
|
102
|
+
// If a custom result was set, use it; otherwise pass through all findings
|
|
103
|
+
if (
|
|
104
|
+
mockDiffFilterResult.shown.length > 0 ||
|
|
105
|
+
mockDiffFilterResult.hidden > 0
|
|
106
|
+
) {
|
|
107
|
+
return mockDiffFilterResult;
|
|
108
|
+
}
|
|
109
|
+
return { shown: findings, hidden: 0 };
|
|
110
|
+
},
|
|
111
|
+
}));
|
|
112
|
+
|
|
113
|
+
mock.module("../../git/index", () => ({
|
|
114
|
+
getStagedFiles: async (..._args: unknown[]) => {
|
|
115
|
+
callOrder.push("getStagedFiles");
|
|
116
|
+
return mockStagedFiles;
|
|
117
|
+
},
|
|
118
|
+
getDiff: async (..._args: unknown[]) => {
|
|
119
|
+
return "+ some changed code";
|
|
120
|
+
},
|
|
121
|
+
}));
|
|
122
|
+
|
|
123
|
+
// Mock AI review
|
|
124
|
+
let mockAIReviewResult: {
|
|
125
|
+
findings: Finding[];
|
|
126
|
+
skipped: boolean;
|
|
127
|
+
tier: string;
|
|
128
|
+
duration: number;
|
|
129
|
+
} = {
|
|
130
|
+
findings: [],
|
|
131
|
+
skipped: true,
|
|
132
|
+
tier: "mechanical",
|
|
133
|
+
duration: 0,
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
mock.module("../ai-review", () => ({
|
|
137
|
+
runAIReview: async (..._args: unknown[]) => {
|
|
138
|
+
callOrder.push("runAIReview");
|
|
139
|
+
return mockAIReviewResult;
|
|
140
|
+
},
|
|
141
|
+
}));
|
|
142
|
+
|
|
143
|
+
mock.module("../../language/detect", () => ({
|
|
144
|
+
detectLanguages: (..._args: unknown[]) => ["typescript"],
|
|
145
|
+
}));
|
|
146
|
+
|
|
147
|
+
mock.module("../../language/profile", () => ({
|
|
148
|
+
getProfile: (..._args: unknown[]) => ({
|
|
149
|
+
id: "typescript",
|
|
150
|
+
syntaxTool: "biome",
|
|
151
|
+
}),
|
|
152
|
+
}));
|
|
153
|
+
|
|
154
|
+
afterAll(() => {
|
|
155
|
+
mock.restore();
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
// Import AFTER mocking
|
|
159
|
+
import { runPipeline } from "../pipeline";
|
|
160
|
+
|
|
161
|
+
// ─── Helpers ───────────────────────────────────────────────────────────────
|
|
162
|
+
|
|
163
|
+
function makeFinding(overrides: Partial<Finding> = {}): Finding {
|
|
164
|
+
return {
|
|
165
|
+
tool: "test",
|
|
166
|
+
file: "src/app.ts",
|
|
167
|
+
line: 10,
|
|
168
|
+
message: "test finding",
|
|
169
|
+
severity: "warning",
|
|
170
|
+
...overrides,
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function makeDetectedTool(name: string, available: boolean): DetectedTool {
|
|
175
|
+
return {
|
|
176
|
+
name,
|
|
177
|
+
command: name,
|
|
178
|
+
version: available ? "1.0.0" : null,
|
|
179
|
+
available,
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// ─── Tests ─────────────────────────────────────────────────────────────────
|
|
184
|
+
|
|
185
|
+
describe("VerifyPipeline", () => {
|
|
186
|
+
beforeEach(() => {
|
|
187
|
+
// Reset all mock state
|
|
188
|
+
callOrder = [];
|
|
189
|
+
mockSyntaxGuardResult = { ok: true, value: undefined };
|
|
190
|
+
mockDetectedTools = [
|
|
191
|
+
makeDetectedTool("biome", true),
|
|
192
|
+
makeDetectedTool("semgrep", true),
|
|
193
|
+
makeDetectedTool("trivy", true),
|
|
194
|
+
makeDetectedTool("secretlint", true),
|
|
195
|
+
];
|
|
196
|
+
mockSlopResult = { findings: [], cached: false };
|
|
197
|
+
mockSemgrepResult = { findings: [], skipped: false };
|
|
198
|
+
mockTrivyResult = { findings: [], skipped: false };
|
|
199
|
+
mockSecretlintResult = { findings: [], skipped: false };
|
|
200
|
+
mockDiffFilterResult = { shown: [], hidden: 0 };
|
|
201
|
+
mockStagedFiles = ["src/app.ts"];
|
|
202
|
+
mockAIReviewResult = {
|
|
203
|
+
findings: [],
|
|
204
|
+
skipped: true,
|
|
205
|
+
tier: "mechanical",
|
|
206
|
+
duration: 0,
|
|
207
|
+
};
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
it("should auto-detect installed tools", async () => {
|
|
211
|
+
mockDetectedTools = [
|
|
212
|
+
makeDetectedTool("biome", true),
|
|
213
|
+
makeDetectedTool("semgrep", true),
|
|
214
|
+
makeDetectedTool("trivy", false),
|
|
215
|
+
makeDetectedTool("secretlint", true),
|
|
216
|
+
];
|
|
217
|
+
|
|
218
|
+
const result = await runPipeline({ files: ["src/app.ts"] });
|
|
219
|
+
|
|
220
|
+
expect(result.detectedTools).toHaveLength(4);
|
|
221
|
+
expect(
|
|
222
|
+
result.detectedTools.find((t) => t.name === "trivy")?.available,
|
|
223
|
+
).toBe(false);
|
|
224
|
+
expect(
|
|
225
|
+
result.detectedTools.find((t) => t.name === "semgrep")?.available,
|
|
226
|
+
).toBe(true);
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
it("should run all detected tools in parallel", async () => {
|
|
230
|
+
const slopFinding = makeFinding({ tool: "slop", message: "slop issue" });
|
|
231
|
+
const semgrepFinding = makeFinding({
|
|
232
|
+
tool: "semgrep",
|
|
233
|
+
message: "semgrep issue",
|
|
234
|
+
});
|
|
235
|
+
const trivyFinding = makeFinding({ tool: "trivy", message: "trivy issue" });
|
|
236
|
+
|
|
237
|
+
mockSlopResult = { findings: [slopFinding], cached: false };
|
|
238
|
+
mockSemgrepResult = { findings: [semgrepFinding], skipped: false };
|
|
239
|
+
mockTrivyResult = { findings: [trivyFinding], skipped: false };
|
|
240
|
+
mockSecretlintResult = { findings: [], skipped: false };
|
|
241
|
+
|
|
242
|
+
// diff filter passes everything through
|
|
243
|
+
mockDiffFilterResult = {
|
|
244
|
+
shown: [slopFinding, semgrepFinding, trivyFinding],
|
|
245
|
+
hidden: 0,
|
|
246
|
+
};
|
|
247
|
+
|
|
248
|
+
const result = await runPipeline({ files: ["src/app.ts"] });
|
|
249
|
+
|
|
250
|
+
// All tools should have run
|
|
251
|
+
expect(callOrder).toContain("detectSlop");
|
|
252
|
+
expect(callOrder).toContain("runSemgrep");
|
|
253
|
+
expect(callOrder).toContain("runTrivy");
|
|
254
|
+
expect(callOrder).toContain("runSecretlint");
|
|
255
|
+
|
|
256
|
+
// 5 tool reports (slop + semgrep + trivy + secretlint + ai-review)
|
|
257
|
+
expect(result.tools).toHaveLength(8);
|
|
258
|
+
expect(result.findings).toHaveLength(3);
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
it("should skip missing tools with info note", async () => {
|
|
262
|
+
mockDetectedTools = [
|
|
263
|
+
makeDetectedTool("biome", true),
|
|
264
|
+
makeDetectedTool("semgrep", false),
|
|
265
|
+
makeDetectedTool("trivy", false),
|
|
266
|
+
makeDetectedTool("secretlint", false),
|
|
267
|
+
];
|
|
268
|
+
|
|
269
|
+
mockSemgrepResult = { findings: [], skipped: true };
|
|
270
|
+
mockTrivyResult = { findings: [], skipped: true };
|
|
271
|
+
mockSecretlintResult = { findings: [], skipped: true };
|
|
272
|
+
|
|
273
|
+
const result = await runPipeline({ files: ["src/app.ts"] });
|
|
274
|
+
|
|
275
|
+
// Semgrep, trivy, secretlint should be marked as skipped
|
|
276
|
+
const semgrepReport = result.tools.find((t) => t.tool === "semgrep");
|
|
277
|
+
const trivyReport = result.tools.find((t) => t.tool === "trivy");
|
|
278
|
+
const secretlintReport = result.tools.find((t) => t.tool === "secretlint");
|
|
279
|
+
|
|
280
|
+
expect(semgrepReport?.skipped).toBe(true);
|
|
281
|
+
expect(trivyReport?.skipped).toBe(true);
|
|
282
|
+
expect(secretlintReport?.skipped).toBe(true);
|
|
283
|
+
|
|
284
|
+
// Slop always runs (doesn't depend on external tools)
|
|
285
|
+
const slopReport = result.tools.find((t) => t.tool === "slop");
|
|
286
|
+
expect(slopReport?.skipped).toBe(false);
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
it("should apply diff-only filtering by default", async () => {
|
|
290
|
+
const finding1 = makeFinding({
|
|
291
|
+
tool: "slop",
|
|
292
|
+
line: 5,
|
|
293
|
+
message: "on changed line",
|
|
294
|
+
});
|
|
295
|
+
const finding2 = makeFinding({
|
|
296
|
+
tool: "slop",
|
|
297
|
+
line: 50,
|
|
298
|
+
message: "on old line",
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
mockSlopResult = { findings: [finding1, finding2], cached: false };
|
|
302
|
+
mockDiffFilterResult = {
|
|
303
|
+
shown: [finding1],
|
|
304
|
+
hidden: 1,
|
|
305
|
+
};
|
|
306
|
+
|
|
307
|
+
const result = await runPipeline({ files: ["src/app.ts"] });
|
|
308
|
+
|
|
309
|
+
expect(callOrder).toContain("filterByDiff");
|
|
310
|
+
expect(result.findings).toHaveLength(1);
|
|
311
|
+
expect(result.hiddenCount).toBe(1);
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
it("should report pre-existing count as hidden", async () => {
|
|
315
|
+
const newFinding = makeFinding({ tool: "slop", message: "new issue" });
|
|
316
|
+
const oldFindings = Array.from({ length: 5 }, (_, i) =>
|
|
317
|
+
makeFinding({ tool: "slop", line: 100 + i, message: `old issue ${i}` }),
|
|
318
|
+
);
|
|
319
|
+
|
|
320
|
+
mockSlopResult = { findings: [newFinding, ...oldFindings], cached: false };
|
|
321
|
+
mockDiffFilterResult = {
|
|
322
|
+
shown: [newFinding],
|
|
323
|
+
hidden: 5,
|
|
324
|
+
};
|
|
325
|
+
|
|
326
|
+
const result = await runPipeline({ files: ["src/app.ts"] });
|
|
327
|
+
|
|
328
|
+
expect(result.hiddenCount).toBe(5);
|
|
329
|
+
expect(result.findings).toHaveLength(1);
|
|
330
|
+
});
|
|
331
|
+
|
|
332
|
+
it("should produce unified pass/fail", async () => {
|
|
333
|
+
// No error-severity findings -> pass
|
|
334
|
+
const warningFinding = makeFinding({ severity: "warning" });
|
|
335
|
+
mockSlopResult = { findings: [warningFinding], cached: false };
|
|
336
|
+
mockDiffFilterResult = { shown: [warningFinding], hidden: 0 };
|
|
337
|
+
|
|
338
|
+
const passResult = await runPipeline({ files: ["src/app.ts"] });
|
|
339
|
+
expect(passResult.passed).toBe(true);
|
|
340
|
+
|
|
341
|
+
// Reset for second assertion
|
|
342
|
+
callOrder = [];
|
|
343
|
+
|
|
344
|
+
// Error-severity finding -> fail
|
|
345
|
+
const errorFinding = makeFinding({ severity: "error" });
|
|
346
|
+
mockSlopResult = { findings: [errorFinding], cached: false };
|
|
347
|
+
mockDiffFilterResult = { shown: [errorFinding], hidden: 0 };
|
|
348
|
+
|
|
349
|
+
const failResult = await runPipeline({ files: ["src/app.ts"] });
|
|
350
|
+
expect(failResult.passed).toBe(false);
|
|
351
|
+
});
|
|
352
|
+
|
|
353
|
+
// ─── Additional orchestration tests ──────────────────────────────────────
|
|
354
|
+
|
|
355
|
+
it("should run syntax guard FIRST before any tools", async () => {
|
|
356
|
+
await runPipeline({ files: ["src/app.ts"] });
|
|
357
|
+
|
|
358
|
+
// syntaxGuard must be the first call
|
|
359
|
+
expect(callOrder[0]).toBe("syntaxGuard");
|
|
360
|
+
|
|
361
|
+
// detectTools should come after syntax guard
|
|
362
|
+
const syntaxIdx = callOrder.indexOf("syntaxGuard");
|
|
363
|
+
const detectIdx = callOrder.indexOf("detectTools");
|
|
364
|
+
expect(syntaxIdx).toBeLessThan(detectIdx);
|
|
365
|
+
});
|
|
366
|
+
|
|
367
|
+
it("should abort pipeline if syntax guard fails", async () => {
|
|
368
|
+
const syntaxErrors: SyntaxDiagnostic[] = [
|
|
369
|
+
{
|
|
370
|
+
file: "src/app.ts",
|
|
371
|
+
line: 1,
|
|
372
|
+
column: 1,
|
|
373
|
+
message: "Unexpected token",
|
|
374
|
+
severity: "error",
|
|
375
|
+
},
|
|
376
|
+
];
|
|
377
|
+
|
|
378
|
+
mockSyntaxGuardResult = { ok: false, error: syntaxErrors };
|
|
379
|
+
|
|
380
|
+
const result = await runPipeline({ files: ["src/app.ts"] });
|
|
381
|
+
|
|
382
|
+
expect(result.passed).toBe(false);
|
|
383
|
+
expect(result.syntaxPassed).toBe(false);
|
|
384
|
+
expect(result.syntaxErrors).toEqual(syntaxErrors);
|
|
385
|
+
|
|
386
|
+
// No other tools should have run
|
|
387
|
+
expect(callOrder).not.toContain("detectTools");
|
|
388
|
+
expect(callOrder).not.toContain("detectSlop");
|
|
389
|
+
expect(callOrder).not.toContain("runSemgrep");
|
|
390
|
+
expect(result.tools).toHaveLength(0);
|
|
391
|
+
});
|
|
392
|
+
|
|
393
|
+
it("should use staged files when no files provided", async () => {
|
|
394
|
+
mockStagedFiles = ["src/staged1.ts", "src/staged2.ts"];
|
|
395
|
+
|
|
396
|
+
const result = await runPipeline();
|
|
397
|
+
|
|
398
|
+
expect(callOrder).toContain("getStagedFiles");
|
|
399
|
+
expect(result.syntaxPassed).toBe(true);
|
|
400
|
+
});
|
|
401
|
+
|
|
402
|
+
it("should skip diff filter when diffOnly is false", async () => {
|
|
403
|
+
const finding = makeFinding({ tool: "slop" });
|
|
404
|
+
mockSlopResult = { findings: [finding], cached: false };
|
|
405
|
+
|
|
406
|
+
const result = await runPipeline({
|
|
407
|
+
files: ["src/app.ts"],
|
|
408
|
+
diffOnly: false,
|
|
409
|
+
});
|
|
410
|
+
|
|
411
|
+
expect(callOrder).not.toContain("filterByDiff");
|
|
412
|
+
expect(result.findings).toHaveLength(1);
|
|
413
|
+
expect(result.hiddenCount).toBe(0);
|
|
414
|
+
});
|
|
415
|
+
|
|
416
|
+
it("should include duration in result", async () => {
|
|
417
|
+
const result = await runPipeline({ files: ["src/app.ts"] });
|
|
418
|
+
|
|
419
|
+
expect(typeof result.duration).toBe("number");
|
|
420
|
+
expect(result.duration).toBeGreaterThanOrEqual(0);
|
|
421
|
+
});
|
|
422
|
+
|
|
423
|
+
it("should include per-tool durations", async () => {
|
|
424
|
+
const result = await runPipeline({ files: ["src/app.ts"] });
|
|
425
|
+
|
|
426
|
+
for (const toolReport of result.tools) {
|
|
427
|
+
expect(typeof toolReport.duration).toBe("number");
|
|
428
|
+
expect(toolReport.duration).toBeGreaterThanOrEqual(0);
|
|
429
|
+
}
|
|
430
|
+
});
|
|
431
|
+
|
|
432
|
+
it("should return empty result for empty file list", async () => {
|
|
433
|
+
mockStagedFiles = [];
|
|
434
|
+
|
|
435
|
+
const result = await runPipeline();
|
|
436
|
+
|
|
437
|
+
expect(result.passed).toBe(true);
|
|
438
|
+
expect(result.syntaxPassed).toBe(true);
|
|
439
|
+
expect(result.findings).toHaveLength(0);
|
|
440
|
+
expect(result.tools).toHaveLength(0);
|
|
441
|
+
});
|
|
442
|
+
|
|
443
|
+
it("should pass with only warning and info findings", async () => {
|
|
444
|
+
const warnings = [
|
|
445
|
+
makeFinding({ severity: "warning" }),
|
|
446
|
+
makeFinding({ severity: "info" }),
|
|
447
|
+
];
|
|
448
|
+
mockSlopResult = { findings: warnings, cached: false };
|
|
449
|
+
mockDiffFilterResult = { shown: warnings, hidden: 0 };
|
|
450
|
+
|
|
451
|
+
const result = await runPipeline({ files: ["src/app.ts"] });
|
|
452
|
+
|
|
453
|
+
expect(result.passed).toBe(true);
|
|
454
|
+
});
|
|
455
|
+
|
|
456
|
+
it("should emit warning finding when all external tools are skipped", async () => {
|
|
457
|
+
mockDetectedTools = [
|
|
458
|
+
makeDetectedTool("biome", true),
|
|
459
|
+
makeDetectedTool("semgrep", false),
|
|
460
|
+
makeDetectedTool("trivy", false),
|
|
461
|
+
makeDetectedTool("secretlint", false),
|
|
462
|
+
];
|
|
463
|
+
|
|
464
|
+
mockSemgrepResult = { findings: [], skipped: true };
|
|
465
|
+
mockTrivyResult = { findings: [], skipped: true };
|
|
466
|
+
mockSecretlintResult = { findings: [], skipped: true };
|
|
467
|
+
|
|
468
|
+
const result = await runPipeline({
|
|
469
|
+
files: ["src/app.ts"],
|
|
470
|
+
diffOnly: false,
|
|
471
|
+
});
|
|
472
|
+
|
|
473
|
+
// Should still pass (warning, not error) but include the warning
|
|
474
|
+
expect(result.passed).toBe(true);
|
|
475
|
+
|
|
476
|
+
const pipelineWarning = result.findings.find(
|
|
477
|
+
(f) => f.tool === "pipeline" && f.severity === "warning",
|
|
478
|
+
);
|
|
479
|
+
expect(pipelineWarning).toBeDefined();
|
|
480
|
+
expect(pipelineWarning?.message).toContain("external");
|
|
481
|
+
});
|
|
482
|
+
|
|
483
|
+
it("should not emit warning when at least one external tool ran", async () => {
|
|
484
|
+
mockDetectedTools = [
|
|
485
|
+
makeDetectedTool("biome", true),
|
|
486
|
+
makeDetectedTool("semgrep", true),
|
|
487
|
+
makeDetectedTool("trivy", false),
|
|
488
|
+
makeDetectedTool("secretlint", false),
|
|
489
|
+
];
|
|
490
|
+
|
|
491
|
+
mockSemgrepResult = { findings: [], skipped: false };
|
|
492
|
+
mockTrivyResult = { findings: [], skipped: true };
|
|
493
|
+
mockSecretlintResult = { findings: [], skipped: true };
|
|
494
|
+
|
|
495
|
+
const result = await runPipeline({
|
|
496
|
+
files: ["src/app.ts"],
|
|
497
|
+
diffOnly: false,
|
|
498
|
+
});
|
|
499
|
+
|
|
500
|
+
const pipelineWarning = result.findings.find((f) => f.tool === "pipeline");
|
|
501
|
+
expect(pipelineWarning).toBeUndefined();
|
|
502
|
+
});
|
|
503
|
+
|
|
504
|
+
it("should run AI review after diff filter and include findings", async () => {
|
|
505
|
+
const aiReviewFinding = makeFinding({
|
|
506
|
+
tool: "ai-review",
|
|
507
|
+
message: "missing null check",
|
|
508
|
+
severity: "warning",
|
|
509
|
+
ruleId: "ai-review/edge-case",
|
|
510
|
+
});
|
|
511
|
+
|
|
512
|
+
mockAIReviewResult = {
|
|
513
|
+
findings: [aiReviewFinding],
|
|
514
|
+
skipped: false,
|
|
515
|
+
tier: "mechanical",
|
|
516
|
+
duration: 100,
|
|
517
|
+
};
|
|
518
|
+
|
|
519
|
+
const result = await runPipeline({ files: ["src/app.ts"] });
|
|
520
|
+
|
|
521
|
+
expect(callOrder).toContain("runAIReview");
|
|
522
|
+
expect(result.findings).toContainEqual(aiReviewFinding);
|
|
523
|
+
const aiReport = result.tools.find((t) => t.tool === "ai-review");
|
|
524
|
+
expect(aiReport).toBeDefined();
|
|
525
|
+
expect(aiReport?.skipped).toBe(false);
|
|
526
|
+
});
|
|
527
|
+
|
|
528
|
+
it("should pass deep flag to AI review when specified", async () => {
|
|
529
|
+
await runPipeline({ files: ["src/app.ts"], deep: true });
|
|
530
|
+
expect(callOrder).toContain("runAIReview");
|
|
531
|
+
});
|
|
532
|
+
|
|
533
|
+
it("should pass when AI review is skipped", async () => {
|
|
534
|
+
mockAIReviewResult = {
|
|
535
|
+
findings: [],
|
|
536
|
+
skipped: true,
|
|
537
|
+
tier: "mechanical",
|
|
538
|
+
duration: 0,
|
|
539
|
+
};
|
|
540
|
+
const result = await runPipeline({ files: ["src/app.ts"] });
|
|
541
|
+
expect(result.passed).toBe(true);
|
|
542
|
+
const aiReport = result.tools.find((t) => t.tool === "ai-review");
|
|
543
|
+
expect(aiReport?.skipped).toBe(true);
|
|
544
|
+
});
|
|
545
|
+
|
|
546
|
+
it("should accept languages option", async () => {
|
|
547
|
+
const result = await runPipeline({
|
|
548
|
+
files: ["src/app.ts"],
|
|
549
|
+
languages: ["typescript"],
|
|
550
|
+
});
|
|
551
|
+
expect(result.syntaxPassed).toBe(true);
|
|
552
|
+
});
|
|
553
|
+
});
|