@mainahq/core 1.0.2 → 1.1.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/package.json +1 -1
- package/src/ai/__tests__/availability.test.ts +131 -0
- package/src/ai/__tests__/delegation.test.ts +55 -1
- package/src/ai/availability.ts +23 -0
- package/src/ai/delegation.ts +5 -3
- package/src/context/__tests__/budget.test.ts +29 -6
- package/src/context/__tests__/engine.test.ts +1 -0
- package/src/context/__tests__/selector.test.ts +23 -3
- package/src/context/__tests__/wiki.test.ts +349 -0
- package/src/context/budget.ts +12 -8
- package/src/context/engine.ts +37 -0
- package/src/context/selector.ts +30 -4
- package/src/context/wiki.ts +296 -0
- package/src/db/index.ts +12 -0
- package/src/feedback/__tests__/capture.test.ts +166 -0
- package/src/feedback/__tests__/signals.test.ts +144 -0
- package/src/feedback/__tests__/tmp-capture-1775575256633-lah0etnzlj/feedback.db +0 -0
- package/src/feedback/__tests__/tmp-capture-1775575256640-2xmjme4qraa/feedback.db +0 -0
- package/src/feedback/capture.ts +102 -0
- package/src/feedback/signals.ts +68 -0
- package/src/index.ts +108 -1
- package/src/init/__tests__/init.test.ts +477 -18
- package/src/init/index.ts +419 -13
- package/src/language/__tests__/__fixtures__/detect/composer.lock +1 -0
- package/src/prompts/defaults/index.ts +3 -1
- package/src/prompts/defaults/wiki-compile.md +20 -0
- package/src/prompts/defaults/wiki-query.md +18 -0
- package/src/stats/__tests__/tool-usage.test.ts +133 -0
- package/src/stats/tracker.ts +92 -0
- package/src/verify/__tests__/builtin.test.ts +270 -0
- package/src/verify/__tests__/pipeline.test.ts +11 -8
- package/src/verify/builtin.ts +350 -0
- package/src/verify/pipeline.ts +32 -2
- package/src/verify/tools/__tests__/wiki-lint.test.ts +784 -0
- package/src/verify/tools/wiki-lint-runner.ts +38 -0
- package/src/verify/tools/wiki-lint.ts +898 -0
- package/src/wiki/__tests__/compiler.test.ts +389 -0
- package/src/wiki/__tests__/extractors/code.test.ts +99 -0
- package/src/wiki/__tests__/extractors/decision.test.ts +323 -0
- package/src/wiki/__tests__/extractors/feature.test.ts +186 -0
- package/src/wiki/__tests__/extractors/workflow.test.ts +131 -0
- package/src/wiki/__tests__/graph.test.ts +344 -0
- package/src/wiki/__tests__/hooks.test.ts +119 -0
- package/src/wiki/__tests__/indexer.test.ts +285 -0
- package/src/wiki/__tests__/linker.test.ts +230 -0
- package/src/wiki/__tests__/louvain.test.ts +229 -0
- package/src/wiki/__tests__/query.test.ts +316 -0
- package/src/wiki/__tests__/schema.test.ts +114 -0
- package/src/wiki/__tests__/signals.test.ts +474 -0
- package/src/wiki/__tests__/state.test.ts +168 -0
- package/src/wiki/__tests__/tracking.test.ts +118 -0
- package/src/wiki/__tests__/types.test.ts +387 -0
- package/src/wiki/compiler.ts +1075 -0
- package/src/wiki/extractors/code.ts +90 -0
- package/src/wiki/extractors/decision.ts +217 -0
- package/src/wiki/extractors/feature.ts +206 -0
- package/src/wiki/extractors/workflow.ts +112 -0
- package/src/wiki/graph.ts +445 -0
- package/src/wiki/hooks.ts +49 -0
- package/src/wiki/indexer.ts +105 -0
- package/src/wiki/linker.ts +117 -0
- package/src/wiki/louvain.ts +190 -0
- package/src/wiki/prompts/compile-architecture.md +59 -0
- package/src/wiki/prompts/compile-decision.md +66 -0
- package/src/wiki/prompts/compile-entity.md +56 -0
- package/src/wiki/prompts/compile-feature.md +60 -0
- package/src/wiki/prompts/compile-module.md +42 -0
- package/src/wiki/prompts/wiki-query.md +25 -0
- package/src/wiki/query.ts +338 -0
- package/src/wiki/schema.ts +111 -0
- package/src/wiki/signals.ts +368 -0
- package/src/wiki/state.ts +89 -0
- package/src/wiki/tracking.ts +30 -0
- package/src/wiki/types.ts +169 -0
- package/src/workflow/context.ts +26 -0
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, test } from "bun:test";
|
|
2
|
+
import { mkdirSync, rmSync } from "node:fs";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { getStatsDb } from "../../db/index";
|
|
5
|
+
import { getToolUsageStats, trackToolUsage } from "../tracker";
|
|
6
|
+
|
|
7
|
+
let tmpDir: string;
|
|
8
|
+
|
|
9
|
+
beforeEach(() => {
|
|
10
|
+
tmpDir = join(
|
|
11
|
+
import.meta.dir,
|
|
12
|
+
`tmp-tool-usage-${Date.now()}-${Math.random().toString(36).slice(2)}`,
|
|
13
|
+
);
|
|
14
|
+
mkdirSync(tmpDir, { recursive: true });
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
afterEach(() => {
|
|
18
|
+
try {
|
|
19
|
+
rmSync(tmpDir, { recursive: true, force: true });
|
|
20
|
+
} catch {
|
|
21
|
+
/* ignore */
|
|
22
|
+
}
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
describe("tool_usage table", () => {
|
|
26
|
+
test("getStatsDb creates tool_usage table", () => {
|
|
27
|
+
const result = getStatsDb(tmpDir);
|
|
28
|
+
expect(result.ok).toBe(true);
|
|
29
|
+
if (!result.ok) return;
|
|
30
|
+
const { db } = result.value;
|
|
31
|
+
const tables = db
|
|
32
|
+
.query(
|
|
33
|
+
"SELECT name FROM sqlite_master WHERE type='table' AND name='tool_usage'",
|
|
34
|
+
)
|
|
35
|
+
.all();
|
|
36
|
+
expect(tables).toHaveLength(1);
|
|
37
|
+
});
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
describe("trackToolUsage", () => {
|
|
41
|
+
test("inserts a tool usage row", () => {
|
|
42
|
+
trackToolUsage(tmpDir, {
|
|
43
|
+
tool: "reviewCode",
|
|
44
|
+
inputHash: "abc123",
|
|
45
|
+
durationMs: 150,
|
|
46
|
+
cacheHit: false,
|
|
47
|
+
});
|
|
48
|
+
const result = getStatsDb(tmpDir);
|
|
49
|
+
expect(result.ok).toBe(true);
|
|
50
|
+
if (!result.ok) return;
|
|
51
|
+
const { db } = result.value;
|
|
52
|
+
const rows = db.query("SELECT * FROM tool_usage").all() as Array<{
|
|
53
|
+
tool: string;
|
|
54
|
+
duration_ms: number;
|
|
55
|
+
cache_hit: number;
|
|
56
|
+
}>;
|
|
57
|
+
expect(rows).toHaveLength(1);
|
|
58
|
+
const row = rows[0];
|
|
59
|
+
expect(row).toBeDefined();
|
|
60
|
+
expect(row?.tool).toBe("reviewCode");
|
|
61
|
+
expect(row?.duration_ms).toBe(150);
|
|
62
|
+
expect(row?.cache_hit).toBe(0);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
test("tracks cache hit", () => {
|
|
66
|
+
trackToolUsage(tmpDir, {
|
|
67
|
+
tool: "reviewCode",
|
|
68
|
+
inputHash: "abc123",
|
|
69
|
+
durationMs: 0,
|
|
70
|
+
cacheHit: true,
|
|
71
|
+
});
|
|
72
|
+
const result = getStatsDb(tmpDir);
|
|
73
|
+
if (!result.ok) return;
|
|
74
|
+
const { db } = result.value;
|
|
75
|
+
const rows = db.query("SELECT * FROM tool_usage WHERE cache_hit = 1").all();
|
|
76
|
+
expect(rows).toHaveLength(1);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
test("stores workflow_id when provided", () => {
|
|
80
|
+
trackToolUsage(tmpDir, {
|
|
81
|
+
tool: "verify",
|
|
82
|
+
inputHash: "def456",
|
|
83
|
+
durationMs: 200,
|
|
84
|
+
cacheHit: false,
|
|
85
|
+
workflowId: "wf-123",
|
|
86
|
+
});
|
|
87
|
+
const result = getStatsDb(tmpDir);
|
|
88
|
+
if (!result.ok) return;
|
|
89
|
+
const { db } = result.value;
|
|
90
|
+
const row = db.query("SELECT workflow_id FROM tool_usage").get() as {
|
|
91
|
+
workflow_id: string;
|
|
92
|
+
} | null;
|
|
93
|
+
expect(row?.workflow_id).toBe("wf-123");
|
|
94
|
+
});
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
describe("getToolUsageStats", () => {
|
|
98
|
+
test("returns stats across multiple tool calls", () => {
|
|
99
|
+
trackToolUsage(tmpDir, {
|
|
100
|
+
tool: "reviewCode",
|
|
101
|
+
inputHash: "a",
|
|
102
|
+
durationMs: 100,
|
|
103
|
+
cacheHit: false,
|
|
104
|
+
});
|
|
105
|
+
trackToolUsage(tmpDir, {
|
|
106
|
+
tool: "reviewCode",
|
|
107
|
+
inputHash: "b",
|
|
108
|
+
durationMs: 200,
|
|
109
|
+
cacheHit: true,
|
|
110
|
+
});
|
|
111
|
+
trackToolUsage(tmpDir, {
|
|
112
|
+
tool: "verify",
|
|
113
|
+
inputHash: "c",
|
|
114
|
+
durationMs: 50,
|
|
115
|
+
cacheHit: false,
|
|
116
|
+
});
|
|
117
|
+
const stats = getToolUsageStats(tmpDir);
|
|
118
|
+
expect(stats.totalCalls).toBe(3);
|
|
119
|
+
expect(stats.cacheHits).toBe(1);
|
|
120
|
+
expect(stats.cacheHitRate).toBeCloseTo(1 / 3, 2);
|
|
121
|
+
expect(stats.byTool.reviewCode).toBeDefined();
|
|
122
|
+
expect(stats.byTool.reviewCode?.calls).toBe(2);
|
|
123
|
+
expect(stats.byTool.reviewCode?.cacheHits).toBe(1);
|
|
124
|
+
expect(stats.byTool.verify).toBeDefined();
|
|
125
|
+
expect(stats.byTool.verify?.calls).toBe(1);
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
test("returns empty stats when no data", () => {
|
|
129
|
+
const stats = getToolUsageStats(tmpDir);
|
|
130
|
+
expect(stats.totalCalls).toBe(0);
|
|
131
|
+
expect(stats.cacheHitRate).toBe(0);
|
|
132
|
+
});
|
|
133
|
+
});
|
package/src/stats/tracker.ts
CHANGED
|
@@ -490,3 +490,95 @@ export function getSkipRate(
|
|
|
490
490
|
return { ok: false, error: e instanceof Error ? e.message : String(e) };
|
|
491
491
|
}
|
|
492
492
|
}
|
|
493
|
+
|
|
494
|
+
export interface ToolUsageInput {
|
|
495
|
+
tool: string;
|
|
496
|
+
inputHash: string;
|
|
497
|
+
durationMs: number;
|
|
498
|
+
cacheHit: boolean;
|
|
499
|
+
workflowId?: string;
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
export interface ToolUsageStats {
|
|
503
|
+
totalCalls: number;
|
|
504
|
+
cacheHits: number;
|
|
505
|
+
cacheHitRate: number;
|
|
506
|
+
byTool: Record<
|
|
507
|
+
string,
|
|
508
|
+
{ calls: number; cacheHits: number; avgDurationMs: number }
|
|
509
|
+
>;
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
export function trackToolUsage(mainaDir: string, input: ToolUsageInput): void {
|
|
513
|
+
try {
|
|
514
|
+
const dbResult = getStatsDb(mainaDir);
|
|
515
|
+
if (!dbResult.ok) return;
|
|
516
|
+
const { db } = dbResult.value;
|
|
517
|
+
const id = crypto.randomUUID();
|
|
518
|
+
const timestamp = new Date().toISOString();
|
|
519
|
+
db.prepare(
|
|
520
|
+
`INSERT INTO tool_usage (id, tool, input_hash, duration_ms, cache_hit, timestamp, workflow_id)
|
|
521
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)`,
|
|
522
|
+
).run(
|
|
523
|
+
id,
|
|
524
|
+
input.tool,
|
|
525
|
+
input.inputHash,
|
|
526
|
+
input.durationMs,
|
|
527
|
+
input.cacheHit ? 1 : 0,
|
|
528
|
+
timestamp,
|
|
529
|
+
input.workflowId ?? null,
|
|
530
|
+
);
|
|
531
|
+
} catch {
|
|
532
|
+
// Never throw from stats tracking
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
export function getToolUsageStats(mainaDir: string): ToolUsageStats {
|
|
537
|
+
const empty: ToolUsageStats = {
|
|
538
|
+
totalCalls: 0,
|
|
539
|
+
cacheHits: 0,
|
|
540
|
+
cacheHitRate: 0,
|
|
541
|
+
byTool: {},
|
|
542
|
+
};
|
|
543
|
+
try {
|
|
544
|
+
const dbResult = getStatsDb(mainaDir);
|
|
545
|
+
if (!dbResult.ok) return empty;
|
|
546
|
+
const { db } = dbResult.value;
|
|
547
|
+
const totals = db
|
|
548
|
+
.query(
|
|
549
|
+
`SELECT COUNT(*) as total, SUM(CASE WHEN cache_hit = 1 THEN 1 ELSE 0 END) as hits FROM tool_usage`,
|
|
550
|
+
)
|
|
551
|
+
.get() as { total: number; hits: number } | null;
|
|
552
|
+
if (!totals || totals.total === 0) return empty;
|
|
553
|
+
const byToolRows = db
|
|
554
|
+
.query(
|
|
555
|
+
`SELECT tool, COUNT(*) as calls, SUM(CASE WHEN cache_hit = 1 THEN 1 ELSE 0 END) as cache_hits, AVG(duration_ms) as avg_duration
|
|
556
|
+
FROM tool_usage GROUP BY tool`,
|
|
557
|
+
)
|
|
558
|
+
.all() as Array<{
|
|
559
|
+
tool: string;
|
|
560
|
+
calls: number;
|
|
561
|
+
cache_hits: number;
|
|
562
|
+
avg_duration: number;
|
|
563
|
+
}>;
|
|
564
|
+
const byTool: Record<
|
|
565
|
+
string,
|
|
566
|
+
{ calls: number; cacheHits: number; avgDurationMs: number }
|
|
567
|
+
> = {};
|
|
568
|
+
for (const row of byToolRows) {
|
|
569
|
+
byTool[row.tool] = {
|
|
570
|
+
calls: row.calls,
|
|
571
|
+
cacheHits: row.cache_hits,
|
|
572
|
+
avgDurationMs: Math.round(row.avg_duration),
|
|
573
|
+
};
|
|
574
|
+
}
|
|
575
|
+
return {
|
|
576
|
+
totalCalls: totals.total,
|
|
577
|
+
cacheHits: totals.hits,
|
|
578
|
+
cacheHitRate: totals.total > 0 ? totals.hits / totals.total : 0,
|
|
579
|
+
byTool,
|
|
580
|
+
};
|
|
581
|
+
} catch {
|
|
582
|
+
return empty;
|
|
583
|
+
}
|
|
584
|
+
}
|
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for built-in verify checks.
|
|
3
|
+
*
|
|
4
|
+
* Each check is a pure function: (filePath, content) => Finding[].
|
|
5
|
+
* No I/O, no side effects — just string analysis.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { describe, expect, it } from "bun:test";
|
|
9
|
+
import {
|
|
10
|
+
checkAnyType,
|
|
11
|
+
checkConsoleLogs,
|
|
12
|
+
checkEmptyCatch,
|
|
13
|
+
checkFileSize,
|
|
14
|
+
checkSecrets,
|
|
15
|
+
checkTodoComments,
|
|
16
|
+
checkUnusedImports,
|
|
17
|
+
runBuiltinChecks,
|
|
18
|
+
} from "../builtin";
|
|
19
|
+
|
|
20
|
+
// ─── checkConsoleLogs ────────────────────────────────────────────────────
|
|
21
|
+
|
|
22
|
+
describe("checkConsoleLogs", () => {
|
|
23
|
+
it("detects console.log in a .ts file", () => {
|
|
24
|
+
const content = `const x = 1;\nconsole.log(x);\nconst y = 2;\n`;
|
|
25
|
+
const findings = checkConsoleLogs("src/app.ts", content);
|
|
26
|
+
expect(findings).toHaveLength(1);
|
|
27
|
+
expect(findings[0]?.line).toBe(2);
|
|
28
|
+
expect(findings[0]?.severity).toBe("warning");
|
|
29
|
+
expect(findings[0]?.ruleId).toBe("no-console-log");
|
|
30
|
+
expect(findings[0]?.tool).toBe("builtin");
|
|
31
|
+
expect(findings[0]?.file).toBe("src/app.ts");
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it("skips .test.ts files", () => {
|
|
35
|
+
const content = `console.log("debug");\n`;
|
|
36
|
+
const findings = checkConsoleLogs("src/app.test.ts", content);
|
|
37
|
+
expect(findings).toHaveLength(0);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it("skips .spec.ts files", () => {
|
|
41
|
+
const content = `console.log("debug");\n`;
|
|
42
|
+
const findings = checkConsoleLogs("src/app.spec.ts", content);
|
|
43
|
+
expect(findings).toHaveLength(0);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it("skips files in __tests__ directories", () => {
|
|
47
|
+
const content = `console.log("debug");\n`;
|
|
48
|
+
const findings = checkConsoleLogs("src/__tests__/app.ts", content);
|
|
49
|
+
expect(findings).toHaveLength(0);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it("detects console.warn and console.error too", () => {
|
|
53
|
+
const content = `console.warn("w");\nconsole.error("e");\n`;
|
|
54
|
+
const findings = checkConsoleLogs("src/app.ts", content);
|
|
55
|
+
expect(findings).toHaveLength(2);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it("returns empty for clean files", () => {
|
|
59
|
+
const content = `const x = 1;\nconst y = 2;\n`;
|
|
60
|
+
const findings = checkConsoleLogs("src/app.ts", content);
|
|
61
|
+
expect(findings).toHaveLength(0);
|
|
62
|
+
});
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
// ─── checkTodoComments ───────────────────────────────────────────────────
|
|
66
|
+
|
|
67
|
+
describe("checkTodoComments", () => {
|
|
68
|
+
it("detects TODO with correct line numbers", () => {
|
|
69
|
+
const content = `const x = 1;\n// TODO: fix this\nconst y = 2;\n// FIXME: broken\n`;
|
|
70
|
+
const findings = checkTodoComments("src/app.ts", content);
|
|
71
|
+
expect(findings).toHaveLength(2);
|
|
72
|
+
expect(findings[0]?.line).toBe(2);
|
|
73
|
+
expect(findings[0]?.message).toContain("TODO");
|
|
74
|
+
expect(findings[1]?.line).toBe(4);
|
|
75
|
+
expect(findings[1]?.message).toContain("FIXME");
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it("detects HACK comments", () => {
|
|
79
|
+
const content = `// HACK: temporary workaround\n`;
|
|
80
|
+
const findings = checkTodoComments("src/app.ts", content);
|
|
81
|
+
expect(findings).toHaveLength(1);
|
|
82
|
+
expect(findings[0]?.ruleId).toBe("todo-comment");
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it("returns empty when no markers present", () => {
|
|
86
|
+
const content = `const x = 1;\n// This is a normal comment\n`;
|
|
87
|
+
const findings = checkTodoComments("src/app.ts", content);
|
|
88
|
+
expect(findings).toHaveLength(0);
|
|
89
|
+
});
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
// ─── checkFileSize ───────────────────────────────────────────────────────
|
|
93
|
+
|
|
94
|
+
describe("checkFileSize", () => {
|
|
95
|
+
it("flags files over 500 lines", () => {
|
|
96
|
+
const lines = Array.from({ length: 501 }, (_, i) => `const x${i} = ${i};`);
|
|
97
|
+
const content = lines.join("\n");
|
|
98
|
+
const findings = checkFileSize("src/big.ts", content);
|
|
99
|
+
expect(findings).toHaveLength(1);
|
|
100
|
+
expect(findings[0]?.severity).toBe("warning");
|
|
101
|
+
expect(findings[0]?.ruleId).toBe("file-too-long");
|
|
102
|
+
expect(findings[0]?.message).toContain("501");
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it("does not flag files with exactly 500 lines", () => {
|
|
106
|
+
const lines = Array.from({ length: 500 }, (_, i) => `const x${i} = ${i};`);
|
|
107
|
+
const content = lines.join("\n");
|
|
108
|
+
const findings = checkFileSize("src/ok.ts", content);
|
|
109
|
+
expect(findings).toHaveLength(0);
|
|
110
|
+
});
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
// ─── checkSecrets ────────────────────────────────────────────────────────
|
|
114
|
+
|
|
115
|
+
describe("checkSecrets", () => {
|
|
116
|
+
it("detects hardcoded password patterns", () => {
|
|
117
|
+
const content = `const config = {\n password="s3cret123"\n};\n`;
|
|
118
|
+
const findings = checkSecrets("src/config.ts", content);
|
|
119
|
+
expect(findings).toHaveLength(1);
|
|
120
|
+
expect(findings[0]?.severity).toBe("error");
|
|
121
|
+
expect(findings[0]?.ruleId).toBe("hardcoded-secret");
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
it("detects api_key patterns", () => {
|
|
125
|
+
const content = `const api_key = "abc123def456";\n`;
|
|
126
|
+
const findings = checkSecrets("src/config.ts", content);
|
|
127
|
+
expect(findings).toHaveLength(1);
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
it("ignores variable references (not hardcoded)", () => {
|
|
131
|
+
const content = `const password = process.env.PASSWORD;\n`;
|
|
132
|
+
const findings = checkSecrets("src/config.ts", content);
|
|
133
|
+
expect(findings).toHaveLength(0);
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
it("detects token patterns", () => {
|
|
137
|
+
const content = `const token = "ghp_abc123def456";\n`;
|
|
138
|
+
const findings = checkSecrets("src/config.ts", content);
|
|
139
|
+
expect(findings).toHaveLength(1);
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
it("detects secret patterns", () => {
|
|
143
|
+
const content = `secret="mySecretValue123";\n`;
|
|
144
|
+
const findings = checkSecrets("src/config.ts", content);
|
|
145
|
+
expect(findings).toHaveLength(1);
|
|
146
|
+
});
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
// ─── checkEmptyCatch ─────────────────────────────────────────────────────
|
|
150
|
+
|
|
151
|
+
describe("checkEmptyCatch", () => {
|
|
152
|
+
it("detects empty catch blocks", () => {
|
|
153
|
+
const content = `try {\n doSomething();\n} catch (e) {\n}\n`;
|
|
154
|
+
const findings = checkEmptyCatch("src/app.ts", content);
|
|
155
|
+
expect(findings).toHaveLength(1);
|
|
156
|
+
expect(findings[0]?.ruleId).toBe("empty-catch");
|
|
157
|
+
expect(findings[0]?.severity).toBe("warning");
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
it("does not flag catch blocks with content", () => {
|
|
161
|
+
const content = `try {\n doSomething();\n} catch (e) {\n console.error(e);\n}\n`;
|
|
162
|
+
const findings = checkEmptyCatch("src/app.ts", content);
|
|
163
|
+
expect(findings).toHaveLength(0);
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
it("detects catch blocks with only whitespace", () => {
|
|
167
|
+
const content = `try {\n doSomething();\n} catch (e) {\n \n}\n`;
|
|
168
|
+
const findings = checkEmptyCatch("src/app.ts", content);
|
|
169
|
+
expect(findings).toHaveLength(1);
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
it("allows catch with a comment (intentional empty catch)", () => {
|
|
173
|
+
const content = `try {\n doSomething();\n} catch (e) {\n // intentionally empty\n}\n`;
|
|
174
|
+
const findings = checkEmptyCatch("src/app.ts", content);
|
|
175
|
+
expect(findings).toHaveLength(0);
|
|
176
|
+
});
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
// ─── checkAnyType ────────────────────────────────────────────────────────
|
|
180
|
+
|
|
181
|
+
describe("checkAnyType", () => {
|
|
182
|
+
it("detects 'any' type annotation in .ts files", () => {
|
|
183
|
+
const content = `function foo(x: any): void {\n return;\n}\n`;
|
|
184
|
+
const findings = checkAnyType("src/app.ts", content);
|
|
185
|
+
expect(findings).toHaveLength(1);
|
|
186
|
+
expect(findings[0]?.line).toBe(1);
|
|
187
|
+
expect(findings[0]?.ruleId).toBe("no-any-type");
|
|
188
|
+
expect(findings[0]?.severity).toBe("warning");
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
it("skips .d.ts files", () => {
|
|
192
|
+
const content = `declare function foo(x: any): void;\n`;
|
|
193
|
+
const findings = checkAnyType("src/types.d.ts", content);
|
|
194
|
+
expect(findings).toHaveLength(0);
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
it("does not flag 'any' in comments or strings", () => {
|
|
198
|
+
const content = `// any type is bad\nconst msg = "any value";\n`;
|
|
199
|
+
const findings = checkAnyType("src/app.ts", content);
|
|
200
|
+
expect(findings).toHaveLength(0);
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
it("detects multiple any usages", () => {
|
|
204
|
+
const content = `const x: any = 1;\nconst y: any = 2;\n`;
|
|
205
|
+
const findings = checkAnyType("src/app.ts", content);
|
|
206
|
+
expect(findings).toHaveLength(2);
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
it("does not flag words containing 'any' like 'many' or 'company'", () => {
|
|
210
|
+
const content = `const many = 1;\nconst company = "acme";\n`;
|
|
211
|
+
const findings = checkAnyType("src/app.ts", content);
|
|
212
|
+
expect(findings).toHaveLength(0);
|
|
213
|
+
});
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
// ─── checkUnusedImports ──────────────────────────────────────────────────
|
|
217
|
+
|
|
218
|
+
describe("checkUnusedImports", () => {
|
|
219
|
+
it("detects unused named imports", () => {
|
|
220
|
+
const content = `import { foo, bar } from "./mod";\nconst x = foo();\n`;
|
|
221
|
+
const findings = checkUnusedImports("src/app.ts", content);
|
|
222
|
+
// bar is unused
|
|
223
|
+
expect(findings).toHaveLength(1);
|
|
224
|
+
expect(findings[0]?.message).toContain("bar");
|
|
225
|
+
expect(findings[0]?.ruleId).toBe("unused-import");
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
it("does not flag used imports", () => {
|
|
229
|
+
const content = `import { foo } from "./mod";\nconst x = foo();\n`;
|
|
230
|
+
const findings = checkUnusedImports("src/app.ts", content);
|
|
231
|
+
expect(findings).toHaveLength(0);
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
it("handles type imports (should not flag)", () => {
|
|
235
|
+
const content = `import type { Foo } from "./mod";\nconst x: Foo = {};\n`;
|
|
236
|
+
const findings = checkUnusedImports("src/app.ts", content);
|
|
237
|
+
expect(findings).toHaveLength(0);
|
|
238
|
+
});
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
// ─── runBuiltinChecks ────────────────────────────────────────────────────
|
|
242
|
+
|
|
243
|
+
describe("runBuiltinChecks", () => {
|
|
244
|
+
it("aggregates findings from all checks", () => {
|
|
245
|
+
const content = [
|
|
246
|
+
`import { unused } from "./mod";`,
|
|
247
|
+
`console.log("bad");`,
|
|
248
|
+
`// TODO: fix later`,
|
|
249
|
+
`const x: any = 1;`,
|
|
250
|
+
`try { f(); } catch (e) {}`,
|
|
251
|
+
`password="secret123"`,
|
|
252
|
+
].join("\n");
|
|
253
|
+
|
|
254
|
+
const findings = runBuiltinChecks("src/app.ts", content);
|
|
255
|
+
// Should have findings from multiple checks
|
|
256
|
+
expect(findings.length).toBeGreaterThanOrEqual(4);
|
|
257
|
+
|
|
258
|
+
// Verify all findings have correct tool
|
|
259
|
+
for (const f of findings) {
|
|
260
|
+
expect(f.tool).toBe("builtin");
|
|
261
|
+
expect(f.file).toBe("src/app.ts");
|
|
262
|
+
}
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
it("returns empty for clean files", () => {
|
|
266
|
+
const content = `import { foo } from "./mod";\nconst x = foo();\n`;
|
|
267
|
+
const findings = runBuiltinChecks("src/app.ts", content);
|
|
268
|
+
expect(findings).toHaveLength(0);
|
|
269
|
+
});
|
|
270
|
+
});
|
|
@@ -272,8 +272,8 @@ describe("VerifyPipeline", () => {
|
|
|
272
272
|
expect(callOrder).toContain("runTrivy");
|
|
273
273
|
expect(callOrder).toContain("runSecretlint");
|
|
274
274
|
|
|
275
|
-
//
|
|
276
|
-
expect(result.tools).toHaveLength(
|
|
275
|
+
// 12 tool reports (slop + semgrep + trivy + secretlint + sonarqube + stryker + diff-cover + typecheck + consistency + builtin + ai-review + wiki-lint)
|
|
276
|
+
expect(result.tools).toHaveLength(12);
|
|
277
277
|
expect(result.findings).toHaveLength(3);
|
|
278
278
|
});
|
|
279
279
|
|
|
@@ -428,8 +428,9 @@ describe("VerifyPipeline", () => {
|
|
|
428
428
|
});
|
|
429
429
|
|
|
430
430
|
expect(callOrder).not.toContain("filterByDiff");
|
|
431
|
-
|
|
432
|
-
expect(result.
|
|
431
|
+
// At least the slop finding; wiki-lint may add more from real .maina/wiki/
|
|
432
|
+
expect(result.findings.length).toBeGreaterThanOrEqual(1);
|
|
433
|
+
expect(result.findings.some((f) => f.tool === "slop")).toBe(true);
|
|
433
434
|
});
|
|
434
435
|
|
|
435
436
|
it("should include duration in result", async () => {
|
|
@@ -489,9 +490,7 @@ describe("VerifyPipeline", () => {
|
|
|
489
490
|
diffOnly: false,
|
|
490
491
|
});
|
|
491
492
|
|
|
492
|
-
// Should
|
|
493
|
-
expect(result.passed).toBe(true);
|
|
494
|
-
|
|
493
|
+
// Should include the pipeline warning about skipped external tools
|
|
495
494
|
const pipelineWarning = result.findings.find(
|
|
496
495
|
(f) => f.tool === "pipeline" && f.severity === "warning",
|
|
497
496
|
);
|
|
@@ -557,7 +556,11 @@ describe("VerifyPipeline", () => {
|
|
|
557
556
|
duration: 0,
|
|
558
557
|
};
|
|
559
558
|
const result = await runPipeline({ files: ["src/app.ts"] });
|
|
560
|
-
|
|
559
|
+
// Pipeline passes if no error-severity findings from non-wiki tools
|
|
560
|
+
const nonWikiErrors = result.findings.filter(
|
|
561
|
+
(f) => f.tool !== "wiki-lint" && f.severity === "error",
|
|
562
|
+
);
|
|
563
|
+
expect(nonWikiErrors).toHaveLength(0);
|
|
561
564
|
const aiReport = result.tools.find((t) => t.tool === "ai-review");
|
|
562
565
|
expect(aiReport?.skipped).toBe(true);
|
|
563
566
|
});
|