@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.
Files changed (87) hide show
  1. package/README.md +2 -2
  2. package/dist/hooks/cost-tracker.js +23 -35
  3. package/dist/hooks/post-edit-context.js +2 -2
  4. package/dist/hooks/post-tool-use.js +43 -58
  5. package/dist/hooks/pre-compact.js +23 -38
  6. package/dist/hooks/pre-delete-check.js +18 -31
  7. package/dist/hooks/quality-event.js +23 -35
  8. package/dist/hooks/session-end.js +62 -78
  9. package/dist/hooks/session-start.js +33 -42
  10. package/dist/hooks/user-prompt.js +23 -38
  11. package/package.json +8 -14
  12. package/src/adr-generator.ts +9 -2
  13. package/src/analytics.ts +9 -3
  14. package/src/audit-trail.ts +10 -3
  15. package/src/cloud-sync.ts +14 -18
  16. package/src/commands/init.ts +1 -5
  17. package/src/cost-tracker.ts +11 -6
  18. package/src/dependency-scorer.ts +9 -2
  19. package/src/docs-tools.ts +13 -10
  20. package/src/hooks/post-edit-context.ts +3 -3
  21. package/src/hooks/session-end.ts +3 -3
  22. package/src/hooks/session-start.ts +2 -2
  23. package/src/memory-db.ts +1351 -23
  24. package/src/memory-tools.ts +14 -15
  25. package/src/observability-tools.ts +13 -2
  26. package/src/prompt-analyzer.ts +9 -2
  27. package/src/regression-detector.ts +9 -3
  28. package/src/security-scorer.ts +9 -2
  29. package/src/sentinel-db.ts +43 -88
  30. package/src/sentinel-tools.ts +8 -11
  31. package/src/server.ts +1 -2
  32. package/src/team-knowledge.ts +9 -2
  33. package/src/tools.ts +771 -35
  34. package/src/validate-features-runner.ts +0 -1
  35. package/src/validation-engine.ts +9 -2
  36. package/dist/cli.js +0 -7890
  37. package/dist/server.js +0 -7008
  38. package/src/__tests__/adr-generator.test.ts +0 -260
  39. package/src/__tests__/analytics.test.ts +0 -282
  40. package/src/__tests__/audit-trail.test.ts +0 -382
  41. package/src/__tests__/backfill-sessions.test.ts +0 -690
  42. package/src/__tests__/cli.test.ts +0 -290
  43. package/src/__tests__/cloud-sync.test.ts +0 -261
  44. package/src/__tests__/config-sections.test.ts +0 -359
  45. package/src/__tests__/config.test.ts +0 -732
  46. package/src/__tests__/cost-tracker.test.ts +0 -348
  47. package/src/__tests__/db.test.ts +0 -177
  48. package/src/__tests__/dependency-scorer.test.ts +0 -325
  49. package/src/__tests__/docs-integration.test.ts +0 -178
  50. package/src/__tests__/docs-tools.test.ts +0 -199
  51. package/src/__tests__/domains.test.ts +0 -236
  52. package/src/__tests__/hooks.test.ts +0 -221
  53. package/src/__tests__/import-resolver.test.ts +0 -95
  54. package/src/__tests__/integration/path-traversal.test.ts +0 -134
  55. package/src/__tests__/integration/pricing-consistency.test.ts +0 -88
  56. package/src/__tests__/integration/tool-registration.test.ts +0 -146
  57. package/src/__tests__/memory-db.test.ts +0 -404
  58. package/src/__tests__/memory-enhancements.test.ts +0 -316
  59. package/src/__tests__/memory-tools.test.ts +0 -199
  60. package/src/__tests__/middleware-tree.test.ts +0 -177
  61. package/src/__tests__/observability-tools.test.ts +0 -595
  62. package/src/__tests__/observability.test.ts +0 -437
  63. package/src/__tests__/observation-extractor.test.ts +0 -167
  64. package/src/__tests__/page-deps.test.ts +0 -60
  65. package/src/__tests__/prompt-analyzer.test.ts +0 -298
  66. package/src/__tests__/regression-detector.test.ts +0 -295
  67. package/src/__tests__/rules.test.ts +0 -87
  68. package/src/__tests__/schema-mapper.test.ts +0 -29
  69. package/src/__tests__/security-scorer.test.ts +0 -238
  70. package/src/__tests__/security-utils.test.ts +0 -175
  71. package/src/__tests__/sentinel-db.test.ts +0 -491
  72. package/src/__tests__/sentinel-scanner.test.ts +0 -750
  73. package/src/__tests__/sentinel-tools.test.ts +0 -324
  74. package/src/__tests__/sentinel-types.test.ts +0 -750
  75. package/src/__tests__/server.test.ts +0 -452
  76. package/src/__tests__/session-archiver.test.ts +0 -524
  77. package/src/__tests__/session-state-generator.test.ts +0 -900
  78. package/src/__tests__/team-knowledge.test.ts +0 -327
  79. package/src/__tests__/tools.test.ts +0 -340
  80. package/src/__tests__/transcript-parser.test.ts +0 -195
  81. package/src/__tests__/trpc-index.test.ts +0 -25
  82. package/src/__tests__/validate-features-runner.test.ts +0 -517
  83. package/src/__tests__/validation-engine.test.ts +0 -300
  84. package/src/core-tools.ts +0 -685
  85. package/src/memory-queries.ts +0 -804
  86. package/src/memory-schema.ts +0 -546
  87. 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
- });