@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.
Files changed (156) hide show
  1. package/README.md +31 -0
  2. package/package.json +37 -0
  3. package/src/ai/__tests__/ai.test.ts +207 -0
  4. package/src/ai/__tests__/design-approaches.test.ts +192 -0
  5. package/src/ai/__tests__/spec-questions.test.ts +191 -0
  6. package/src/ai/__tests__/tiers.test.ts +110 -0
  7. package/src/ai/commit-msg.ts +28 -0
  8. package/src/ai/design-approaches.ts +76 -0
  9. package/src/ai/index.ts +205 -0
  10. package/src/ai/pr-summary.ts +60 -0
  11. package/src/ai/spec-questions.ts +74 -0
  12. package/src/ai/tiers.ts +52 -0
  13. package/src/ai/try-generate.ts +89 -0
  14. package/src/ai/validate.ts +66 -0
  15. package/src/benchmark/__tests__/reporter.test.ts +525 -0
  16. package/src/benchmark/__tests__/runner.test.ts +113 -0
  17. package/src/benchmark/__tests__/story-loader.test.ts +152 -0
  18. package/src/benchmark/reporter.ts +332 -0
  19. package/src/benchmark/runner.ts +91 -0
  20. package/src/benchmark/story-loader.ts +88 -0
  21. package/src/benchmark/types.ts +95 -0
  22. package/src/cache/__tests__/keys.test.ts +97 -0
  23. package/src/cache/__tests__/manager.test.ts +312 -0
  24. package/src/cache/__tests__/ttl.test.ts +94 -0
  25. package/src/cache/keys.ts +44 -0
  26. package/src/cache/manager.ts +231 -0
  27. package/src/cache/ttl.ts +77 -0
  28. package/src/config/__tests__/config.test.ts +376 -0
  29. package/src/config/index.ts +198 -0
  30. package/src/context/__tests__/budget.test.ts +179 -0
  31. package/src/context/__tests__/engine.test.ts +163 -0
  32. package/src/context/__tests__/episodic.test.ts +291 -0
  33. package/src/context/__tests__/relevance.test.ts +323 -0
  34. package/src/context/__tests__/retrieval.test.ts +143 -0
  35. package/src/context/__tests__/selector.test.ts +174 -0
  36. package/src/context/__tests__/semantic.test.ts +252 -0
  37. package/src/context/__tests__/treesitter.test.ts +229 -0
  38. package/src/context/__tests__/working.test.ts +236 -0
  39. package/src/context/budget.ts +130 -0
  40. package/src/context/engine.ts +394 -0
  41. package/src/context/episodic.ts +251 -0
  42. package/src/context/relevance.ts +325 -0
  43. package/src/context/retrieval.ts +325 -0
  44. package/src/context/selector.ts +93 -0
  45. package/src/context/semantic.ts +331 -0
  46. package/src/context/treesitter.ts +216 -0
  47. package/src/context/working.ts +192 -0
  48. package/src/db/__tests__/db.test.ts +151 -0
  49. package/src/db/index.ts +211 -0
  50. package/src/db/schema.ts +84 -0
  51. package/src/design/__tests__/design.test.ts +310 -0
  52. package/src/design/__tests__/generate-hld-lld.test.ts +109 -0
  53. package/src/design/__tests__/review.test.ts +561 -0
  54. package/src/design/index.ts +297 -0
  55. package/src/design/review.ts +327 -0
  56. package/src/explain/__tests__/explain.test.ts +173 -0
  57. package/src/explain/index.ts +181 -0
  58. package/src/features/__tests__/analyzer.test.ts +358 -0
  59. package/src/features/__tests__/checklist.test.ts +454 -0
  60. package/src/features/__tests__/numbering.test.ts +319 -0
  61. package/src/features/__tests__/quality.test.ts +295 -0
  62. package/src/features/__tests__/traceability.test.ts +147 -0
  63. package/src/features/analyzer.ts +445 -0
  64. package/src/features/checklist.ts +366 -0
  65. package/src/features/index.ts +18 -0
  66. package/src/features/numbering.ts +404 -0
  67. package/src/features/quality.ts +349 -0
  68. package/src/features/test-stubs.ts +157 -0
  69. package/src/features/traceability.ts +260 -0
  70. package/src/feedback/__tests__/async-feedback.test.ts +52 -0
  71. package/src/feedback/__tests__/collector.test.ts +219 -0
  72. package/src/feedback/__tests__/compress.test.ts +150 -0
  73. package/src/feedback/__tests__/preferences.test.ts +169 -0
  74. package/src/feedback/collector.ts +135 -0
  75. package/src/feedback/compress.ts +92 -0
  76. package/src/feedback/preferences.ts +108 -0
  77. package/src/git/__tests__/git.test.ts +62 -0
  78. package/src/git/index.ts +110 -0
  79. package/src/hooks/__tests__/runner.test.ts +266 -0
  80. package/src/hooks/index.ts +8 -0
  81. package/src/hooks/runner.ts +130 -0
  82. package/src/index.ts +356 -0
  83. package/src/init/__tests__/init.test.ts +228 -0
  84. package/src/init/index.ts +364 -0
  85. package/src/language/__tests__/detect.test.ts +77 -0
  86. package/src/language/__tests__/profile.test.ts +51 -0
  87. package/src/language/detect.ts +70 -0
  88. package/src/language/profile.ts +110 -0
  89. package/src/prompts/__tests__/defaults.test.ts +52 -0
  90. package/src/prompts/__tests__/engine.test.ts +183 -0
  91. package/src/prompts/__tests__/evolution-resolve.test.ts +169 -0
  92. package/src/prompts/__tests__/evolution.test.ts +187 -0
  93. package/src/prompts/__tests__/loader.test.ts +105 -0
  94. package/src/prompts/candidates/review-v2.md +55 -0
  95. package/src/prompts/defaults/ai-review.md +49 -0
  96. package/src/prompts/defaults/commit.md +30 -0
  97. package/src/prompts/defaults/context.md +26 -0
  98. package/src/prompts/defaults/design-approaches.md +57 -0
  99. package/src/prompts/defaults/design-hld-lld.md +55 -0
  100. package/src/prompts/defaults/design.md +53 -0
  101. package/src/prompts/defaults/explain.md +31 -0
  102. package/src/prompts/defaults/fix.md +32 -0
  103. package/src/prompts/defaults/index.ts +38 -0
  104. package/src/prompts/defaults/review.md +41 -0
  105. package/src/prompts/defaults/spec-questions.md +59 -0
  106. package/src/prompts/defaults/tests.md +72 -0
  107. package/src/prompts/engine.ts +137 -0
  108. package/src/prompts/evolution.ts +409 -0
  109. package/src/prompts/loader.ts +71 -0
  110. package/src/review/__tests__/review.test.ts +288 -0
  111. package/src/review/comprehensive.ts +362 -0
  112. package/src/review/index.ts +417 -0
  113. package/src/stats/__tests__/tracker.test.ts +323 -0
  114. package/src/stats/index.ts +11 -0
  115. package/src/stats/tracker.ts +492 -0
  116. package/src/ticket/__tests__/ticket.test.ts +273 -0
  117. package/src/ticket/index.ts +185 -0
  118. package/src/utils.ts +87 -0
  119. package/src/verify/__tests__/ai-review.test.ts +242 -0
  120. package/src/verify/__tests__/coverage.test.ts +83 -0
  121. package/src/verify/__tests__/detect.test.ts +175 -0
  122. package/src/verify/__tests__/diff-filter.test.ts +338 -0
  123. package/src/verify/__tests__/fix.test.ts +478 -0
  124. package/src/verify/__tests__/linters/clippy.test.ts +45 -0
  125. package/src/verify/__tests__/linters/go-vet.test.ts +27 -0
  126. package/src/verify/__tests__/linters/ruff.test.ts +64 -0
  127. package/src/verify/__tests__/mutation.test.ts +141 -0
  128. package/src/verify/__tests__/pipeline.test.ts +553 -0
  129. package/src/verify/__tests__/proof.test.ts +97 -0
  130. package/src/verify/__tests__/secretlint.test.ts +190 -0
  131. package/src/verify/__tests__/semgrep.test.ts +217 -0
  132. package/src/verify/__tests__/slop.test.ts +366 -0
  133. package/src/verify/__tests__/sonar.test.ts +113 -0
  134. package/src/verify/__tests__/syntax-guard.test.ts +227 -0
  135. package/src/verify/__tests__/trivy.test.ts +191 -0
  136. package/src/verify/__tests__/visual.test.ts +139 -0
  137. package/src/verify/ai-review.ts +276 -0
  138. package/src/verify/coverage.ts +134 -0
  139. package/src/verify/detect.ts +171 -0
  140. package/src/verify/diff-filter.ts +183 -0
  141. package/src/verify/fix.ts +317 -0
  142. package/src/verify/linters/clippy.ts +52 -0
  143. package/src/verify/linters/go-vet.ts +32 -0
  144. package/src/verify/linters/ruff.ts +47 -0
  145. package/src/verify/mutation.ts +143 -0
  146. package/src/verify/pipeline.ts +328 -0
  147. package/src/verify/proof.ts +277 -0
  148. package/src/verify/secretlint.ts +168 -0
  149. package/src/verify/semgrep.ts +170 -0
  150. package/src/verify/slop.ts +493 -0
  151. package/src/verify/sonar.ts +146 -0
  152. package/src/verify/syntax-guard.ts +251 -0
  153. package/src/verify/trivy.ts +161 -0
  154. package/src/verify/visual.ts +460 -0
  155. package/src/workflow/__tests__/context.test.ts +110 -0
  156. package/src/workflow/context.ts +81 -0
@@ -0,0 +1,97 @@
1
+ import { describe, expect, it } from "bun:test";
2
+ import { formatVerificationProof, type VerificationProof } from "../proof";
3
+
4
+ describe("formatVerificationProof", () => {
5
+ const baseProof: VerificationProof = {
6
+ pipeline: [
7
+ { tool: "slop", findings: 0, duration: 234, skipped: false },
8
+ { tool: "semgrep", findings: 0, duration: 1200, skipped: false },
9
+ { tool: "trivy", findings: 0, duration: 3400, skipped: false },
10
+ { tool: "secretlint", findings: 0, duration: 890, skipped: false },
11
+ { tool: "sonarqube", findings: 0, duration: 0, skipped: true },
12
+ { tool: "ai-review", findings: 0, duration: 2100, skipped: false },
13
+ ],
14
+ pipelinePassed: true,
15
+ pipelineDuration: 9100,
16
+ tests: { passed: 980, failed: 0, files: 87 },
17
+ review: {
18
+ stage1Passed: true,
19
+ stage1Findings: 0,
20
+ stage2Passed: true,
21
+ stage2Findings: 1,
22
+ },
23
+ slop: { findings: 0 },
24
+ visual: { pages: 2, regressions: 0 },
25
+ workflowSummary: "# Workflow: feature/018\n\n## plan\nScaffolded.",
26
+ };
27
+
28
+ it("should include pipeline section with tool table", () => {
29
+ const md = formatVerificationProof(baseProof);
30
+ expect(md).toContain("## Verification Proof");
31
+ expect(md).toContain("Pipeline:");
32
+ expect(md).toContain("| slop |");
33
+ expect(md).toContain("| semgrep |");
34
+ expect(md).toContain("skipped");
35
+ });
36
+
37
+ it("should include test results", () => {
38
+ const md = formatVerificationProof(baseProof);
39
+ expect(md).toContain("Tests: 980 pass, 0 fail");
40
+ });
41
+
42
+ it("should include code review results", () => {
43
+ const md = formatVerificationProof(baseProof);
44
+ expect(md).toContain("Code Review");
45
+ expect(md).toContain("spec compliance");
46
+ expect(md).toContain("code quality");
47
+ });
48
+
49
+ it("should include slop results", () => {
50
+ const md = formatVerificationProof(baseProof);
51
+ expect(md).toContain("Slop: clean");
52
+ });
53
+
54
+ it("should include visual results", () => {
55
+ const md = formatVerificationProof(baseProof);
56
+ expect(md).toContain("Visual: 2 page(s)");
57
+ });
58
+
59
+ it("should include workflow context", () => {
60
+ const md = formatVerificationProof(baseProof);
61
+ expect(md).toContain("Workflow Context");
62
+ expect(md).toContain("feature/018");
63
+ });
64
+
65
+ it("should use collapsible details tags", () => {
66
+ const md = formatVerificationProof(baseProof);
67
+ expect(md).toContain("<details>");
68
+ expect(md).toContain("</details>");
69
+ });
70
+
71
+ it("should handle missing optional sections", () => {
72
+ const minimal: VerificationProof = {
73
+ pipeline: [],
74
+ pipelinePassed: true,
75
+ pipelineDuration: 0,
76
+ tests: null,
77
+ review: null,
78
+ slop: null,
79
+ visual: null,
80
+ workflowSummary: null,
81
+ };
82
+ const md = formatVerificationProof(minimal);
83
+ expect(md).toContain("## Verification Proof");
84
+ expect(md).not.toContain("Tests:");
85
+ expect(md).not.toContain("Visual:");
86
+ });
87
+
88
+ it("should show failure icons when checks fail", () => {
89
+ const failed: VerificationProof = {
90
+ ...baseProof,
91
+ pipelinePassed: false,
92
+ tests: { passed: 978, failed: 2, files: 87 },
93
+ };
94
+ const md = formatVerificationProof(failed);
95
+ expect(md).toContain("❌");
96
+ });
97
+ });
@@ -0,0 +1,190 @@
1
+ import { describe, expect, it } from "bun:test";
2
+ import { parseSecretlintOutput, runSecretlint } from "../secretlint";
3
+
4
+ // ─── parseSecretlintOutput ─────────────────────────────────────────────────
5
+
6
+ describe("parseSecretlintOutput", () => {
7
+ it("should return empty array for empty output", () => {
8
+ const findings = parseSecretlintOutput("");
9
+ expect(findings).toEqual([]);
10
+ });
11
+
12
+ it("should parse a single secret finding", () => {
13
+ const output = JSON.stringify([
14
+ {
15
+ filePath: "src/config.ts",
16
+ messages: [
17
+ {
18
+ ruleId: "@secretlint/secretlint-rule-preset-recommend",
19
+ message: "Found AWS Access Key ID",
20
+ range: [10, 30],
21
+ loc: {
22
+ start: { line: 5, column: 10 },
23
+ end: { line: 5, column: 30 },
24
+ },
25
+ severity: 2,
26
+ },
27
+ ],
28
+ },
29
+ ]);
30
+
31
+ const findings = parseSecretlintOutput(output);
32
+ expect(findings.length).toBe(1);
33
+ expect(findings[0]?.tool).toBe("secretlint");
34
+ expect(findings[0]?.file).toBe("src/config.ts");
35
+ expect(findings[0]?.line).toBe(5);
36
+ expect(findings[0]?.column).toBe(10);
37
+ expect(findings[0]?.message).toBe("Found AWS Access Key ID");
38
+ expect(findings[0]?.severity).toBe("error");
39
+ expect(findings[0]?.ruleId).toBe(
40
+ "@secretlint/secretlint-rule-preset-recommend",
41
+ );
42
+ });
43
+
44
+ it("should map severity levels correctly", () => {
45
+ const makeOutput = (severity: number) =>
46
+ JSON.stringify([
47
+ {
48
+ filePath: "file.ts",
49
+ messages: [
50
+ {
51
+ ruleId: "rule",
52
+ message: "msg",
53
+ loc: {
54
+ start: { line: 1, column: 0 },
55
+ end: { line: 1, column: 10 },
56
+ },
57
+ severity,
58
+ },
59
+ ],
60
+ },
61
+ ]);
62
+
63
+ // severity 2 = error, 1 = warning, 0 = info
64
+ expect(parseSecretlintOutput(makeOutput(2))[0]?.severity).toBe("error");
65
+ expect(parseSecretlintOutput(makeOutput(1))[0]?.severity).toBe("warning");
66
+ expect(parseSecretlintOutput(makeOutput(0))[0]?.severity).toBe("info");
67
+ });
68
+
69
+ it("should handle multiple files with multiple messages", () => {
70
+ const output = JSON.stringify([
71
+ {
72
+ filePath: "a.ts",
73
+ messages: [
74
+ {
75
+ ruleId: "rule-a",
76
+ message: "Secret A",
77
+ loc: {
78
+ start: { line: 1, column: 0 },
79
+ end: { line: 1, column: 10 },
80
+ },
81
+ severity: 2,
82
+ },
83
+ {
84
+ ruleId: "rule-b",
85
+ message: "Secret B",
86
+ loc: {
87
+ start: { line: 5, column: 3 },
88
+ end: { line: 5, column: 20 },
89
+ },
90
+ severity: 1,
91
+ },
92
+ ],
93
+ },
94
+ {
95
+ filePath: "b.ts",
96
+ messages: [
97
+ {
98
+ ruleId: "rule-c",
99
+ message: "Secret C",
100
+ loc: {
101
+ start: { line: 10, column: 0 },
102
+ end: { line: 10, column: 30 },
103
+ },
104
+ severity: 2,
105
+ },
106
+ ],
107
+ },
108
+ ]);
109
+
110
+ const findings = parseSecretlintOutput(output);
111
+ expect(findings.length).toBe(3);
112
+ expect(findings[0]?.file).toBe("a.ts");
113
+ expect(findings[1]?.file).toBe("a.ts");
114
+ expect(findings[2]?.file).toBe("b.ts");
115
+ });
116
+
117
+ it("should handle files with empty messages array", () => {
118
+ const output = JSON.stringify([
119
+ {
120
+ filePath: "clean.ts",
121
+ messages: [],
122
+ },
123
+ ]);
124
+ const findings = parseSecretlintOutput(output);
125
+ expect(findings).toEqual([]);
126
+ });
127
+
128
+ it("should return empty array for invalid JSON", () => {
129
+ const findings = parseSecretlintOutput("not valid json {{{");
130
+ expect(findings).toEqual([]);
131
+ });
132
+
133
+ it("should return empty array for malformed structure", () => {
134
+ const findings = parseSecretlintOutput(
135
+ JSON.stringify({ unexpected: true }),
136
+ );
137
+ expect(findings).toEqual([]);
138
+ });
139
+
140
+ it("should handle messages with missing loc gracefully", () => {
141
+ const output = JSON.stringify([
142
+ {
143
+ filePath: "file.ts",
144
+ messages: [
145
+ {
146
+ ruleId: "rule-x",
147
+ message: "No loc info",
148
+ severity: 2,
149
+ },
150
+ ],
151
+ },
152
+ ]);
153
+ const findings = parseSecretlintOutput(output);
154
+ expect(findings.length).toBe(1);
155
+ expect(findings[0]?.line).toBe(0);
156
+ expect(findings[0]?.column).toBeUndefined();
157
+ });
158
+ });
159
+
160
+ // ─── runSecretlint ─────────────────────────────────────────────────────────
161
+
162
+ describe("runSecretlint", () => {
163
+ it("should skip when secretlint is not installed", async () => {
164
+ const result = await runSecretlint();
165
+ if (result.skipped) {
166
+ expect(result.findings).toEqual([]);
167
+ expect(result.skipped).toBe(true);
168
+ } else {
169
+ expect(Array.isArray(result.findings)).toBe(true);
170
+ expect(result.skipped).toBe(false);
171
+ }
172
+ });
173
+
174
+ it("should return correct result shape", async () => {
175
+ const result = await runSecretlint();
176
+ expect(result).toHaveProperty("findings");
177
+ expect(result).toHaveProperty("skipped");
178
+ expect(Array.isArray(result.findings)).toBe(true);
179
+ expect(typeof result.skipped).toBe("boolean");
180
+ });
181
+
182
+ it("should accept options without crashing", async () => {
183
+ const result = await runSecretlint({
184
+ files: ["nonexistent-file.ts"],
185
+ cwd: "/tmp",
186
+ });
187
+ expect(result).toHaveProperty("findings");
188
+ expect(result).toHaveProperty("skipped");
189
+ });
190
+ });
@@ -0,0 +1,217 @@
1
+ import { describe, expect, it } from "bun:test";
2
+ import { parseSarif, runSemgrep } from "../semgrep";
3
+
4
+ // ─── parseSarif ────────────────────────────────────────────────────────────
5
+
6
+ describe("parseSarif", () => {
7
+ it("should return empty array for empty SARIF", () => {
8
+ const sarif = JSON.stringify({
9
+ runs: [{ results: [] }],
10
+ });
11
+ const findings = parseSarif(sarif);
12
+ expect(findings).toEqual([]);
13
+ });
14
+
15
+ it("should parse a single finding from SARIF", () => {
16
+ const sarif = JSON.stringify({
17
+ runs: [
18
+ {
19
+ results: [
20
+ {
21
+ ruleId: "javascript.express.security.audit.xss",
22
+ message: { text: "Found potential XSS vulnerability" },
23
+ locations: [
24
+ {
25
+ physicalLocation: {
26
+ artifactLocation: { uri: "src/api.ts" },
27
+ region: { startLine: 42, startColumn: 10 },
28
+ },
29
+ },
30
+ ],
31
+ level: "error",
32
+ },
33
+ ],
34
+ },
35
+ ],
36
+ });
37
+
38
+ const findings = parseSarif(sarif);
39
+ expect(findings.length).toBe(1);
40
+ expect(findings[0]?.tool).toBe("semgrep");
41
+ expect(findings[0]?.file).toBe("src/api.ts");
42
+ expect(findings[0]?.line).toBe(42);
43
+ expect(findings[0]?.column).toBe(10);
44
+ expect(findings[0]?.message).toBe("Found potential XSS vulnerability");
45
+ expect(findings[0]?.severity).toBe("error");
46
+ expect(findings[0]?.ruleId).toBe("javascript.express.security.audit.xss");
47
+ });
48
+
49
+ it("should map SARIF levels to severity correctly", () => {
50
+ const makeSarif = (level: string) =>
51
+ JSON.stringify({
52
+ runs: [
53
+ {
54
+ results: [
55
+ {
56
+ ruleId: "rule1",
57
+ message: { text: "msg" },
58
+ locations: [
59
+ {
60
+ physicalLocation: {
61
+ artifactLocation: { uri: "file.ts" },
62
+ region: { startLine: 1 },
63
+ },
64
+ },
65
+ ],
66
+ level,
67
+ },
68
+ ],
69
+ },
70
+ ],
71
+ });
72
+
73
+ expect(parseSarif(makeSarif("error"))[0]?.severity).toBe("error");
74
+ expect(parseSarif(makeSarif("warning"))[0]?.severity).toBe("warning");
75
+ expect(parseSarif(makeSarif("note"))[0]?.severity).toBe("info");
76
+ expect(parseSarif(makeSarif("none"))[0]?.severity).toBe("info");
77
+ expect(parseSarif(makeSarif("unknown-level"))[0]?.severity).toBe("warning");
78
+ });
79
+
80
+ it("should handle multiple runs with multiple results", () => {
81
+ const sarif = JSON.stringify({
82
+ runs: [
83
+ {
84
+ results: [
85
+ {
86
+ ruleId: "rule-a",
87
+ message: { text: "Issue A" },
88
+ locations: [
89
+ {
90
+ physicalLocation: {
91
+ artifactLocation: { uri: "a.ts" },
92
+ region: { startLine: 10 },
93
+ },
94
+ },
95
+ ],
96
+ level: "warning",
97
+ },
98
+ ],
99
+ },
100
+ {
101
+ results: [
102
+ {
103
+ ruleId: "rule-b",
104
+ message: { text: "Issue B" },
105
+ locations: [
106
+ {
107
+ physicalLocation: {
108
+ artifactLocation: { uri: "b.ts" },
109
+ region: { startLine: 20, startColumn: 5 },
110
+ },
111
+ },
112
+ ],
113
+ level: "error",
114
+ },
115
+ ],
116
+ },
117
+ ],
118
+ });
119
+
120
+ const findings = parseSarif(sarif);
121
+ expect(findings.length).toBe(2);
122
+ expect(findings[0]?.file).toBe("a.ts");
123
+ expect(findings[1]?.file).toBe("b.ts");
124
+ });
125
+
126
+ it("should handle results with no locations gracefully", () => {
127
+ const sarif = JSON.stringify({
128
+ runs: [
129
+ {
130
+ results: [
131
+ {
132
+ ruleId: "rule-x",
133
+ message: { text: "No location" },
134
+ locations: [],
135
+ level: "warning",
136
+ },
137
+ ],
138
+ },
139
+ ],
140
+ });
141
+
142
+ const findings = parseSarif(sarif);
143
+ expect(findings.length).toBe(1);
144
+ expect(findings[0]?.file).toBe("");
145
+ expect(findings[0]?.line).toBe(0);
146
+ });
147
+
148
+ it("should return empty array for invalid JSON", () => {
149
+ const findings = parseSarif("not valid json {{{");
150
+ expect(findings).toEqual([]);
151
+ });
152
+
153
+ it("should return empty array for malformed SARIF structure", () => {
154
+ const findings = parseSarif(JSON.stringify({ unexpected: true }));
155
+ expect(findings).toEqual([]);
156
+ });
157
+
158
+ it("should handle results with missing region fields", () => {
159
+ const sarif = JSON.stringify({
160
+ runs: [
161
+ {
162
+ results: [
163
+ {
164
+ ruleId: "rule-y",
165
+ message: { text: "Partial location" },
166
+ locations: [
167
+ {
168
+ physicalLocation: {
169
+ artifactLocation: { uri: "partial.ts" },
170
+ },
171
+ },
172
+ ],
173
+ level: "error",
174
+ },
175
+ ],
176
+ },
177
+ ],
178
+ });
179
+
180
+ const findings = parseSarif(sarif);
181
+ expect(findings.length).toBe(1);
182
+ expect(findings[0]?.file).toBe("partial.ts");
183
+ expect(findings[0]?.line).toBe(0);
184
+ expect(findings[0]?.column).toBeUndefined();
185
+ });
186
+ });
187
+
188
+ // ─── runSemgrep ────────────────────────────────────────────────────────────
189
+
190
+ describe("runSemgrep", () => {
191
+ it("should return skipped when availability is false", async () => {
192
+ const result = await runSemgrep({ available: false });
193
+ expect(result.findings).toEqual([]);
194
+ expect(result.skipped).toBe(true);
195
+ });
196
+
197
+ it("should return correct result shape", async () => {
198
+ // Use available: false to avoid running the actual tool in tests
199
+ const result = await runSemgrep({ available: false });
200
+ expect(result).toHaveProperty("findings");
201
+ expect(result).toHaveProperty("skipped");
202
+ expect(Array.isArray(result.findings)).toBe(true);
203
+ expect(typeof result.skipped).toBe("boolean");
204
+ });
205
+
206
+ it("should accept options without crashing", async () => {
207
+ const result = await runSemgrep({
208
+ files: ["nonexistent-file.ts"],
209
+ config: "auto",
210
+ cwd: "/tmp",
211
+ available: false,
212
+ });
213
+ // Should not throw
214
+ expect(result).toHaveProperty("findings");
215
+ expect(result).toHaveProperty("skipped");
216
+ });
217
+ });