@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,242 @@
|
|
|
1
|
+
import { afterAll, beforeEach, describe, expect, it, mock } from "bun:test";
|
|
2
|
+
|
|
3
|
+
// Mock tryAIGenerate
|
|
4
|
+
let mockAIResult: {
|
|
5
|
+
text: string | null;
|
|
6
|
+
fromAI: boolean;
|
|
7
|
+
hostDelegation: boolean;
|
|
8
|
+
} = {
|
|
9
|
+
text: null,
|
|
10
|
+
fromAI: false,
|
|
11
|
+
hostDelegation: false,
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
mock.module("../../ai/try-generate", () => ({
|
|
15
|
+
tryAIGenerate: async () => mockAIResult,
|
|
16
|
+
}));
|
|
17
|
+
|
|
18
|
+
mock.module("../../cache/keys", () => ({
|
|
19
|
+
hashContent: (s: string) => `hash-${s.length}`,
|
|
20
|
+
buildCacheKey: async () => "test-cache-key",
|
|
21
|
+
}));
|
|
22
|
+
|
|
23
|
+
mock.module("../../cache/manager", () => ({
|
|
24
|
+
createCacheManager: () => ({
|
|
25
|
+
get: () => null,
|
|
26
|
+
set: () => {},
|
|
27
|
+
stats: () => ({ l1Hits: 0, l2Hits: 0, misses: 0 }),
|
|
28
|
+
}),
|
|
29
|
+
}));
|
|
30
|
+
|
|
31
|
+
afterAll(() => mock.restore());
|
|
32
|
+
|
|
33
|
+
import { resolveReferencedFunctions } from "../ai-review";
|
|
34
|
+
|
|
35
|
+
describe("resolveReferencedFunctions", () => {
|
|
36
|
+
it("should extract function calls from added lines in diff", () => {
|
|
37
|
+
const diff = `--- a/src/app.ts
|
|
38
|
+
+++ b/src/app.ts
|
|
39
|
+
@@ -10,3 +10,5 @@ function existing() {
|
|
40
|
+
+ const result = validateInput(data);
|
|
41
|
+
+ processResult(result);
|
|
42
|
+
return true;`;
|
|
43
|
+
|
|
44
|
+
const entities = [
|
|
45
|
+
{
|
|
46
|
+
name: "validateInput",
|
|
47
|
+
kind: "function" as const,
|
|
48
|
+
startLine: 1,
|
|
49
|
+
endLine: 5,
|
|
50
|
+
filePath: "src/utils.ts",
|
|
51
|
+
body: "function validateInput(data: unknown) {\n if (!data) return null;\n return data;\n}",
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
name: "processResult",
|
|
55
|
+
kind: "function" as const,
|
|
56
|
+
startLine: 10,
|
|
57
|
+
endLine: 15,
|
|
58
|
+
filePath: "src/utils.ts",
|
|
59
|
+
body: "function processResult(result: unknown) {\n console.log(result);\n}",
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
name: "unusedFunction",
|
|
63
|
+
kind: "function" as const,
|
|
64
|
+
startLine: 20,
|
|
65
|
+
endLine: 25,
|
|
66
|
+
filePath: "src/other.ts",
|
|
67
|
+
body: "function unusedFunction() { return 1; }",
|
|
68
|
+
},
|
|
69
|
+
];
|
|
70
|
+
|
|
71
|
+
const result = resolveReferencedFunctions(diff, entities);
|
|
72
|
+
|
|
73
|
+
expect(result).toHaveLength(2);
|
|
74
|
+
expect(result[0]?.name).toBe("validateInput");
|
|
75
|
+
expect(result[1]?.name).toBe("processResult");
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it("should cap at 3 referenced functions per file", () => {
|
|
79
|
+
const diff = `--- a/src/app.ts
|
|
80
|
+
+++ b/src/app.ts
|
|
81
|
+
@@ -1,3 +1,7 @@
|
|
82
|
+
+ fn1();
|
|
83
|
+
+ fn2();
|
|
84
|
+
+ fn3();
|
|
85
|
+
+ fn4();`;
|
|
86
|
+
|
|
87
|
+
const entities = [
|
|
88
|
+
{
|
|
89
|
+
name: "fn1",
|
|
90
|
+
kind: "function" as const,
|
|
91
|
+
startLine: 1,
|
|
92
|
+
endLine: 3,
|
|
93
|
+
filePath: "src/a.ts",
|
|
94
|
+
body: "function fn1() {}",
|
|
95
|
+
},
|
|
96
|
+
{
|
|
97
|
+
name: "fn2",
|
|
98
|
+
kind: "function" as const,
|
|
99
|
+
startLine: 1,
|
|
100
|
+
endLine: 3,
|
|
101
|
+
filePath: "src/b.ts",
|
|
102
|
+
body: "function fn2() {}",
|
|
103
|
+
},
|
|
104
|
+
{
|
|
105
|
+
name: "fn3",
|
|
106
|
+
kind: "function" as const,
|
|
107
|
+
startLine: 1,
|
|
108
|
+
endLine: 3,
|
|
109
|
+
filePath: "src/c.ts",
|
|
110
|
+
body: "function fn3() {}",
|
|
111
|
+
},
|
|
112
|
+
{
|
|
113
|
+
name: "fn4",
|
|
114
|
+
kind: "function" as const,
|
|
115
|
+
startLine: 1,
|
|
116
|
+
endLine: 3,
|
|
117
|
+
filePath: "src/d.ts",
|
|
118
|
+
body: "function fn4() {}",
|
|
119
|
+
},
|
|
120
|
+
];
|
|
121
|
+
|
|
122
|
+
const result = resolveReferencedFunctions(diff, entities);
|
|
123
|
+
expect(result).toHaveLength(3);
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
it("should return empty array when no functions match", () => {
|
|
127
|
+
const diff = `+++ b/src/app.ts
|
|
128
|
+
@@ -1,1 +1,2 @@
|
|
129
|
+
+ const x = 42;`;
|
|
130
|
+
|
|
131
|
+
const result = resolveReferencedFunctions(diff, []);
|
|
132
|
+
expect(result).toHaveLength(0);
|
|
133
|
+
});
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
// Import AFTER mocking
|
|
137
|
+
const { runAIReview } = await import("../ai-review");
|
|
138
|
+
|
|
139
|
+
describe("runAIReview", () => {
|
|
140
|
+
const baseOptions = {
|
|
141
|
+
diff: "+ const x = validateInput(data);",
|
|
142
|
+
entities: [],
|
|
143
|
+
mainaDir: ".maina",
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
beforeEach(() => {
|
|
147
|
+
mockAIResult = { text: null, fromAI: false, hostDelegation: false };
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
it("should return findings from AI response (mechanical tier)", async () => {
|
|
151
|
+
mockAIResult = {
|
|
152
|
+
text: JSON.stringify({
|
|
153
|
+
findings: [
|
|
154
|
+
{
|
|
155
|
+
file: "src/app.ts",
|
|
156
|
+
line: 10,
|
|
157
|
+
message: "validateInput may return null but caller doesn't check",
|
|
158
|
+
severity: "warning",
|
|
159
|
+
ruleId: "ai-review/edge-case",
|
|
160
|
+
},
|
|
161
|
+
],
|
|
162
|
+
}),
|
|
163
|
+
fromAI: true,
|
|
164
|
+
hostDelegation: false,
|
|
165
|
+
};
|
|
166
|
+
|
|
167
|
+
const result = await runAIReview(baseOptions);
|
|
168
|
+
|
|
169
|
+
expect(result.findings).toHaveLength(1);
|
|
170
|
+
expect(result.findings[0]?.tool).toBe("ai-review");
|
|
171
|
+
expect(result.findings[0]?.severity).toBe("warning");
|
|
172
|
+
expect(result.tier).toBe("mechanical");
|
|
173
|
+
expect(result.skipped).toBe(false);
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
it("should cap severity to warning in mechanical mode", async () => {
|
|
177
|
+
mockAIResult = {
|
|
178
|
+
text: JSON.stringify({
|
|
179
|
+
findings: [
|
|
180
|
+
{
|
|
181
|
+
file: "src/app.ts",
|
|
182
|
+
line: 5,
|
|
183
|
+
message: "bad",
|
|
184
|
+
severity: "error",
|
|
185
|
+
ruleId: "ai-review/contract",
|
|
186
|
+
},
|
|
187
|
+
],
|
|
188
|
+
}),
|
|
189
|
+
fromAI: true,
|
|
190
|
+
hostDelegation: false,
|
|
191
|
+
};
|
|
192
|
+
|
|
193
|
+
const result = await runAIReview(baseOptions);
|
|
194
|
+
expect(result.findings[0]?.severity).toBe("warning");
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
it("should allow error severity in deep mode", async () => {
|
|
198
|
+
mockAIResult = {
|
|
199
|
+
text: JSON.stringify({
|
|
200
|
+
findings: [
|
|
201
|
+
{
|
|
202
|
+
file: "src/app.ts",
|
|
203
|
+
line: 5,
|
|
204
|
+
message: "spec violation",
|
|
205
|
+
severity: "error",
|
|
206
|
+
ruleId: "ai-review/spec-compliance",
|
|
207
|
+
},
|
|
208
|
+
],
|
|
209
|
+
}),
|
|
210
|
+
fromAI: true,
|
|
211
|
+
hostDelegation: false,
|
|
212
|
+
};
|
|
213
|
+
|
|
214
|
+
const result = await runAIReview({ ...baseOptions, deep: true });
|
|
215
|
+
expect(result.findings[0]?.severity).toBe("error");
|
|
216
|
+
expect(result.tier).toBe("standard");
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
it("should skip gracefully when AI is unavailable", async () => {
|
|
220
|
+
mockAIResult = { text: null, fromAI: false, hostDelegation: false };
|
|
221
|
+
const result = await runAIReview(baseOptions);
|
|
222
|
+
expect(result.findings).toHaveLength(0);
|
|
223
|
+
expect(result.skipped).toBe(true);
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
it("should skip gracefully on malformed AI response", async () => {
|
|
227
|
+
mockAIResult = { text: "not json", fromAI: true, hostDelegation: false };
|
|
228
|
+
const result = await runAIReview(baseOptions);
|
|
229
|
+
expect(result.findings).toHaveLength(0);
|
|
230
|
+
expect(result.skipped).toBe(true);
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
it("should handle host delegation by skipping", async () => {
|
|
234
|
+
mockAIResult = {
|
|
235
|
+
text: "[HOST_DELEGATION] prompt here",
|
|
236
|
+
fromAI: false,
|
|
237
|
+
hostDelegation: true,
|
|
238
|
+
};
|
|
239
|
+
const result = await runAIReview(baseOptions);
|
|
240
|
+
expect(result.skipped).toBe(true);
|
|
241
|
+
});
|
|
242
|
+
});
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { describe, expect, it } from "bun:test";
|
|
2
|
+
import { parseDiffCoverJson, runCoverage } from "../coverage";
|
|
3
|
+
|
|
4
|
+
describe("diff-cover Coverage Integration", () => {
|
|
5
|
+
describe("parseDiffCoverJson", () => {
|
|
6
|
+
it("should parse diff-cover JSON into findings", () => {
|
|
7
|
+
const json = JSON.stringify({
|
|
8
|
+
report_name: "Diff Coverage",
|
|
9
|
+
diff_name: "master...HEAD",
|
|
10
|
+
src_stats: {
|
|
11
|
+
"src/app.ts": {
|
|
12
|
+
covered_lines: [10, 11, 12],
|
|
13
|
+
violation_lines: [15, 16],
|
|
14
|
+
percent_covered: 60.0,
|
|
15
|
+
},
|
|
16
|
+
"src/utils.ts": {
|
|
17
|
+
covered_lines: [1, 2, 3, 4, 5],
|
|
18
|
+
violation_lines: [],
|
|
19
|
+
percent_covered: 100.0,
|
|
20
|
+
},
|
|
21
|
+
},
|
|
22
|
+
total_num_lines: 10,
|
|
23
|
+
total_num_violations: 2,
|
|
24
|
+
total_percent_covered: 80.0,
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
const findings = parseDiffCoverJson(json);
|
|
28
|
+
|
|
29
|
+
// Only files with violations should produce findings
|
|
30
|
+
expect(findings).toHaveLength(2);
|
|
31
|
+
expect(findings[0]?.tool).toBe("diff-cover");
|
|
32
|
+
expect(findings[0]?.file).toBe("src/app.ts");
|
|
33
|
+
expect(findings[0]?.line).toBe(15);
|
|
34
|
+
expect(findings[0]?.severity).toBe("warning");
|
|
35
|
+
expect(findings[0]?.ruleId).toBe("diff-cover/uncovered-line");
|
|
36
|
+
expect(findings[1]?.line).toBe(16);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it("should handle empty src_stats", () => {
|
|
40
|
+
const json = JSON.stringify({ src_stats: {} });
|
|
41
|
+
expect(parseDiffCoverJson(json)).toHaveLength(0);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it("should handle malformed JSON", () => {
|
|
45
|
+
expect(parseDiffCoverJson("not json")).toHaveLength(0);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it("should handle files with no violations", () => {
|
|
49
|
+
const json = JSON.stringify({
|
|
50
|
+
src_stats: {
|
|
51
|
+
"src/clean.ts": {
|
|
52
|
+
covered_lines: [1, 2, 3],
|
|
53
|
+
violation_lines: [],
|
|
54
|
+
percent_covered: 100.0,
|
|
55
|
+
},
|
|
56
|
+
},
|
|
57
|
+
});
|
|
58
|
+
expect(parseDiffCoverJson(json)).toHaveLength(0);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it("should include coverage percentage in message", () => {
|
|
62
|
+
const json = JSON.stringify({
|
|
63
|
+
src_stats: {
|
|
64
|
+
"src/app.ts": {
|
|
65
|
+
covered_lines: [1],
|
|
66
|
+
violation_lines: [2],
|
|
67
|
+
percent_covered: 50.0,
|
|
68
|
+
},
|
|
69
|
+
},
|
|
70
|
+
});
|
|
71
|
+
const findings = parseDiffCoverJson(json);
|
|
72
|
+
expect(findings[0]?.message).toContain("50%");
|
|
73
|
+
});
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
describe("runCoverage", () => {
|
|
77
|
+
it("should skip when diff-cover is not available", async () => {
|
|
78
|
+
const result = await runCoverage({ available: false });
|
|
79
|
+
expect(result.skipped).toBe(true);
|
|
80
|
+
expect(result.findings).toHaveLength(0);
|
|
81
|
+
});
|
|
82
|
+
});
|
|
83
|
+
});
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
import { describe, expect, it, test } from "bun:test";
|
|
2
|
+
import {
|
|
3
|
+
detectTool,
|
|
4
|
+
detectTools,
|
|
5
|
+
isToolAvailable,
|
|
6
|
+
TOOL_REGISTRY,
|
|
7
|
+
type ToolName,
|
|
8
|
+
} from "../detect";
|
|
9
|
+
|
|
10
|
+
// ─── Type checks ────────────────────────────────────────────────────────────
|
|
11
|
+
|
|
12
|
+
describe("detect types", () => {
|
|
13
|
+
test("TOOL_REGISTRY has all expected tools", () => {
|
|
14
|
+
const expectedTools: ToolName[] = [
|
|
15
|
+
"biome",
|
|
16
|
+
"semgrep",
|
|
17
|
+
"trivy",
|
|
18
|
+
"secretlint",
|
|
19
|
+
"sonarqube",
|
|
20
|
+
"stryker",
|
|
21
|
+
];
|
|
22
|
+
for (const tool of expectedTools) {
|
|
23
|
+
expect(TOOL_REGISTRY[tool]).toBeDefined();
|
|
24
|
+
expect(typeof TOOL_REGISTRY[tool].command).toBe("string");
|
|
25
|
+
expect(typeof TOOL_REGISTRY[tool].versionFlag).toBe("string");
|
|
26
|
+
}
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
test("TOOL_REGISTRY maps sonarqube to sonar-scanner command", () => {
|
|
30
|
+
expect(TOOL_REGISTRY.sonarqube.command).toBe("sonar-scanner");
|
|
31
|
+
});
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
// ─── detectTool ─────────────────────────────────────────────────────────────
|
|
35
|
+
|
|
36
|
+
describe("detectTool", () => {
|
|
37
|
+
test("returns a DetectedTool shape", async () => {
|
|
38
|
+
const result = await detectTool("biome");
|
|
39
|
+
expect(result).toHaveProperty("name");
|
|
40
|
+
expect(result).toHaveProperty("command");
|
|
41
|
+
expect(result).toHaveProperty("version");
|
|
42
|
+
expect(result).toHaveProperty("available");
|
|
43
|
+
expect(typeof result.name).toBe("string");
|
|
44
|
+
expect(typeof result.command).toBe("string");
|
|
45
|
+
expect(typeof result.available).toBe("boolean");
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
test("detects biome as available (installed in project)", async () => {
|
|
49
|
+
const result = await detectTool("biome");
|
|
50
|
+
expect(result.name).toBe("biome");
|
|
51
|
+
// command may be "biome" (global) or a local node_modules/.bin path
|
|
52
|
+
expect(result.command).toContain("biome");
|
|
53
|
+
expect(result.available).toBe(true);
|
|
54
|
+
expect(result.version).not.toBeNull();
|
|
55
|
+
expect(typeof result.version).toBe("string");
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
test("detects sonarqube status correctly", async () => {
|
|
59
|
+
const result = await detectTool("sonarqube");
|
|
60
|
+
expect(result.name).toBe("sonarqube");
|
|
61
|
+
expect(result.command).toBe("sonar-scanner");
|
|
62
|
+
// sonarqube may or may not be installed — just verify shape
|
|
63
|
+
expect(typeof result.available).toBe("boolean");
|
|
64
|
+
});
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
// ─── detectTools ────────────────────────────────────────────────────────────
|
|
68
|
+
|
|
69
|
+
describe("detectTools", () => {
|
|
70
|
+
test("returns an array of DetectedTool for all registered tools", async () => {
|
|
71
|
+
const results = await detectTools();
|
|
72
|
+
expect(Array.isArray(results)).toBe(true);
|
|
73
|
+
expect(results.length).toBe(Object.keys(TOOL_REGISTRY).length);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
test("each result has the correct shape", async () => {
|
|
77
|
+
const results = await detectTools();
|
|
78
|
+
for (const tool of results) {
|
|
79
|
+
expect(typeof tool.name).toBe("string");
|
|
80
|
+
expect(typeof tool.command).toBe("string");
|
|
81
|
+
expect(typeof tool.available).toBe("boolean");
|
|
82
|
+
// version is string or null
|
|
83
|
+
if (tool.available) {
|
|
84
|
+
expect(typeof tool.version).toBe("string");
|
|
85
|
+
} else {
|
|
86
|
+
expect(tool.version).toBeNull();
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
test("should auto-detect installed tools", async () => {
|
|
92
|
+
const results = await detectTools();
|
|
93
|
+
const biome = results.find((t) => t.name === "biome");
|
|
94
|
+
expect(biome).toBeDefined();
|
|
95
|
+
expect(biome?.available).toBe(true);
|
|
96
|
+
expect(biome?.version).not.toBeNull();
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
test("detects sonarqube status in detectTools", async () => {
|
|
100
|
+
const results = await detectTools();
|
|
101
|
+
const sonarqube = results.find((t) => t.name === "sonarqube");
|
|
102
|
+
expect(sonarqube).toBeDefined();
|
|
103
|
+
expect(typeof sonarqube?.available).toBe("boolean");
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
test("detects tools in parallel (all results returned)", async () => {
|
|
107
|
+
const results = await detectTools();
|
|
108
|
+
const names = results.map((t) => t.name);
|
|
109
|
+
expect(names).toContain("biome");
|
|
110
|
+
expect(names).toContain("semgrep");
|
|
111
|
+
expect(names).toContain("trivy");
|
|
112
|
+
expect(names).toContain("secretlint");
|
|
113
|
+
expect(names).toContain("sonarqube");
|
|
114
|
+
expect(names).toContain("stryker");
|
|
115
|
+
});
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
// ─── isToolAvailable ────────────────────────────────────────────────────────
|
|
119
|
+
|
|
120
|
+
describe("isToolAvailable", () => {
|
|
121
|
+
test("returns true for biome", async () => {
|
|
122
|
+
const available = await isToolAvailable("biome");
|
|
123
|
+
expect(available).toBe(true);
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
test("returns boolean for sonarqube", async () => {
|
|
127
|
+
const available = await isToolAvailable("sonarqube");
|
|
128
|
+
expect(typeof available).toBe("boolean");
|
|
129
|
+
});
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
// ─── Language-specific linter tools ─────────────────────────────────────────
|
|
133
|
+
|
|
134
|
+
describe("language-specific linter tools", () => {
|
|
135
|
+
it("should have ruff in tool registry", () => {
|
|
136
|
+
expect(TOOL_REGISTRY.ruff).toBeDefined();
|
|
137
|
+
expect(TOOL_REGISTRY.ruff.command).toBe("ruff");
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
it("should have golangci-lint in tool registry", () => {
|
|
141
|
+
expect(TOOL_REGISTRY["golangci-lint"]).toBeDefined();
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
it("should have cargo-clippy in tool registry", () => {
|
|
145
|
+
expect(TOOL_REGISTRY["cargo-clippy"]).toBeDefined();
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
it("should have cargo-audit in tool registry", () => {
|
|
149
|
+
expect(TOOL_REGISTRY["cargo-audit"]).toBeDefined();
|
|
150
|
+
});
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
// ─── VerifyPipeline (TDD contract from Sprint 3) ───────────────────────────
|
|
154
|
+
|
|
155
|
+
describe("VerifyPipeline", () => {
|
|
156
|
+
it("should auto-detect installed tools", async () => {
|
|
157
|
+
const results = await detectTools();
|
|
158
|
+
const installed = results.filter((t) => t.available);
|
|
159
|
+
expect(installed.length).toBeGreaterThan(0);
|
|
160
|
+
// biome is always installed in this project
|
|
161
|
+
const biome = installed.find((t) => t.name === "biome");
|
|
162
|
+
expect(biome).toBeDefined();
|
|
163
|
+
expect(biome?.version).not.toBeNull();
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
it("should skip missing tools with info note", async () => {
|
|
167
|
+
const results = await detectTools();
|
|
168
|
+
const missing = results.filter((t) => !t.available);
|
|
169
|
+
expect(missing.length).toBeGreaterThan(0);
|
|
170
|
+
for (const tool of missing) {
|
|
171
|
+
expect(tool.available).toBe(false);
|
|
172
|
+
expect(tool.version).toBeNull();
|
|
173
|
+
}
|
|
174
|
+
});
|
|
175
|
+
});
|