@mainahq/core 0.2.0 → 0.4.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 (38) hide show
  1. package/package.json +4 -1
  2. package/src/ai/__tests__/delegation.test.ts +105 -0
  3. package/src/ai/delegation.ts +111 -0
  4. package/src/ai/try-generate.ts +17 -0
  5. package/src/cloud/__tests__/auth.test.ts +164 -0
  6. package/src/cloud/__tests__/client.test.ts +253 -0
  7. package/src/cloud/auth.ts +232 -0
  8. package/src/cloud/client.ts +190 -0
  9. package/src/cloud/types.ts +106 -0
  10. package/src/context/relevance.ts +5 -0
  11. package/src/context/retrieval.ts +3 -0
  12. package/src/context/semantic.ts +3 -0
  13. package/src/feedback/__tests__/trace-analysis.test.ts +98 -0
  14. package/src/feedback/trace-analysis.ts +153 -0
  15. package/src/index.ts +55 -0
  16. package/src/init/__tests__/init.test.ts +51 -0
  17. package/src/init/index.ts +43 -0
  18. package/src/language/__tests__/detect.test.ts +61 -1
  19. package/src/language/__tests__/profile.test.ts +68 -1
  20. package/src/language/detect.ts +33 -3
  21. package/src/language/profile.ts +67 -2
  22. package/src/ticket/index.ts +5 -0
  23. package/src/verify/__tests__/consistency.test.ts +98 -0
  24. package/src/verify/__tests__/lighthouse.test.ts +215 -0
  25. package/src/verify/__tests__/linters/checkstyle.test.ts +23 -0
  26. package/src/verify/__tests__/linters/dotnet-format.test.ts +18 -0
  27. package/src/verify/__tests__/pipeline.test.ts +21 -2
  28. package/src/verify/__tests__/typecheck.test.ts +160 -0
  29. package/src/verify/__tests__/zap.test.ts +188 -0
  30. package/src/verify/consistency.ts +199 -0
  31. package/src/verify/detect.ts +13 -1
  32. package/src/verify/lighthouse.ts +173 -0
  33. package/src/verify/linters/checkstyle.ts +41 -0
  34. package/src/verify/linters/dotnet-format.ts +37 -0
  35. package/src/verify/pipeline.ts +20 -2
  36. package/src/verify/syntax-guard.ts +8 -0
  37. package/src/verify/typecheck.ts +178 -0
  38. package/src/verify/zap.ts +189 -0
@@ -16,6 +16,7 @@ export interface TicketOptions {
16
16
  body: string;
17
17
  labels?: string[];
18
18
  cwd?: string;
19
+ repo?: string; // Cross-repo: "owner/name" for gh --repo flag
19
20
  }
20
21
 
21
22
  export interface TicketResult {
@@ -156,6 +157,10 @@ export async function createTicket(
156
157
  args.push("--label", options.labels.join(","));
157
158
  }
158
159
 
160
+ if (options.repo) {
161
+ args.push("--repo", options.repo);
162
+ }
163
+
159
164
  const { exitCode, stdout, stderr } = await deps.spawn(args, {
160
165
  cwd: options.cwd,
161
166
  });
@@ -0,0 +1,98 @@
1
+ /**
2
+ * Tests for cross-function consistency checking.
3
+ *
4
+ * Verifies that checkConsistency() catches cases where related functions
5
+ * use inconsistent patterns (e.g., calling isURL but not isIP).
6
+ */
7
+
8
+ import { afterEach, beforeEach, describe, expect, it } from "bun:test";
9
+ import { mkdirSync, rmSync, writeFileSync } from "node:fs";
10
+ import { tmpdir } from "node:os";
11
+ import { join } from "node:path";
12
+
13
+ describe("checkConsistency", () => {
14
+ let testDir: string;
15
+ let mainaDir: string;
16
+
17
+ beforeEach(() => {
18
+ testDir = join(tmpdir(), `maina-consistency-test-${Date.now()}`);
19
+ mainaDir = join(testDir, ".maina");
20
+ mkdirSync(mainaDir, { recursive: true });
21
+ });
22
+
23
+ afterEach(() => {
24
+ rmSync(testDir, { recursive: true, force: true });
25
+ });
26
+
27
+ it("should detect inconsistent validator usage from spec constraints", async () => {
28
+ // Spec says "use isIP for IP hosts"
29
+ writeFileSync(
30
+ join(mainaDir, "constitution.md"),
31
+ "## Rules\n- Always validate IP addresses with isIP when checking hosts\n",
32
+ );
33
+
34
+ const code = [
35
+ "function processUrl(host: string) {",
36
+ " if (isURL(host)) {",
37
+ " // should also call isIP but doesn't",
38
+ " return fetch(host);",
39
+ " }",
40
+ "}",
41
+ ].join("\n");
42
+
43
+ writeFileSync(join(testDir, "handler.ts"), code);
44
+
45
+ const { checkConsistency } = await import("../consistency");
46
+ const result = await checkConsistency(["handler.ts"], testDir, mainaDir);
47
+
48
+ expect(result.rulesChecked).toBeGreaterThan(0);
49
+ });
50
+
51
+ it("should return empty findings when no spec exists", async () => {
52
+ writeFileSync(join(testDir, "clean.ts"), "const x = 1;\n");
53
+
54
+ const { checkConsistency } = await import("../consistency");
55
+ const result = await checkConsistency(
56
+ ["clean.ts"],
57
+ testDir,
58
+ join(testDir, "nonexistent-maina"),
59
+ );
60
+
61
+ expect(result.findings).toEqual([]);
62
+ });
63
+
64
+ it("should detect heuristic pattern: validator used on one path but not another", async () => {
65
+ const code = [
66
+ 'import { isValid } from "./validators";',
67
+ "",
68
+ "function handleA(input: string) {",
69
+ " if (isValid(input)) return process(input);",
70
+ "}",
71
+ "",
72
+ "function handleB(input: string) {",
73
+ " // Missing isValid check — inconsistent with handleA",
74
+ " return process(input);",
75
+ "}",
76
+ ].join("\n");
77
+
78
+ writeFileSync(join(testDir, "handlers.ts"), code);
79
+
80
+ const { checkConsistency } = await import("../consistency");
81
+ const result = await checkConsistency(["handlers.ts"], testDir, mainaDir);
82
+
83
+ // Heuristic mode should at least check for patterns
84
+ expect(result.rulesChecked).toBeGreaterThanOrEqual(0);
85
+ });
86
+
87
+ it("should return ConsistencyResult with correct shape", async () => {
88
+ writeFileSync(join(testDir, "simple.ts"), "const x = 1;\n");
89
+
90
+ const { checkConsistency } = await import("../consistency");
91
+ const result = await checkConsistency(["simple.ts"], testDir, mainaDir);
92
+
93
+ expect(result).toHaveProperty("findings");
94
+ expect(result).toHaveProperty("rulesChecked");
95
+ expect(Array.isArray(result.findings)).toBe(true);
96
+ expect(typeof result.rulesChecked).toBe("number");
97
+ });
98
+ });
@@ -0,0 +1,215 @@
1
+ import { describe, expect, it } from "bun:test";
2
+ import { parseLighthouseJson, runLighthouse } from "../lighthouse";
3
+
4
+ // ─── parseLighthouseJson ──────────────────────────────────────────────────
5
+
6
+ describe("parseLighthouseJson", () => {
7
+ it("should return empty findings when all scores are above thresholds", () => {
8
+ const json = JSON.stringify({
9
+ categories: {
10
+ performance: { score: 0.95 },
11
+ accessibility: { score: 0.92 },
12
+ seo: { score: 0.98 },
13
+ },
14
+ });
15
+
16
+ const result = parseLighthouseJson(json);
17
+ expect(result.findings).toEqual([]);
18
+ expect(result.scores).toEqual({
19
+ performance: 95,
20
+ accessibility: 92,
21
+ seo: 98,
22
+ });
23
+ });
24
+
25
+ it("should generate findings when scores fall below default thresholds", () => {
26
+ const json = JSON.stringify({
27
+ categories: {
28
+ performance: { score: 0.72 },
29
+ accessibility: { score: 0.85 },
30
+ seo: { score: 0.6 },
31
+ },
32
+ });
33
+
34
+ const result = parseLighthouseJson(json);
35
+ expect(result.findings.length).toBe(3);
36
+
37
+ const perfFinding = result.findings.find((f) =>
38
+ f.message.includes("performance"),
39
+ );
40
+ expect(perfFinding?.tool).toBe("lighthouse");
41
+ expect(perfFinding?.severity).toBe("warning");
42
+ expect(perfFinding?.message).toContain("72");
43
+ expect(perfFinding?.message).toContain("90");
44
+
45
+ const a11yFinding = result.findings.find((f) =>
46
+ f.message.includes("accessibility"),
47
+ );
48
+ expect(a11yFinding?.severity).toBe("warning");
49
+
50
+ const seoFinding = result.findings.find((f) => f.message.includes("seo"));
51
+ expect(seoFinding?.severity).toBe("warning");
52
+ });
53
+
54
+ it("should use custom thresholds when provided", () => {
55
+ const json = JSON.stringify({
56
+ categories: {
57
+ performance: { score: 0.72 },
58
+ accessibility: { score: 0.85 },
59
+ seo: { score: 0.6 },
60
+ },
61
+ });
62
+
63
+ const result = parseLighthouseJson(json, {
64
+ performance: 70,
65
+ accessibility: 80,
66
+ seo: 50,
67
+ });
68
+
69
+ // All scores are above custom thresholds
70
+ expect(result.findings).toEqual([]);
71
+ });
72
+
73
+ it("should generate error severity for very low scores", () => {
74
+ const json = JSON.stringify({
75
+ categories: {
76
+ performance: { score: 0.3 },
77
+ accessibility: { score: 0.4 },
78
+ seo: { score: 0.25 },
79
+ },
80
+ });
81
+
82
+ const result = parseLighthouseJson(json);
83
+ for (const finding of result.findings) {
84
+ expect(finding.severity).toBe("error");
85
+ }
86
+ });
87
+
88
+ it("should handle missing categories gracefully", () => {
89
+ const json = JSON.stringify({
90
+ categories: {
91
+ performance: { score: 0.95 },
92
+ },
93
+ });
94
+
95
+ const result = parseLighthouseJson(json);
96
+ expect(result.findings).toEqual([]);
97
+ expect(result.scores).toEqual({ performance: 95 });
98
+ });
99
+
100
+ it("should return empty findings for invalid JSON", () => {
101
+ const result = parseLighthouseJson("not valid json {{{");
102
+ expect(result.findings).toEqual([]);
103
+ expect(result.scores).toEqual({});
104
+ });
105
+
106
+ it("should return empty findings for malformed structure", () => {
107
+ const result = parseLighthouseJson(JSON.stringify({ unexpected: true }));
108
+ expect(result.findings).toEqual([]);
109
+ expect(result.scores).toEqual({});
110
+ });
111
+
112
+ it("should handle categories with null scores", () => {
113
+ const json = JSON.stringify({
114
+ categories: {
115
+ performance: { score: null },
116
+ accessibility: { score: 0.95 },
117
+ },
118
+ });
119
+
120
+ const result = parseLighthouseJson(json);
121
+ expect(result.scores).toEqual({ accessibility: 95 });
122
+ });
123
+
124
+ it("should include the URL in findings when provided", () => {
125
+ const json = JSON.stringify({
126
+ requestedUrl: "https://example.com",
127
+ categories: {
128
+ performance: { score: 0.5 },
129
+ },
130
+ });
131
+
132
+ const result = parseLighthouseJson(json);
133
+ expect(result.findings.length).toBe(1);
134
+ expect(result.findings[0]?.file).toBe("https://example.com");
135
+ });
136
+
137
+ it("should handle best-practices category", () => {
138
+ const json = JSON.stringify({
139
+ categories: {
140
+ "best-practices": { score: 0.7 },
141
+ },
142
+ });
143
+
144
+ const result = parseLighthouseJson(json);
145
+ // best-practices is not in default thresholds, so no findings
146
+ expect(result.findings).toEqual([]);
147
+ expect(result.scores).toEqual({ "best-practices": 70 });
148
+ });
149
+
150
+ it("should generate findings for best-practices when threshold set", () => {
151
+ const json = JSON.stringify({
152
+ categories: {
153
+ "best-practices": { score: 0.7 },
154
+ },
155
+ });
156
+
157
+ const result = parseLighthouseJson(json, {
158
+ "best-practices": 90,
159
+ });
160
+ expect(result.findings.length).toBe(1);
161
+ expect(result.findings[0]?.message).toContain("best-practices");
162
+ });
163
+ });
164
+
165
+ // ─── runLighthouse ────────────────────────────────────────────────────────
166
+
167
+ describe("runLighthouse", () => {
168
+ it("should skip when lighthouse is not available", async () => {
169
+ const result = await runLighthouse({
170
+ url: "https://example.com",
171
+ cwd: "/tmp",
172
+ available: false,
173
+ });
174
+ expect(result.findings).toEqual([]);
175
+ expect(result.skipped).toBe(true);
176
+ expect(result.scores).toEqual({});
177
+ });
178
+
179
+ it("should skip when no URL is provided", async () => {
180
+ const result = await runLighthouse({
181
+ url: "",
182
+ cwd: "/tmp",
183
+ available: true,
184
+ });
185
+ expect(result.findings).toEqual([]);
186
+ expect(result.skipped).toBe(true);
187
+ expect(result.scores).toEqual({});
188
+ });
189
+
190
+ it("should return correct result shape", async () => {
191
+ const result = await runLighthouse({
192
+ url: "https://example.com",
193
+ cwd: "/tmp",
194
+ available: false,
195
+ });
196
+ expect(result).toHaveProperty("findings");
197
+ expect(result).toHaveProperty("skipped");
198
+ expect(result).toHaveProperty("scores");
199
+ expect(Array.isArray(result.findings)).toBe(true);
200
+ expect(typeof result.skipped).toBe("boolean");
201
+ expect(typeof result.scores).toBe("object");
202
+ });
203
+
204
+ it("should accept custom thresholds without crashing", async () => {
205
+ const result = await runLighthouse({
206
+ url: "https://example.com",
207
+ cwd: "/tmp",
208
+ available: false,
209
+ thresholds: { performance: 80, accessibility: 85 },
210
+ });
211
+ expect(result).toHaveProperty("findings");
212
+ expect(result).toHaveProperty("skipped");
213
+ expect(result).toHaveProperty("scores");
214
+ });
215
+ });
@@ -0,0 +1,23 @@
1
+ import { describe, expect, it } from "bun:test";
2
+ import { parseCheckstyleOutput } from "../../linters/checkstyle";
3
+
4
+ describe("parseCheckstyleOutput", () => {
5
+ it("should parse checkstyle XML", () => {
6
+ const xml = `<?xml version="1.0" encoding="UTF-8"?>
7
+ <checkstyle>
8
+ <file name="src/Main.java">
9
+ <error line="10" column="5" severity="error" message="Missing Javadoc comment" source="com.puppycrawl.tools.checkstyle.checks.javadoc"/>
10
+ <error line="20" severity="warning" message="Line is longer than 120 characters" source="com.puppycrawl.tools.checkstyle.checks.sizes"/>
11
+ </file>
12
+ </checkstyle>`;
13
+ const diagnostics = parseCheckstyleOutput(xml);
14
+ expect(diagnostics).toHaveLength(2);
15
+ expect(diagnostics[0]?.file).toBe("src/Main.java");
16
+ expect(diagnostics[0]?.severity).toBe("error");
17
+ expect(diagnostics[1]?.severity).toBe("warning");
18
+ });
19
+
20
+ it("should handle empty XML", () => {
21
+ expect(parseCheckstyleOutput("")).toHaveLength(0);
22
+ });
23
+ });
@@ -0,0 +1,18 @@
1
+ import { describe, expect, it } from "bun:test";
2
+ import { parseDotnetFormatOutput } from "../../linters/dotnet-format";
3
+
4
+ describe("parseDotnetFormatOutput", () => {
5
+ it("should parse dotnet format diagnostics", () => {
6
+ const output = `Program.cs(10,5): warning CS1234: Unused variable 'x'
7
+ Service.cs(20,1): error CS0246: The type or namespace name 'Foo' could not be found`;
8
+ const diagnostics = parseDotnetFormatOutput(output);
9
+ expect(diagnostics).toHaveLength(2);
10
+ expect(diagnostics[0]?.file).toBe("Program.cs");
11
+ expect(diagnostics[0]?.severity).toBe("warning");
12
+ expect(diagnostics[1]?.severity).toBe("error");
13
+ });
14
+
15
+ it("should handle empty output", () => {
16
+ expect(parseDotnetFormatOutput("")).toHaveLength(0);
17
+ });
18
+ });
@@ -33,6 +33,11 @@ let mockStagedFiles: string[] = ["src/app.ts"];
33
33
  let callOrder: string[] = [];
34
34
 
35
35
  // Mock the modules
36
+ // NOTE: These mocks are intentionally minimal — they only export what pipeline.ts
37
+ // needs. Tests MUST be run via `bun run test` (scripts/test-isolated.ts) which
38
+ // runs each test file in its own subprocess, preventing mock.module() bleed.
39
+ // Running `bun test` directly (single process) will cause cross-file mock leaks.
40
+
36
41
  mock.module("../syntax-guard", () => ({
37
42
  syntaxGuard: async (..._args: unknown[]) => {
38
43
  callOrder.push("syntaxGuard");
@@ -140,6 +145,20 @@ mock.module("../ai-review", () => ({
140
145
  },
141
146
  }));
142
147
 
148
+ mock.module("../typecheck", () => ({
149
+ runTypecheck: async (..._args: unknown[]) => {
150
+ callOrder.push("runTypecheck");
151
+ return { findings: [], duration: 0, tool: "tsc", skipped: true };
152
+ },
153
+ }));
154
+
155
+ mock.module("../consistency", () => ({
156
+ checkConsistency: async (..._args: unknown[]) => {
157
+ callOrder.push("checkConsistency");
158
+ return { findings: [], rulesChecked: 0 };
159
+ },
160
+ }));
161
+
143
162
  mock.module("../../language/detect", () => ({
144
163
  detectLanguages: (..._args: unknown[]) => ["typescript"],
145
164
  }));
@@ -253,8 +272,8 @@ describe("VerifyPipeline", () => {
253
272
  expect(callOrder).toContain("runTrivy");
254
273
  expect(callOrder).toContain("runSecretlint");
255
274
 
256
- // 5 tool reports (slop + semgrep + trivy + secretlint + ai-review)
257
- expect(result.tools).toHaveLength(8);
275
+ // 10 tool reports (slop + semgrep + trivy + secretlint + sonarqube + stryker + diff-cover + typecheck + consistency + ai-review)
276
+ expect(result.tools).toHaveLength(10);
258
277
  expect(result.findings).toHaveLength(3);
259
278
  });
260
279
 
@@ -0,0 +1,160 @@
1
+ /**
2
+ * Tests for built-in type checking in the verify pipeline.
3
+ *
4
+ * Verifies that runTypecheck() spawns the correct language-specific
5
+ * type checker and parses its output into Finding[].
6
+ */
7
+
8
+ import { afterEach, beforeEach, describe, expect, it } from "bun:test";
9
+ import { mkdirSync, rmSync, writeFileSync } from "node:fs";
10
+ import { tmpdir } from "node:os";
11
+ import { join } from "node:path";
12
+
13
+ describe("runTypecheck", () => {
14
+ let testDir: string;
15
+
16
+ beforeEach(() => {
17
+ testDir = join(tmpdir(), `maina-typecheck-test-${Date.now()}`);
18
+ mkdirSync(testDir, { recursive: true });
19
+ });
20
+
21
+ afterEach(() => {
22
+ rmSync(testDir, { recursive: true, force: true });
23
+ });
24
+
25
+ it("should parse tsc --noEmit output into Finding[]", async () => {
26
+ // Create a tsconfig.json and install typescript locally
27
+ writeFileSync(
28
+ join(testDir, "tsconfig.json"),
29
+ JSON.stringify({
30
+ compilerOptions: { strict: true, noEmit: true },
31
+ include: ["*.ts"],
32
+ }),
33
+ );
34
+ // Install typescript locally so tsc is in node_modules/.bin
35
+ const install = Bun.spawnSync(["bun", "add", "typescript"], {
36
+ cwd: testDir,
37
+ });
38
+ if (install.exitCode !== 0) {
39
+ // Skip if we can't install (CI without network, etc.)
40
+ return;
41
+ }
42
+
43
+ // Create a file with a type error
44
+ writeFileSync(
45
+ join(testDir, "bad.ts"),
46
+ 'const x: number = "not a number";\n',
47
+ );
48
+
49
+ const { runTypecheck } = await import("../typecheck");
50
+ const result = await runTypecheck(["bad.ts"], testDir);
51
+
52
+ expect(result.findings.length).toBeGreaterThan(0);
53
+ const first = result.findings[0];
54
+ if (!first) throw new Error("Expected at least one finding");
55
+ expect(first.tool).toBe("tsc");
56
+ expect(first.severity).toBe("error");
57
+ expect(first.file).toContain("bad.ts");
58
+ expect(first.line).toBeGreaterThan(0);
59
+ expect(result.duration).toBeGreaterThanOrEqual(0);
60
+ });
61
+
62
+ it("should return empty findings for clean file", async () => {
63
+ writeFileSync(
64
+ join(testDir, "tsconfig.json"),
65
+ JSON.stringify({
66
+ compilerOptions: { strict: true, noEmit: true },
67
+ include: ["*.ts"],
68
+ }),
69
+ );
70
+ Bun.spawnSync(["bun", "add", "typescript"], { cwd: testDir });
71
+
72
+ writeFileSync(join(testDir, "good.ts"), "const x: number = 42;\n");
73
+
74
+ const { runTypecheck } = await import("../typecheck");
75
+ const result = await runTypecheck(["good.ts"], testDir);
76
+
77
+ expect(result.findings).toEqual([]);
78
+ expect(result.tool).toBe("tsc");
79
+ });
80
+
81
+ it("should skip with info when tsc is not found", async () => {
82
+ // Use a non-existent tool path
83
+ const { runTypecheck } = await import("../typecheck");
84
+ const result = await runTypecheck(["file.ts"], testDir, {
85
+ command: "nonexistent-tsc-binary",
86
+ });
87
+
88
+ expect(result.findings).toEqual([]);
89
+ expect(result.skipped).toBe(true);
90
+ });
91
+
92
+ it("should detect language and use appropriate checker", async () => {
93
+ const { getTypecheckCommand } = await import("../typecheck");
94
+
95
+ expect(getTypecheckCommand("typescript")).toEqual(
96
+ expect.objectContaining({ tool: "tsc" }),
97
+ );
98
+ expect(getTypecheckCommand("python")).toEqual(
99
+ expect.objectContaining({ tool: "mypy" }),
100
+ );
101
+ expect(getTypecheckCommand("go")).toEqual(
102
+ expect.objectContaining({ tool: "go-vet" }),
103
+ );
104
+ expect(getTypecheckCommand("rust")).toEqual(
105
+ expect.objectContaining({ tool: "cargo-check" }),
106
+ );
107
+ expect(getTypecheckCommand("csharp")).toEqual(
108
+ expect.objectContaining({ tool: "dotnet-build" }),
109
+ );
110
+ expect(getTypecheckCommand("java")).toEqual(
111
+ expect.objectContaining({ tool: "javac" }),
112
+ );
113
+ });
114
+
115
+ it("should parse multiple errors from tsc output", async () => {
116
+ const { parseTscOutput } = await import("../typecheck");
117
+
118
+ const tscOutput = [
119
+ "src/foo.ts(10,5): error TS2322: Type 'string' is not assignable to type 'number'.",
120
+ "src/bar.ts(3,1): error TS2304: Cannot find name 'x'.",
121
+ ].join("\n");
122
+
123
+ const findings = parseTscOutput(tscOutput);
124
+
125
+ expect(findings).toHaveLength(2);
126
+ expect(findings[0]).toEqual({
127
+ tool: "tsc",
128
+ file: "src/foo.ts",
129
+ line: 10,
130
+ column: 5,
131
+ message: "TS2322: Type 'string' is not assignable to type 'number'.",
132
+ severity: "error",
133
+ ruleId: "TS2322",
134
+ });
135
+ expect(findings[1]).toEqual({
136
+ tool: "tsc",
137
+ file: "src/bar.ts",
138
+ line: 3,
139
+ column: 1,
140
+ message: "TS2304: Cannot find name 'x'.",
141
+ severity: "error",
142
+ ruleId: "TS2304",
143
+ });
144
+ });
145
+
146
+ it("should return empty for empty tsc output", async () => {
147
+ const { parseTscOutput } = await import("../typecheck");
148
+ const findings = parseTscOutput("");
149
+ expect(findings).toEqual([]);
150
+ });
151
+
152
+ it("should handle no tsconfig.json gracefully", async () => {
153
+ // No tsconfig.json in testDir
154
+ const { runTypecheck } = await import("../typecheck");
155
+ const result = await runTypecheck(["file.ts"], testDir);
156
+
157
+ // Should not crash, should skip or return empty
158
+ expect(result.skipped).toBe(true);
159
+ });
160
+ });