@massu/core 0.1.1 → 0.1.2
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 +2 -2
- package/dist/hooks/cost-tracker.js +23 -35
- package/dist/hooks/post-edit-context.js +2 -2
- package/dist/hooks/post-tool-use.js +43 -58
- package/dist/hooks/pre-compact.js +23 -38
- package/dist/hooks/pre-delete-check.js +18 -31
- package/dist/hooks/quality-event.js +23 -35
- package/dist/hooks/session-end.js +62 -78
- package/dist/hooks/session-start.js +33 -42
- package/dist/hooks/user-prompt.js +23 -38
- package/package.json +8 -14
- package/src/adr-generator.ts +9 -2
- package/src/analytics.ts +9 -3
- package/src/audit-trail.ts +10 -3
- package/src/cloud-sync.ts +14 -18
- package/src/commands/init.ts +1 -5
- package/src/cost-tracker.ts +11 -6
- package/src/dependency-scorer.ts +9 -2
- package/src/docs-tools.ts +13 -10
- package/src/hooks/post-edit-context.ts +3 -3
- package/src/hooks/session-end.ts +3 -3
- package/src/hooks/session-start.ts +2 -2
- package/src/memory-db.ts +1351 -23
- package/src/memory-tools.ts +14 -15
- package/src/observability-tools.ts +13 -2
- package/src/prompt-analyzer.ts +9 -2
- package/src/regression-detector.ts +9 -3
- package/src/security-scorer.ts +9 -2
- package/src/sentinel-db.ts +43 -88
- package/src/sentinel-tools.ts +8 -11
- package/src/server.ts +1 -2
- package/src/team-knowledge.ts +9 -2
- package/src/tools.ts +771 -35
- package/src/validate-features-runner.ts +0 -1
- package/src/validation-engine.ts +9 -2
- package/dist/cli.js +0 -7890
- package/dist/server.js +0 -7008
- package/src/__tests__/adr-generator.test.ts +0 -260
- package/src/__tests__/analytics.test.ts +0 -282
- package/src/__tests__/audit-trail.test.ts +0 -382
- package/src/__tests__/backfill-sessions.test.ts +0 -690
- package/src/__tests__/cli.test.ts +0 -290
- package/src/__tests__/cloud-sync.test.ts +0 -261
- package/src/__tests__/config-sections.test.ts +0 -359
- package/src/__tests__/config.test.ts +0 -732
- package/src/__tests__/cost-tracker.test.ts +0 -348
- package/src/__tests__/db.test.ts +0 -177
- package/src/__tests__/dependency-scorer.test.ts +0 -325
- package/src/__tests__/docs-integration.test.ts +0 -178
- package/src/__tests__/docs-tools.test.ts +0 -199
- package/src/__tests__/domains.test.ts +0 -236
- package/src/__tests__/hooks.test.ts +0 -221
- package/src/__tests__/import-resolver.test.ts +0 -95
- package/src/__tests__/integration/path-traversal.test.ts +0 -134
- package/src/__tests__/integration/pricing-consistency.test.ts +0 -88
- package/src/__tests__/integration/tool-registration.test.ts +0 -146
- package/src/__tests__/memory-db.test.ts +0 -404
- package/src/__tests__/memory-enhancements.test.ts +0 -316
- package/src/__tests__/memory-tools.test.ts +0 -199
- package/src/__tests__/middleware-tree.test.ts +0 -177
- package/src/__tests__/observability-tools.test.ts +0 -595
- package/src/__tests__/observability.test.ts +0 -437
- package/src/__tests__/observation-extractor.test.ts +0 -167
- package/src/__tests__/page-deps.test.ts +0 -60
- package/src/__tests__/prompt-analyzer.test.ts +0 -298
- package/src/__tests__/regression-detector.test.ts +0 -295
- package/src/__tests__/rules.test.ts +0 -87
- package/src/__tests__/schema-mapper.test.ts +0 -29
- package/src/__tests__/security-scorer.test.ts +0 -238
- package/src/__tests__/security-utils.test.ts +0 -175
- package/src/__tests__/sentinel-db.test.ts +0 -491
- package/src/__tests__/sentinel-scanner.test.ts +0 -750
- package/src/__tests__/sentinel-tools.test.ts +0 -324
- package/src/__tests__/sentinel-types.test.ts +0 -750
- package/src/__tests__/server.test.ts +0 -452
- package/src/__tests__/session-archiver.test.ts +0 -524
- package/src/__tests__/session-state-generator.test.ts +0 -900
- package/src/__tests__/team-knowledge.test.ts +0 -327
- package/src/__tests__/tools.test.ts +0 -340
- package/src/__tests__/transcript-parser.test.ts +0 -195
- package/src/__tests__/trpc-index.test.ts +0 -25
- package/src/__tests__/validate-features-runner.test.ts +0 -517
- package/src/__tests__/validation-engine.test.ts +0 -300
- package/src/core-tools.ts +0 -685
- package/src/memory-queries.ts +0 -804
- package/src/memory-schema.ts +0 -546
- package/src/tool-helpers.ts +0 -41
|
@@ -1,298 +0,0 @@
|
|
|
1
|
-
// Copyright (c) 2026 Massu. All rights reserved.
|
|
2
|
-
// Licensed under BSL 1.1 - see LICENSE file for details.
|
|
3
|
-
|
|
4
|
-
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
5
|
-
import Database from 'better-sqlite3';
|
|
6
|
-
import {
|
|
7
|
-
getPromptToolDefinitions,
|
|
8
|
-
isPromptTool,
|
|
9
|
-
categorizePrompt,
|
|
10
|
-
hashPrompt,
|
|
11
|
-
detectOutcome,
|
|
12
|
-
analyzeSessionPrompts,
|
|
13
|
-
handlePromptToolCall,
|
|
14
|
-
} from '../prompt-analyzer.ts';
|
|
15
|
-
|
|
16
|
-
function createTestDb(): Database.Database {
|
|
17
|
-
const db = new Database(':memory:');
|
|
18
|
-
db.pragma('journal_mode = WAL');
|
|
19
|
-
|
|
20
|
-
db.exec(`
|
|
21
|
-
CREATE TABLE sessions (
|
|
22
|
-
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
23
|
-
session_id TEXT UNIQUE NOT NULL,
|
|
24
|
-
started_at TEXT NOT NULL,
|
|
25
|
-
started_at_epoch INTEGER NOT NULL
|
|
26
|
-
);
|
|
27
|
-
|
|
28
|
-
CREATE TABLE user_prompts (
|
|
29
|
-
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
30
|
-
session_id TEXT NOT NULL,
|
|
31
|
-
prompt_text TEXT NOT NULL,
|
|
32
|
-
prompt_number INTEGER NOT NULL DEFAULT 1,
|
|
33
|
-
created_at TEXT NOT NULL,
|
|
34
|
-
created_at_epoch INTEGER NOT NULL
|
|
35
|
-
);
|
|
36
|
-
|
|
37
|
-
CREATE TABLE prompt_outcomes (
|
|
38
|
-
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
39
|
-
session_id TEXT NOT NULL,
|
|
40
|
-
prompt_hash TEXT NOT NULL,
|
|
41
|
-
prompt_text TEXT NOT NULL,
|
|
42
|
-
prompt_category TEXT NOT NULL DEFAULT 'feature',
|
|
43
|
-
word_count INTEGER NOT NULL DEFAULT 0,
|
|
44
|
-
outcome TEXT NOT NULL DEFAULT 'success' CHECK(outcome IN ('success', 'partial', 'failure', 'abandoned')),
|
|
45
|
-
corrections_needed INTEGER NOT NULL DEFAULT 0,
|
|
46
|
-
follow_up_prompts INTEGER NOT NULL DEFAULT 0,
|
|
47
|
-
created_at TEXT DEFAULT (datetime('now'))
|
|
48
|
-
);
|
|
49
|
-
`);
|
|
50
|
-
|
|
51
|
-
return db;
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
describe('prompt-analyzer', () => {
|
|
55
|
-
let db: Database.Database;
|
|
56
|
-
|
|
57
|
-
beforeEach(() => {
|
|
58
|
-
db = createTestDb();
|
|
59
|
-
});
|
|
60
|
-
|
|
61
|
-
afterEach(() => {
|
|
62
|
-
db.close();
|
|
63
|
-
});
|
|
64
|
-
|
|
65
|
-
describe('getPromptToolDefinitions', () => {
|
|
66
|
-
it('should return tool definitions for prompt tools', () => {
|
|
67
|
-
const defs = getPromptToolDefinitions();
|
|
68
|
-
expect(defs.length).toBe(2);
|
|
69
|
-
|
|
70
|
-
const names = defs.map(d => d.name);
|
|
71
|
-
expect(names.some(n => n.includes('prompt_effectiveness'))).toBe(true);
|
|
72
|
-
expect(names.some(n => n.includes('prompt_suggestions'))).toBe(true);
|
|
73
|
-
|
|
74
|
-
for (const def of defs) {
|
|
75
|
-
expect(def.name).toBeTruthy();
|
|
76
|
-
expect(def.description).toBeTruthy();
|
|
77
|
-
expect(def.inputSchema).toBeTruthy();
|
|
78
|
-
expect(def.inputSchema.type).toBe('object');
|
|
79
|
-
}
|
|
80
|
-
});
|
|
81
|
-
});
|
|
82
|
-
|
|
83
|
-
describe('isPromptTool', () => {
|
|
84
|
-
it('should return true for prompt tools', () => {
|
|
85
|
-
expect(isPromptTool('massu_prompt_effectiveness')).toBe(true);
|
|
86
|
-
expect(isPromptTool('massu_prompt_suggestions')).toBe(true);
|
|
87
|
-
});
|
|
88
|
-
|
|
89
|
-
it('should return false for non-prompt tools', () => {
|
|
90
|
-
expect(isPromptTool('massu_cost_session')).toBe(false);
|
|
91
|
-
expect(isPromptTool('massu_quality_score')).toBe(false);
|
|
92
|
-
expect(isPromptTool('random_tool')).toBe(false);
|
|
93
|
-
});
|
|
94
|
-
|
|
95
|
-
it('should handle base names without prefix', () => {
|
|
96
|
-
expect(isPromptTool('prompt_effectiveness')).toBe(true);
|
|
97
|
-
expect(isPromptTool('prompt_suggestions')).toBe(true);
|
|
98
|
-
});
|
|
99
|
-
});
|
|
100
|
-
|
|
101
|
-
describe('categorizePrompt', () => {
|
|
102
|
-
it('should categorize bugfix prompts', () => {
|
|
103
|
-
expect(categorizePrompt('Fix the bug in auth.ts')).toBe('bugfix');
|
|
104
|
-
expect(categorizePrompt('There is an error in the validation')).toBe('bugfix');
|
|
105
|
-
expect(categorizePrompt('The app crash on submit')).toBe('bugfix');
|
|
106
|
-
});
|
|
107
|
-
|
|
108
|
-
it('should categorize refactor prompts', () => {
|
|
109
|
-
expect(categorizePrompt('Refactor the user module')).toBe('refactor');
|
|
110
|
-
expect(categorizePrompt('Rename the function to be more clear')).toBe('refactor');
|
|
111
|
-
expect(categorizePrompt('Extract this into a separate component')).toBe('refactor');
|
|
112
|
-
});
|
|
113
|
-
|
|
114
|
-
it('should categorize question prompts', () => {
|
|
115
|
-
expect(categorizePrompt('What does this function do?')).toBe('question');
|
|
116
|
-
expect(categorizePrompt('How do I implement authentication?')).toBe('question');
|
|
117
|
-
expect(categorizePrompt('Explain the database schema')).toBe('question');
|
|
118
|
-
});
|
|
119
|
-
|
|
120
|
-
it('should categorize command prompts', () => {
|
|
121
|
-
expect(categorizePrompt('/commit')).toBe('command');
|
|
122
|
-
expect(categorizePrompt('/massu-loop')).toBe('command');
|
|
123
|
-
});
|
|
124
|
-
|
|
125
|
-
it('should categorize feature prompts', () => {
|
|
126
|
-
expect(categorizePrompt('Add a new user registration form')).toBe('feature');
|
|
127
|
-
expect(categorizePrompt('Create a dashboard component')).toBe('feature');
|
|
128
|
-
expect(categorizePrompt('Implement password reset')).toBe('feature');
|
|
129
|
-
});
|
|
130
|
-
|
|
131
|
-
it('should default to feature for ambiguous prompts', () => {
|
|
132
|
-
expect(categorizePrompt('Update the styles')).toBe('feature');
|
|
133
|
-
expect(categorizePrompt('Make it better')).toBe('feature');
|
|
134
|
-
});
|
|
135
|
-
});
|
|
136
|
-
|
|
137
|
-
describe('hashPrompt', () => {
|
|
138
|
-
it('should generate consistent hashes for same prompt', () => {
|
|
139
|
-
const hash1 = hashPrompt('Fix the bug in auth.ts');
|
|
140
|
-
const hash2 = hashPrompt('Fix the bug in auth.ts');
|
|
141
|
-
expect(hash1).toBe(hash2);
|
|
142
|
-
});
|
|
143
|
-
|
|
144
|
-
it('should normalize whitespace', () => {
|
|
145
|
-
const hash1 = hashPrompt('Fix the bug');
|
|
146
|
-
const hash2 = hashPrompt('Fix the bug');
|
|
147
|
-
expect(hash1).toBe(hash2);
|
|
148
|
-
});
|
|
149
|
-
|
|
150
|
-
it('should be case-insensitive', () => {
|
|
151
|
-
const hash1 = hashPrompt('Fix The Bug');
|
|
152
|
-
const hash2 = hashPrompt('fix the bug');
|
|
153
|
-
expect(hash1).toBe(hash2);
|
|
154
|
-
});
|
|
155
|
-
|
|
156
|
-
it('should return different hashes for different prompts', () => {
|
|
157
|
-
const hash1 = hashPrompt('Fix the bug');
|
|
158
|
-
const hash2 = hashPrompt('Add a feature');
|
|
159
|
-
expect(hash1).not.toBe(hash2);
|
|
160
|
-
});
|
|
161
|
-
|
|
162
|
-
it('should return 16-character hash', () => {
|
|
163
|
-
const hash = hashPrompt('Test prompt');
|
|
164
|
-
expect(hash.length).toBe(16);
|
|
165
|
-
});
|
|
166
|
-
});
|
|
167
|
-
|
|
168
|
-
describe('detectOutcome', () => {
|
|
169
|
-
it('should detect success outcome', () => {
|
|
170
|
-
const followUps = ['Great, that works!', 'Perfect, thanks'];
|
|
171
|
-
const responses = ['Done.'];
|
|
172
|
-
const result = detectOutcome(followUps, responses);
|
|
173
|
-
expect(result.outcome).toBe('success');
|
|
174
|
-
expect(result.correctionsNeeded).toBe(0);
|
|
175
|
-
});
|
|
176
|
-
|
|
177
|
-
it('should detect partial outcome with corrections', () => {
|
|
178
|
-
const followUps = ['No, that\'s wrong', 'Fix this issue', 'Try again'];
|
|
179
|
-
const responses = ['Updated.'];
|
|
180
|
-
const result = detectOutcome(followUps, responses);
|
|
181
|
-
expect(result.outcome).toBe('partial');
|
|
182
|
-
expect(result.correctionsNeeded).toBeGreaterThan(0);
|
|
183
|
-
});
|
|
184
|
-
|
|
185
|
-
it('should detect abandoned outcome', () => {
|
|
186
|
-
const followUps = ['Nevermind, skip this', 'Let\'s move on'];
|
|
187
|
-
const responses = ['OK.'];
|
|
188
|
-
const result = detectOutcome(followUps, responses);
|
|
189
|
-
expect(result.outcome).toBe('abandoned');
|
|
190
|
-
});
|
|
191
|
-
|
|
192
|
-
it('should detect failure from assistant responses', () => {
|
|
193
|
-
const followUps: string[] = [];
|
|
194
|
-
const responses = ['Error: cannot complete', 'Failed to process'];
|
|
195
|
-
const result = detectOutcome(followUps, responses);
|
|
196
|
-
expect(result.outcome).toBe('failure');
|
|
197
|
-
});
|
|
198
|
-
|
|
199
|
-
it('should count follow-up prompts', () => {
|
|
200
|
-
const followUps = ['One', 'Two', 'Three'];
|
|
201
|
-
const responses: string[] = [];
|
|
202
|
-
const result = detectOutcome(followUps, responses);
|
|
203
|
-
expect(result.followUpCount).toBe(3);
|
|
204
|
-
});
|
|
205
|
-
});
|
|
206
|
-
|
|
207
|
-
describe('analyzeSessionPrompts', () => {
|
|
208
|
-
it('should analyze prompts from a session', () => {
|
|
209
|
-
const sessionId = 'test-session-1';
|
|
210
|
-
|
|
211
|
-
db.prepare('INSERT INTO sessions (session_id, started_at, started_at_epoch) VALUES (?, ?, ?)').run(
|
|
212
|
-
sessionId,
|
|
213
|
-
new Date().toISOString(),
|
|
214
|
-
Math.floor(Date.now() / 1000)
|
|
215
|
-
);
|
|
216
|
-
|
|
217
|
-
const prompts = [
|
|
218
|
-
'Fix the bug in auth',
|
|
219
|
-
'Add validation',
|
|
220
|
-
'Refactor the code',
|
|
221
|
-
];
|
|
222
|
-
|
|
223
|
-
for (let i = 0; i < prompts.length; i++) {
|
|
224
|
-
db.prepare('INSERT INTO user_prompts (session_id, prompt_text, prompt_number, created_at, created_at_epoch) VALUES (?, ?, ?, ?, ?)').run(
|
|
225
|
-
sessionId,
|
|
226
|
-
prompts[i],
|
|
227
|
-
i + 1,
|
|
228
|
-
new Date().toISOString(),
|
|
229
|
-
Math.floor(Date.now() / 1000)
|
|
230
|
-
);
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
const stored = analyzeSessionPrompts(db, sessionId);
|
|
234
|
-
expect(stored).toBe(3);
|
|
235
|
-
|
|
236
|
-
const outcomes = db.prepare('SELECT * FROM prompt_outcomes WHERE session_id = ?').all(sessionId) as Array<Record<string, unknown>>;
|
|
237
|
-
expect(outcomes.length).toBe(3);
|
|
238
|
-
|
|
239
|
-
for (const outcome of outcomes) {
|
|
240
|
-
expect(outcome.prompt_category).toBeTruthy();
|
|
241
|
-
expect(outcome.prompt_hash).toBeTruthy();
|
|
242
|
-
expect(outcome.word_count).toBeGreaterThan(0);
|
|
243
|
-
}
|
|
244
|
-
});
|
|
245
|
-
|
|
246
|
-
it('should skip duplicate prompts', () => {
|
|
247
|
-
const sessionId = 'test-session-2';
|
|
248
|
-
|
|
249
|
-
db.prepare('INSERT INTO sessions (session_id, started_at, started_at_epoch) VALUES (?, ?, ?)').run(
|
|
250
|
-
sessionId,
|
|
251
|
-
new Date().toISOString(),
|
|
252
|
-
Math.floor(Date.now() / 1000)
|
|
253
|
-
);
|
|
254
|
-
|
|
255
|
-
db.prepare('INSERT INTO user_prompts (session_id, prompt_text, prompt_number, created_at, created_at_epoch) VALUES (?, ?, ?, ?, ?)').run(
|
|
256
|
-
sessionId,
|
|
257
|
-
'Fix the bug',
|
|
258
|
-
1,
|
|
259
|
-
new Date().toISOString(),
|
|
260
|
-
Math.floor(Date.now() / 1000)
|
|
261
|
-
);
|
|
262
|
-
|
|
263
|
-
analyzeSessionPrompts(db, sessionId);
|
|
264
|
-
const first = analyzeSessionPrompts(db, sessionId);
|
|
265
|
-
|
|
266
|
-
expect(first).toBe(0); // No new prompts stored
|
|
267
|
-
});
|
|
268
|
-
});
|
|
269
|
-
|
|
270
|
-
describe('handlePromptToolCall', () => {
|
|
271
|
-
it('should handle prompt_suggestions tool call', () => {
|
|
272
|
-
const result = handlePromptToolCall('massu_prompt_suggestions', { prompt: 'Fix the bug' }, db);
|
|
273
|
-
|
|
274
|
-
expect(result.content).toBeDefined();
|
|
275
|
-
expect(result.content.length).toBeGreaterThan(0);
|
|
276
|
-
expect(result.content[0].type).toBe('text');
|
|
277
|
-
const text = result.content[0].text;
|
|
278
|
-
expect(text).toContain('Prompt Analysis');
|
|
279
|
-
expect(text).toContain('Category');
|
|
280
|
-
});
|
|
281
|
-
|
|
282
|
-
it('should return error for missing prompt', () => {
|
|
283
|
-
const result = handlePromptToolCall('massu_prompt_suggestions', {}, db);
|
|
284
|
-
|
|
285
|
-
expect(result.content).toBeDefined();
|
|
286
|
-
expect(result.content[0].type).toBe('text');
|
|
287
|
-
expect(result.content[0].text).toContain('Usage');
|
|
288
|
-
});
|
|
289
|
-
|
|
290
|
-
it('should handle unknown tool name', () => {
|
|
291
|
-
const result = handlePromptToolCall('massu_unknown_prompt_tool', {}, db);
|
|
292
|
-
|
|
293
|
-
expect(result.content).toBeDefined();
|
|
294
|
-
expect(result.content[0].type).toBe('text');
|
|
295
|
-
expect(result.content[0].text).toContain('Unknown prompt tool');
|
|
296
|
-
});
|
|
297
|
-
});
|
|
298
|
-
});
|
|
@@ -1,295 +0,0 @@
|
|
|
1
|
-
// Copyright (c) 2026 Massu. All rights reserved.
|
|
2
|
-
// Licensed under BSL 1.1 - see LICENSE file for details.
|
|
3
|
-
|
|
4
|
-
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
5
|
-
import Database from 'better-sqlite3';
|
|
6
|
-
import {
|
|
7
|
-
getRegressionToolDefinitions,
|
|
8
|
-
isRegressionTool,
|
|
9
|
-
calculateHealthScore,
|
|
10
|
-
trackModification,
|
|
11
|
-
recordTestResult,
|
|
12
|
-
handleRegressionToolCall,
|
|
13
|
-
} from '../regression-detector.ts';
|
|
14
|
-
|
|
15
|
-
function createTestDb(): Database.Database {
|
|
16
|
-
const db = new Database(':memory:');
|
|
17
|
-
db.exec(`
|
|
18
|
-
CREATE TABLE feature_health (
|
|
19
|
-
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
20
|
-
feature_key TEXT NOT NULL UNIQUE,
|
|
21
|
-
health_score INTEGER NOT NULL DEFAULT 100,
|
|
22
|
-
tests_passing INTEGER NOT NULL DEFAULT 0,
|
|
23
|
-
tests_failing INTEGER NOT NULL DEFAULT 0,
|
|
24
|
-
test_coverage_pct REAL,
|
|
25
|
-
modifications_since_test INTEGER NOT NULL DEFAULT 0,
|
|
26
|
-
last_modified TEXT,
|
|
27
|
-
last_tested TEXT,
|
|
28
|
-
created_at TEXT DEFAULT (datetime('now'))
|
|
29
|
-
);
|
|
30
|
-
`);
|
|
31
|
-
return db;
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
describe('regression-detector', () => {
|
|
35
|
-
let db: Database.Database;
|
|
36
|
-
|
|
37
|
-
beforeEach(() => {
|
|
38
|
-
db = createTestDb();
|
|
39
|
-
});
|
|
40
|
-
|
|
41
|
-
afterEach(() => {
|
|
42
|
-
db.close();
|
|
43
|
-
});
|
|
44
|
-
|
|
45
|
-
describe('getRegressionToolDefinitions', () => {
|
|
46
|
-
it('returns 2 tool definitions', () => {
|
|
47
|
-
const tools = getRegressionToolDefinitions();
|
|
48
|
-
expect(tools).toHaveLength(2);
|
|
49
|
-
expect(tools.map(t => t.name.split('_').slice(-2).join('_'))).toEqual([
|
|
50
|
-
'feature_health',
|
|
51
|
-
'regression_risk',
|
|
52
|
-
]);
|
|
53
|
-
});
|
|
54
|
-
|
|
55
|
-
it('has required fields in tool definitions', () => {
|
|
56
|
-
const tools = getRegressionToolDefinitions();
|
|
57
|
-
tools.forEach(tool => {
|
|
58
|
-
expect(tool.name).toBeTruthy();
|
|
59
|
-
expect(tool.description).toBeTruthy();
|
|
60
|
-
expect(tool.inputSchema).toBeDefined();
|
|
61
|
-
});
|
|
62
|
-
});
|
|
63
|
-
});
|
|
64
|
-
|
|
65
|
-
describe('isRegressionTool', () => {
|
|
66
|
-
it('returns true for regression tool names', () => {
|
|
67
|
-
expect(isRegressionTool('massu_feature_health')).toBe(true);
|
|
68
|
-
expect(isRegressionTool('massu_regression_risk')).toBe(true);
|
|
69
|
-
});
|
|
70
|
-
|
|
71
|
-
it('returns false for non-regression tool names', () => {
|
|
72
|
-
expect(isRegressionTool('massu_security_score')).toBe(false);
|
|
73
|
-
expect(isRegressionTool('massu_unknown')).toBe(false);
|
|
74
|
-
});
|
|
75
|
-
});
|
|
76
|
-
|
|
77
|
-
describe('calculateHealthScore', () => {
|
|
78
|
-
it('returns 100 for healthy feature', () => {
|
|
79
|
-
const score = calculateHealthScore(
|
|
80
|
-
10, // tests passing
|
|
81
|
-
0, // tests failing
|
|
82
|
-
0, // modifications since test
|
|
83
|
-
new Date().toISOString(),
|
|
84
|
-
new Date().toISOString()
|
|
85
|
-
);
|
|
86
|
-
expect(score).toBe(100);
|
|
87
|
-
});
|
|
88
|
-
|
|
89
|
-
it('reduces score for failing tests', () => {
|
|
90
|
-
const score = calculateHealthScore(5, 2, 0, null, null);
|
|
91
|
-
expect(score).toBeLessThan(100);
|
|
92
|
-
expect(score).toBeLessThanOrEqual(80); // -20 for 2 failing tests
|
|
93
|
-
});
|
|
94
|
-
|
|
95
|
-
it('reduces score for untested modifications', () => {
|
|
96
|
-
const score = calculateHealthScore(10, 0, 3, null, null);
|
|
97
|
-
expect(score).toBeLessThan(100);
|
|
98
|
-
expect(score).toBeLessThanOrEqual(85); // -15 for 3 modifications
|
|
99
|
-
});
|
|
100
|
-
|
|
101
|
-
it('reduces score for modified but never tested', () => {
|
|
102
|
-
const score = calculateHealthScore(0, 0, 0, null, new Date().toISOString());
|
|
103
|
-
expect(score).toBeLessThan(100);
|
|
104
|
-
expect(score).toBe(70); // -30 for never tested
|
|
105
|
-
});
|
|
106
|
-
|
|
107
|
-
it('reduces score for time gap between modification and test', () => {
|
|
108
|
-
const modDate = new Date();
|
|
109
|
-
const testDate = new Date(modDate.getTime() - 10 * 24 * 60 * 60 * 1000); // 10 days ago
|
|
110
|
-
const score = calculateHealthScore(10, 0, 0, testDate.toISOString(), modDate.toISOString());
|
|
111
|
-
expect(score).toBeLessThan(100);
|
|
112
|
-
});
|
|
113
|
-
|
|
114
|
-
it('floors score at 0', () => {
|
|
115
|
-
const score = calculateHealthScore(0, 10, 10, null, null);
|
|
116
|
-
expect(score).toBeGreaterThanOrEqual(0);
|
|
117
|
-
});
|
|
118
|
-
|
|
119
|
-
it('combines multiple risk factors', () => {
|
|
120
|
-
const score = calculateHealthScore(
|
|
121
|
-
5, // passing
|
|
122
|
-
3, // 3 failing = -30
|
|
123
|
-
5, // 5 modifications = -25
|
|
124
|
-
null,
|
|
125
|
-
null
|
|
126
|
-
);
|
|
127
|
-
expect(score).toBeLessThan(50);
|
|
128
|
-
});
|
|
129
|
-
});
|
|
130
|
-
|
|
131
|
-
describe('trackModification', () => {
|
|
132
|
-
it('creates new feature health record on first modification', () => {
|
|
133
|
-
trackModification(db, 'feature-auth');
|
|
134
|
-
|
|
135
|
-
const feature = db.prepare('SELECT * FROM feature_health WHERE feature_key = ?').get('feature-auth') as Record<string, unknown>;
|
|
136
|
-
expect(feature).toBeDefined();
|
|
137
|
-
expect(feature.modifications_since_test).toBe(1);
|
|
138
|
-
expect(feature.health_score).toBe(70); // Default for new feature with 1 modification
|
|
139
|
-
});
|
|
140
|
-
|
|
141
|
-
it('increments modifications for existing feature', () => {
|
|
142
|
-
// Create initial feature
|
|
143
|
-
db.prepare(`
|
|
144
|
-
INSERT INTO feature_health (feature_key, tests_passing, tests_failing, modifications_since_test, health_score)
|
|
145
|
-
VALUES (?, ?, ?, ?, ?)
|
|
146
|
-
`).run('feature-orders', 10, 0, 0, 100);
|
|
147
|
-
|
|
148
|
-
trackModification(db, 'feature-orders');
|
|
149
|
-
|
|
150
|
-
const feature = db.prepare('SELECT * FROM feature_health WHERE feature_key = ?').get('feature-orders') as Record<string, unknown>;
|
|
151
|
-
expect(feature.modifications_since_test).toBe(1);
|
|
152
|
-
expect(feature.health_score).toBeLessThan(100);
|
|
153
|
-
});
|
|
154
|
-
|
|
155
|
-
it('updates last_modified timestamp', () => {
|
|
156
|
-
trackModification(db, 'feature-products');
|
|
157
|
-
|
|
158
|
-
const feature = db.prepare('SELECT last_modified FROM feature_health WHERE feature_key = ?').get('feature-products') as { last_modified: string };
|
|
159
|
-
expect(feature.last_modified).toBeTruthy();
|
|
160
|
-
});
|
|
161
|
-
});
|
|
162
|
-
|
|
163
|
-
describe('recordTestResult', () => {
|
|
164
|
-
it('creates new feature health record with test results', () => {
|
|
165
|
-
recordTestResult(db, 'feature-auth', 15, 2);
|
|
166
|
-
|
|
167
|
-
const feature = db.prepare('SELECT * FROM feature_health WHERE feature_key = ?').get('feature-auth') as Record<string, unknown>;
|
|
168
|
-
expect(feature).toBeDefined();
|
|
169
|
-
expect(feature.tests_passing).toBe(15);
|
|
170
|
-
expect(feature.tests_failing).toBe(2);
|
|
171
|
-
expect(feature.modifications_since_test).toBe(0);
|
|
172
|
-
});
|
|
173
|
-
|
|
174
|
-
it('updates existing feature with test results', () => {
|
|
175
|
-
// Create feature with modifications
|
|
176
|
-
db.prepare(`
|
|
177
|
-
INSERT INTO feature_health (feature_key, modifications_since_test, health_score)
|
|
178
|
-
VALUES (?, ?, ?)
|
|
179
|
-
`).run('feature-orders', 5, 75);
|
|
180
|
-
|
|
181
|
-
recordTestResult(db, 'feature-orders', 20, 1);
|
|
182
|
-
|
|
183
|
-
const feature = db.prepare('SELECT * FROM feature_health WHERE feature_key = ?').get('feature-orders') as Record<string, unknown>;
|
|
184
|
-
expect(feature.tests_passing).toBe(20);
|
|
185
|
-
expect(feature.tests_failing).toBe(1);
|
|
186
|
-
expect(feature.modifications_since_test).toBe(0); // Reset after test
|
|
187
|
-
});
|
|
188
|
-
|
|
189
|
-
it('updates last_tested timestamp', () => {
|
|
190
|
-
recordTestResult(db, 'feature-products', 10, 0);
|
|
191
|
-
|
|
192
|
-
const feature = db.prepare('SELECT last_tested FROM feature_health WHERE feature_key = ?').get('feature-products') as { last_tested: string };
|
|
193
|
-
expect(feature.last_tested).toBeTruthy();
|
|
194
|
-
});
|
|
195
|
-
|
|
196
|
-
it('calculates test coverage percentage', () => {
|
|
197
|
-
recordTestResult(db, 'feature-auth', 15, 5);
|
|
198
|
-
|
|
199
|
-
const feature = db.prepare('SELECT test_coverage_pct FROM feature_health WHERE feature_key = ?').get('feature-auth') as { test_coverage_pct: number };
|
|
200
|
-
expect(feature.test_coverage_pct).toBe(75); // 15 / (15 + 5) = 75%
|
|
201
|
-
});
|
|
202
|
-
|
|
203
|
-
it('handles all passing tests', () => {
|
|
204
|
-
recordTestResult(db, 'feature-healthy', 20, 0);
|
|
205
|
-
|
|
206
|
-
const feature = db.prepare('SELECT * FROM feature_health WHERE feature_key = ?').get('feature-healthy') as Record<string, unknown>;
|
|
207
|
-
expect(feature.health_score).toBe(100);
|
|
208
|
-
expect(feature.test_coverage_pct).toBe(100);
|
|
209
|
-
});
|
|
210
|
-
});
|
|
211
|
-
|
|
212
|
-
describe('handleRegressionToolCall', () => {
|
|
213
|
-
it('handles feature_health with no data', () => {
|
|
214
|
-
const result = handleRegressionToolCall('massu_feature_health', {}, db);
|
|
215
|
-
const text = result.content[0].text;
|
|
216
|
-
expect(text).toContain('No feature health data available');
|
|
217
|
-
});
|
|
218
|
-
|
|
219
|
-
it('handles feature_health with features', () => {
|
|
220
|
-
db.prepare(`
|
|
221
|
-
INSERT INTO feature_health (feature_key, health_score, tests_passing, tests_failing, modifications_since_test)
|
|
222
|
-
VALUES (?, ?, ?, ?, ?)
|
|
223
|
-
`).run('feature-auth', 85, 10, 1, 2);
|
|
224
|
-
|
|
225
|
-
db.prepare(`
|
|
226
|
-
INSERT INTO feature_health (feature_key, health_score, tests_passing, tests_failing, modifications_since_test)
|
|
227
|
-
VALUES (?, ?, ?, ?, ?)
|
|
228
|
-
`).run('feature-orders', 60, 5, 3, 5);
|
|
229
|
-
|
|
230
|
-
const result = handleRegressionToolCall('massu_feature_health', {}, db);
|
|
231
|
-
const text = result.content[0].text;
|
|
232
|
-
expect(text).toContain('Feature Health Dashboard');
|
|
233
|
-
expect(text).toContain('feature-auth');
|
|
234
|
-
expect(text).toContain('feature-orders');
|
|
235
|
-
expect(text).toContain('85');
|
|
236
|
-
expect(text).toContain('60');
|
|
237
|
-
});
|
|
238
|
-
|
|
239
|
-
it('handles feature_health with unhealthy_only filter', () => {
|
|
240
|
-
db.prepare(`
|
|
241
|
-
INSERT INTO feature_health (feature_key, health_score, tests_passing, tests_failing)
|
|
242
|
-
VALUES (?, ?, ?, ?)
|
|
243
|
-
`).run('feature-healthy', 95, 20, 0);
|
|
244
|
-
|
|
245
|
-
db.prepare(`
|
|
246
|
-
INSERT INTO feature_health (feature_key, health_score, tests_passing, tests_failing)
|
|
247
|
-
VALUES (?, ?, ?, ?)
|
|
248
|
-
`).run('feature-unhealthy', 45, 5, 5);
|
|
249
|
-
|
|
250
|
-
const result = handleRegressionToolCall('massu_feature_health', { unhealthy_only: true }, db);
|
|
251
|
-
const text = result.content[0].text;
|
|
252
|
-
expect(text).toContain('feature-unhealthy');
|
|
253
|
-
expect(text).not.toContain('feature-healthy');
|
|
254
|
-
});
|
|
255
|
-
|
|
256
|
-
it('handles regression_risk with no modifications', () => {
|
|
257
|
-
const result = handleRegressionToolCall('massu_regression_risk', {}, db);
|
|
258
|
-
const text = result.content[0].text;
|
|
259
|
-
expect(text).toContain('No features have been modified');
|
|
260
|
-
});
|
|
261
|
-
|
|
262
|
-
it('handles regression_risk and categorizes by risk level', () => {
|
|
263
|
-
db.prepare(`
|
|
264
|
-
INSERT INTO feature_health (feature_key, health_score, modifications_since_test)
|
|
265
|
-
VALUES (?, ?, ?)
|
|
266
|
-
`).run('feature-critical', 30, 5);
|
|
267
|
-
|
|
268
|
-
db.prepare(`
|
|
269
|
-
INSERT INTO feature_health (feature_key, health_score, modifications_since_test)
|
|
270
|
-
VALUES (?, ?, ?)
|
|
271
|
-
`).run('feature-medium', 65, 3);
|
|
272
|
-
|
|
273
|
-
db.prepare(`
|
|
274
|
-
INSERT INTO feature_health (feature_key, health_score, modifications_since_test)
|
|
275
|
-
VALUES (?, ?, ?)
|
|
276
|
-
`).run('feature-low', 85, 1);
|
|
277
|
-
|
|
278
|
-
const result = handleRegressionToolCall('massu_regression_risk', {}, db);
|
|
279
|
-
const text = result.content[0].text;
|
|
280
|
-
expect(text).toContain('Regression Risk Assessment');
|
|
281
|
-
expect(text).toContain('HIGH RISK');
|
|
282
|
-
expect(text).toContain('Medium Risk');
|
|
283
|
-
expect(text).toContain('Low Risk');
|
|
284
|
-
expect(text).toContain('feature-critical');
|
|
285
|
-
expect(text).toContain('feature-medium');
|
|
286
|
-
expect(text).toContain('feature-low');
|
|
287
|
-
});
|
|
288
|
-
|
|
289
|
-
it('handles unknown tool name', () => {
|
|
290
|
-
const result = handleRegressionToolCall('massu_regression_unknown', {}, db);
|
|
291
|
-
const text = result.content[0].text;
|
|
292
|
-
expect(text).toContain('Unknown regression tool');
|
|
293
|
-
});
|
|
294
|
-
});
|
|
295
|
-
});
|
|
@@ -1,87 +0,0 @@
|
|
|
1
|
-
// Copyright (c) 2026 Massu. All rights reserved.
|
|
2
|
-
// Licensed under BSL 1.1 - see LICENSE file for details.
|
|
3
|
-
|
|
4
|
-
import { describe, it, expect, vi } from 'vitest';
|
|
5
|
-
import { matchRules, globMatch } from '../rules.ts';
|
|
6
|
-
|
|
7
|
-
// Mock config to avoid interference from parallel tests that change process.cwd()
|
|
8
|
-
// Rules match massu.config.yaml content
|
|
9
|
-
vi.mock('../config.ts', () => ({
|
|
10
|
-
getConfig: () => ({
|
|
11
|
-
toolPrefix: 'massu',
|
|
12
|
-
rules: [
|
|
13
|
-
{
|
|
14
|
-
pattern: 'src/**/*.ts',
|
|
15
|
-
rules: [
|
|
16
|
-
'Use ESM imports (import/export), not CommonJS (require/module.exports)',
|
|
17
|
-
'All database operations go through better-sqlite3',
|
|
18
|
-
],
|
|
19
|
-
},
|
|
20
|
-
{
|
|
21
|
-
pattern: 'src/hooks/**/*.ts',
|
|
22
|
-
rules: [
|
|
23
|
-
'Hooks receive JSON on stdin, output JSON on stdout',
|
|
24
|
-
'Hooks must exit within 5 seconds',
|
|
25
|
-
'Never import heavy dependencies - hooks must be fast',
|
|
26
|
-
],
|
|
27
|
-
},
|
|
28
|
-
],
|
|
29
|
-
}),
|
|
30
|
-
}));
|
|
31
|
-
|
|
32
|
-
describe('globMatch', () => {
|
|
33
|
-
it('matches simple glob patterns', () => {
|
|
34
|
-
expect(globMatch('src/server/api/routers/orders.ts', 'src/server/api/routers/**')).toBe(true);
|
|
35
|
-
expect(globMatch('src/components/orders/OrderCard.tsx', 'src/components/**')).toBe(true);
|
|
36
|
-
expect(globMatch('src/app/orders/page.tsx', 'src/app/**/page.tsx')).toBe(true);
|
|
37
|
-
});
|
|
38
|
-
|
|
39
|
-
it('matches wildcard table name patterns', () => {
|
|
40
|
-
expect(globMatch('src/server/api/routers/unified_products.ts', '**/unified_products**')).toBe(true);
|
|
41
|
-
expect(globMatch('src/lib/unified_products/helpers.ts', '**/unified_products**')).toBe(true);
|
|
42
|
-
});
|
|
43
|
-
|
|
44
|
-
it('rejects non-matching paths', () => {
|
|
45
|
-
expect(globMatch('src/lib/utils.ts', 'src/server/api/routers/**')).toBe(false);
|
|
46
|
-
expect(globMatch('package.json', 'src/components/**')).toBe(false);
|
|
47
|
-
});
|
|
48
|
-
|
|
49
|
-
it('matches middleware.ts exactly', () => {
|
|
50
|
-
expect(globMatch('src/middleware.ts', 'src/middleware.ts')).toBe(true);
|
|
51
|
-
expect(globMatch('src/other-middleware.ts', 'src/middleware.ts')).toBe(false);
|
|
52
|
-
});
|
|
53
|
-
});
|
|
54
|
-
|
|
55
|
-
describe('matchRules', () => {
|
|
56
|
-
// These tests validate against Massu's own config rules:
|
|
57
|
-
// src/**/*.ts -> ESM imports, better-sqlite3
|
|
58
|
-
// src/hooks/**/*.ts -> stdin/stdout JSON, exit within 5 seconds, no heavy deps
|
|
59
|
-
|
|
60
|
-
it('returns rules for TypeScript source files', () => {
|
|
61
|
-
const rules = matchRules('src/config.ts');
|
|
62
|
-
expect(rules.length).toBeGreaterThan(0);
|
|
63
|
-
const allRuleTexts = rules.flatMap(r => r.rules);
|
|
64
|
-
expect(allRuleTexts.some(r => r.includes('ESM'))).toBe(true);
|
|
65
|
-
});
|
|
66
|
-
|
|
67
|
-
it('returns hook-specific rules for hook files', () => {
|
|
68
|
-
const rules = matchRules('src/hooks/post-edit-context.ts');
|
|
69
|
-
// Should match both src/**/*.ts AND src/hooks/**/*.ts patterns
|
|
70
|
-
expect(rules.length).toBeGreaterThanOrEqual(2);
|
|
71
|
-
const allRuleTexts = rules.flatMap(r => r.rules);
|
|
72
|
-
expect(allRuleTexts.some(r => r.includes('stdin'))).toBe(true);
|
|
73
|
-
expect(allRuleTexts.some(r => r.includes('5 seconds'))).toBe(true);
|
|
74
|
-
});
|
|
75
|
-
|
|
76
|
-
it('returns no rules for non-matching paths', () => {
|
|
77
|
-
const rules = matchRules('package.json');
|
|
78
|
-
expect(rules.length).toBe(0);
|
|
79
|
-
});
|
|
80
|
-
|
|
81
|
-
it('returns rules for deeply nested src files', () => {
|
|
82
|
-
const rules = matchRules('src/deep/nested/module.ts');
|
|
83
|
-
expect(rules.length).toBeGreaterThan(0);
|
|
84
|
-
const allRuleTexts = rules.flatMap(r => r.rules);
|
|
85
|
-
expect(allRuleTexts.some(r => r.includes('better-sqlite3'))).toBe(true);
|
|
86
|
-
});
|
|
87
|
-
});
|