@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,316 +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 { resolve } from 'path';
7
- import { unlinkSync, existsSync } from 'fs';
8
- import {
9
- assignImportance,
10
- createSession,
11
- addObservation,
12
- addSummary,
13
- deduplicateFailedAttempt,
14
- getFailedAttempts,
15
- linkSessionToTask,
16
- getSessionsByTask,
17
- getCrossTaskProgress,
18
- autoDetectTaskId,
19
- getRecentObservations,
20
- getSessionSummaries,
21
- } from '../memory-db.ts';
22
- import { isNoisyToolCall, detectPlanProgress, classifyRealTimeToolCall } from '../observation-extractor.ts';
23
-
24
- // P7-006: Enhancement-Specific Tests
25
-
26
- const TEST_DB_PATH = resolve(__dirname, '../test-enhancements.db');
27
-
28
- function createTestDb(): Database.Database {
29
- if (existsSync(TEST_DB_PATH)) unlinkSync(TEST_DB_PATH);
30
- const db = new Database(TEST_DB_PATH);
31
- db.pragma('journal_mode = WAL');
32
- db.pragma('foreign_keys = ON');
33
-
34
- db.exec(`
35
- CREATE TABLE IF NOT EXISTS sessions (
36
- id INTEGER PRIMARY KEY AUTOINCREMENT,
37
- session_id TEXT UNIQUE NOT NULL,
38
- project TEXT NOT NULL DEFAULT 'my-project',
39
- git_branch TEXT,
40
- started_at TEXT NOT NULL,
41
- started_at_epoch INTEGER NOT NULL,
42
- ended_at TEXT, ended_at_epoch INTEGER,
43
- status TEXT CHECK(status IN ('active', 'completed', 'abandoned')) NOT NULL DEFAULT 'active',
44
- plan_file TEXT, plan_phase TEXT, task_id TEXT
45
- );
46
- CREATE INDEX IF NOT EXISTS idx_sessions_task_id ON sessions(task_id);
47
- CREATE TABLE IF NOT EXISTS observations (
48
- id INTEGER PRIMARY KEY AUTOINCREMENT,
49
- session_id TEXT NOT NULL,
50
- type TEXT NOT NULL CHECK(type IN (
51
- 'decision', 'bugfix', 'feature', 'refactor', 'discovery',
52
- 'cr_violation', 'vr_check', 'pattern_compliance', 'failed_attempt',
53
- 'file_change', 'incident_near_miss'
54
- )),
55
- title TEXT NOT NULL, detail TEXT,
56
- files_involved TEXT DEFAULT '[]', plan_item TEXT, cr_rule TEXT, vr_type TEXT, evidence TEXT,
57
- importance INTEGER NOT NULL DEFAULT 3 CHECK(importance BETWEEN 1 AND 5),
58
- recurrence_count INTEGER NOT NULL DEFAULT 1,
59
- original_tokens INTEGER DEFAULT 0,
60
- created_at TEXT NOT NULL, created_at_epoch INTEGER NOT NULL,
61
- FOREIGN KEY(session_id) REFERENCES sessions(session_id) ON DELETE CASCADE
62
- );
63
- CREATE INDEX IF NOT EXISTS idx_observations_importance ON observations(importance DESC);
64
- CREATE VIRTUAL TABLE IF NOT EXISTS observations_fts USING fts5(
65
- title, detail, evidence, content='observations', content_rowid='id'
66
- );
67
- CREATE TRIGGER IF NOT EXISTS observations_ai AFTER INSERT ON observations BEGIN
68
- INSERT INTO observations_fts(rowid, title, detail, evidence) VALUES (new.id, new.title, new.detail, new.evidence);
69
- END;
70
- CREATE TRIGGER IF NOT EXISTS observations_ad AFTER DELETE ON observations BEGIN
71
- INSERT INTO observations_fts(observations_fts, rowid, title, detail, evidence) VALUES ('delete', old.id, old.title, old.detail, old.evidence);
72
- END;
73
- CREATE TABLE IF NOT EXISTS session_summaries (
74
- id INTEGER PRIMARY KEY AUTOINCREMENT,
75
- session_id TEXT NOT NULL,
76
- request TEXT, investigated TEXT, decisions TEXT, completed TEXT,
77
- failed_attempts TEXT, next_steps TEXT,
78
- files_created TEXT DEFAULT '[]', files_modified TEXT DEFAULT '[]',
79
- verification_results TEXT DEFAULT '{}', plan_progress TEXT DEFAULT '{}',
80
- created_at TEXT NOT NULL, created_at_epoch INTEGER NOT NULL,
81
- FOREIGN KEY(session_id) REFERENCES sessions(session_id) ON DELETE CASCADE
82
- );
83
- CREATE TABLE IF NOT EXISTS user_prompts (
84
- id INTEGER PRIMARY KEY AUTOINCREMENT,
85
- session_id TEXT NOT NULL,
86
- prompt_text TEXT NOT NULL, prompt_number INTEGER NOT NULL DEFAULT 1,
87
- created_at TEXT NOT NULL, created_at_epoch INTEGER NOT NULL,
88
- FOREIGN KEY(session_id) REFERENCES sessions(session_id) ON DELETE CASCADE
89
- );
90
- CREATE TABLE IF NOT EXISTS memory_meta (key TEXT PRIMARY KEY, value TEXT NOT NULL);
91
- `);
92
-
93
- return db;
94
- }
95
-
96
- describe('Enhancement Features', () => {
97
- let db: Database.Database;
98
-
99
- beforeEach(() => {
100
- db = createTestDb();
101
- });
102
-
103
- afterEach(() => {
104
- db.close();
105
- if (existsSync(TEST_DB_PATH)) unlinkSync(TEST_DB_PATH);
106
- });
107
-
108
- describe('Importance Scoring', () => {
109
- it('assigns importance=5 for decisions', () => {
110
- expect(assignImportance('decision')).toBe(5);
111
- });
112
-
113
- it('assigns importance=5 for failed_attempts', () => {
114
- expect(assignImportance('failed_attempt')).toBe(5);
115
- });
116
-
117
- it('assigns importance=4 for cr_violation', () => {
118
- expect(assignImportance('cr_violation')).toBe(4);
119
- });
120
-
121
- it('assigns importance=4 for vr_check FAIL', () => {
122
- expect(assignImportance('vr_check', 'FAIL')).toBe(4);
123
- });
124
-
125
- it('assigns importance=2 for vr_check PASS', () => {
126
- expect(assignImportance('vr_check', 'PASS')).toBe(2);
127
- });
128
-
129
- it('assigns importance=3 for feature/bugfix', () => {
130
- expect(assignImportance('feature')).toBe(3);
131
- expect(assignImportance('bugfix')).toBe(3);
132
- });
133
-
134
- it('assigns importance=2 for refactor', () => {
135
- expect(assignImportance('refactor')).toBe(2);
136
- });
137
-
138
- it('assigns importance=1 for file_change/discovery', () => {
139
- expect(assignImportance('file_change')).toBe(1);
140
- expect(assignImportance('discovery')).toBe(1);
141
- });
142
-
143
- it('stores importance in observation', () => {
144
- createSession(db, 'session-1');
145
- const id = addObservation(db, 'session-1', 'decision', 'Important decision', null);
146
- const obs = db.prepare('SELECT importance FROM observations WHERE id = ?').get(id) as { importance: number };
147
- expect(obs.importance).toBe(5);
148
- });
149
- });
150
-
151
- describe('Noise Filtering', () => {
152
- it('filters Glob calls', () => {
153
- const seenReads = new Set<string>();
154
- expect(isNoisyToolCall({ toolName: 'Glob', toolUseId: '1', input: {}, result: 'files' }, seenReads)).toBe(true);
155
- });
156
-
157
- it('filters Grep calls', () => {
158
- const seenReads = new Set<string>();
159
- expect(isNoisyToolCall({ toolName: 'Grep', toolUseId: '1', input: {}, result: 'match' }, seenReads)).toBe(true);
160
- });
161
-
162
- it('filters trivial Bash: ls, pwd, echo', () => {
163
- const seenReads = new Set<string>();
164
- expect(isNoisyToolCall({ toolName: 'Bash', toolUseId: '1', input: { command: 'ls -la' }, result: 'output' }, seenReads)).toBe(true);
165
- expect(isNoisyToolCall({ toolName: 'Bash', toolUseId: '2', input: { command: 'pwd' }, result: '/path' }, seenReads)).toBe(true);
166
- expect(isNoisyToolCall({ toolName: 'Bash', toolUseId: '3', input: { command: 'echo hello' }, result: 'hello' }, seenReads)).toBe(true);
167
- });
168
-
169
- it('keeps Edit calls', () => {
170
- const seenReads = new Set<string>();
171
- expect(isNoisyToolCall({ toolName: 'Edit', toolUseId: '1', input: { file_path: '/f.ts' }, result: 'ok' }, seenReads)).toBe(false);
172
- });
173
-
174
- it('keeps Write calls', () => {
175
- const seenReads = new Set<string>();
176
- expect(isNoisyToolCall({ toolName: 'Write', toolUseId: '1', input: { file_path: '/f.ts' }, result: 'ok' }, seenReads)).toBe(false);
177
- });
178
-
179
- it('keeps npm test Bash calls', () => {
180
- const seenReads = new Set<string>();
181
- expect(isNoisyToolCall({ toolName: 'Bash', toolUseId: '1', input: { command: 'npm test' }, result: 'passed' }, seenReads)).toBe(false);
182
- });
183
- });
184
-
185
- describe('Token Budget Enforcement', () => {
186
- it('estimates tokens as chars/4', () => {
187
- const text = 'a'.repeat(100);
188
- expect(Math.ceil(text.length / 4)).toBe(25);
189
- });
190
-
191
- it('observations sorted by importance for budget filling', () => {
192
- createSession(db, 'session-1');
193
- addObservation(db, 'session-1', 'file_change', 'Low importance', null, { importance: 1 });
194
- addObservation(db, 'session-1', 'decision', 'High importance', null, { importance: 5 });
195
- addObservation(db, 'session-1', 'feature', 'Medium importance', null, { importance: 3 });
196
-
197
- const obs = getRecentObservations(db, 10, 'session-1');
198
- const sorted = [...obs].sort((a, b) => b.importance - a.importance);
199
- expect(sorted[0].importance).toBe(5);
200
- expect(sorted[1].importance).toBe(3);
201
- expect(sorted[2].importance).toBe(1);
202
- });
203
- });
204
-
205
- describe('Compaction-Aware Injection', () => {
206
- it('compact source gets 4000 tokens', () => {
207
- // Verify token budget logic
208
- const budgets: Record<string, number> = {
209
- compact: 4000,
210
- startup: 2000,
211
- resume: 1000,
212
- };
213
- expect(budgets.compact).toBe(4000);
214
- expect(budgets.startup).toBe(2000);
215
- expect(budgets.resume).toBe(1000);
216
- });
217
- });
218
-
219
- describe('Failed Attempt Deduplication', () => {
220
- it('increments recurrence_count for same title', () => {
221
- createSession(db, 'session-1');
222
- createSession(db, 'session-2');
223
-
224
- deduplicateFailedAttempt(db, 'session-1', 'process.cwd() wrong in tests', 'Detail 1');
225
- deduplicateFailedAttempt(db, 'session-2', 'process.cwd() wrong in tests', 'Detail 2');
226
-
227
- const failures = getFailedAttempts(db);
228
- expect(failures.length).toBe(1);
229
- expect(failures[0].recurrence_count).toBe(2);
230
- });
231
-
232
- it('creates new entry for different title', () => {
233
- createSession(db, 'session-1');
234
- deduplicateFailedAttempt(db, 'session-1', 'Error A', null);
235
- deduplicateFailedAttempt(db, 'session-1', 'Error B', null);
236
-
237
- const failures = getFailedAttempts(db);
238
- expect(failures.length).toBe(2);
239
- });
240
- });
241
-
242
- describe('Session Linking', () => {
243
- it('auto-detects task_id from plan file', () => {
244
- expect(autoDetectTaskId('/path/to/2026-01-30-massu-memory.md')).toBe('2026-01-30-massu-memory');
245
- });
246
-
247
- it('links sessions on creation with plan file', () => {
248
- createSession(db, 'session-1', { planFile: '/path/2026-01-30-plan.md' });
249
- createSession(db, 'session-2', { planFile: '/path/2026-01-30-plan.md' });
250
-
251
- const s1 = db.prepare('SELECT task_id FROM sessions WHERE session_id = ?').get('session-1') as { task_id: string };
252
- const s2 = db.prepare('SELECT task_id FROM sessions WHERE session_id = ?').get('session-2') as { task_id: string };
253
- expect(s1.task_id).toBe('2026-01-30-plan');
254
- expect(s2.task_id).toBe('2026-01-30-plan');
255
- });
256
-
257
- it('aggregates cross-task progress', () => {
258
- createSession(db, 'session-1', { planFile: '/path/plan.md' });
259
- createSession(db, 'session-2', { planFile: '/path/plan.md' });
260
-
261
- addSummary(db, 'session-1', { planProgress: { 'P1-001': 'complete' } });
262
- addSummary(db, 'session-2', { planProgress: { 'P2-001': 'complete' } });
263
-
264
- const progress = getCrossTaskProgress(db, 'plan');
265
- expect(progress['P1-001']).toBe('complete');
266
- expect(progress['P2-001']).toBe('complete');
267
- });
268
-
269
- it('gets all sessions for a task', () => {
270
- createSession(db, 'session-1');
271
- createSession(db, 'session-2');
272
- linkSessionToTask(db, 'session-1', 'my-task');
273
- linkSessionToTask(db, 'session-2', 'my-task');
274
-
275
- const sessions = getSessionsByTask(db, 'my-task');
276
- expect(sessions.length).toBe(2);
277
- });
278
- });
279
-
280
- describe('Plan Progress Auto-Detection', () => {
281
- it('detects P1-001 COMPLETE pattern', () => {
282
- const progress = detectPlanProgress('Verified P1-001: COMPLETE');
283
- expect(progress.length).toBe(1);
284
- expect(progress[0].planItem).toBe('P1-001');
285
- expect(progress[0].status).toBe('complete');
286
- });
287
-
288
- it('detects multiple plan items', () => {
289
- const progress = detectPlanProgress('P1-001: DONE, P2-003: PASS');
290
- expect(progress.length).toBe(2);
291
- });
292
-
293
- it('ignores non-matching text', () => {
294
- const progress = detectPlanProgress('Working on the project');
295
- expect(progress.length).toBe(0);
296
- });
297
- });
298
-
299
- describe('PreCompact Snapshot', () => {
300
- it('stores mid-session summary with pre_compact marker', () => {
301
- createSession(db, 'session-1');
302
- addObservation(db, 'session-1', 'feature', 'Added login page', null);
303
-
304
- // Simulate pre-compact summary
305
- addSummary(db, 'session-1', {
306
- completed: '- Added login page',
307
- planProgress: { snapshot_type: 'pre_compact', 'P1-001': 'in_progress' },
308
- });
309
-
310
- const summaries = getSessionSummaries(db, 5);
311
- expect(summaries.length).toBe(1);
312
- const progress = JSON.parse(summaries[0].plan_progress);
313
- expect(progress.snapshot_type).toBe('pre_compact');
314
- });
315
- });
316
- });
@@ -1,199 +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 { resolve } from 'path';
7
- import { unlinkSync, existsSync } from 'fs';
8
- import { handleMemoryToolCall, getMemoryToolDefinitions } from '../memory-tools.ts';
9
- import { createSession, addObservation, addSummary, addUserPrompt, endSession } from '../memory-db.ts';
10
-
11
- // P7-005: Memory Tools Tests
12
-
13
- const TEST_DB_PATH = resolve(__dirname, '../test-memory-tools.db');
14
-
15
- function createTestDb(): Database.Database {
16
- if (existsSync(TEST_DB_PATH)) unlinkSync(TEST_DB_PATH);
17
- const db = new Database(TEST_DB_PATH);
18
- db.pragma('journal_mode = WAL');
19
- db.pragma('foreign_keys = ON');
20
-
21
- db.exec(`
22
- CREATE TABLE IF NOT EXISTS sessions (
23
- id INTEGER PRIMARY KEY AUTOINCREMENT,
24
- session_id TEXT UNIQUE NOT NULL,
25
- project TEXT NOT NULL DEFAULT 'my-project',
26
- git_branch TEXT,
27
- started_at TEXT NOT NULL,
28
- started_at_epoch INTEGER NOT NULL,
29
- ended_at TEXT, ended_at_epoch INTEGER,
30
- status TEXT CHECK(status IN ('active', 'completed', 'abandoned')) NOT NULL DEFAULT 'active',
31
- plan_file TEXT, plan_phase TEXT, task_id TEXT
32
- );
33
- CREATE TABLE IF NOT EXISTS observations (
34
- id INTEGER PRIMARY KEY AUTOINCREMENT,
35
- session_id TEXT NOT NULL,
36
- type TEXT NOT NULL CHECK(type IN (
37
- 'decision', 'bugfix', 'feature', 'refactor', 'discovery',
38
- 'cr_violation', 'vr_check', 'pattern_compliance', 'failed_attempt',
39
- 'file_change', 'incident_near_miss'
40
- )),
41
- title TEXT NOT NULL, detail TEXT,
42
- files_involved TEXT DEFAULT '[]', plan_item TEXT, cr_rule TEXT, vr_type TEXT, evidence TEXT,
43
- importance INTEGER NOT NULL DEFAULT 3 CHECK(importance BETWEEN 1 AND 5),
44
- recurrence_count INTEGER NOT NULL DEFAULT 1,
45
- original_tokens INTEGER DEFAULT 0,
46
- created_at TEXT NOT NULL, created_at_epoch INTEGER NOT NULL,
47
- FOREIGN KEY(session_id) REFERENCES sessions(session_id) ON DELETE CASCADE
48
- );
49
- CREATE INDEX IF NOT EXISTS idx_observations_session ON observations(session_id);
50
- CREATE INDEX IF NOT EXISTS idx_observations_importance ON observations(importance DESC);
51
- CREATE VIRTUAL TABLE IF NOT EXISTS observations_fts USING fts5(
52
- title, detail, evidence, content='observations', content_rowid='id'
53
- );
54
- CREATE TRIGGER IF NOT EXISTS observations_ai AFTER INSERT ON observations BEGIN
55
- INSERT INTO observations_fts(rowid, title, detail, evidence) VALUES (new.id, new.title, new.detail, new.evidence);
56
- END;
57
- CREATE TRIGGER IF NOT EXISTS observations_ad AFTER DELETE ON observations BEGIN
58
- INSERT INTO observations_fts(observations_fts, rowid, title, detail, evidence) VALUES ('delete', old.id, old.title, old.detail, old.evidence);
59
- END;
60
- CREATE TABLE IF NOT EXISTS session_summaries (
61
- id INTEGER PRIMARY KEY AUTOINCREMENT,
62
- session_id TEXT NOT NULL,
63
- request TEXT, investigated TEXT, decisions TEXT, completed TEXT,
64
- failed_attempts TEXT, next_steps TEXT,
65
- files_created TEXT DEFAULT '[]', files_modified TEXT DEFAULT '[]',
66
- verification_results TEXT DEFAULT '{}', plan_progress TEXT DEFAULT '{}',
67
- created_at TEXT NOT NULL, created_at_epoch INTEGER NOT NULL,
68
- FOREIGN KEY(session_id) REFERENCES sessions(session_id) ON DELETE CASCADE
69
- );
70
- CREATE TABLE IF NOT EXISTS user_prompts (
71
- id INTEGER PRIMARY KEY AUTOINCREMENT,
72
- session_id TEXT NOT NULL,
73
- prompt_text TEXT NOT NULL, prompt_number INTEGER NOT NULL DEFAULT 1,
74
- created_at TEXT NOT NULL, created_at_epoch INTEGER NOT NULL,
75
- FOREIGN KEY(session_id) REFERENCES sessions(session_id) ON DELETE CASCADE
76
- );
77
- CREATE TABLE IF NOT EXISTS memory_meta (key TEXT PRIMARY KEY, value TEXT NOT NULL);
78
- `);
79
-
80
- return db;
81
- }
82
-
83
- describe('Memory Tools', () => {
84
- let db: Database.Database;
85
-
86
- beforeEach(() => {
87
- db = createTestDb();
88
- // Seed test data
89
- createSession(db, 'session-1', { branch: 'main' });
90
- addObservation(db, 'session-1', 'decision', 'Use FTS5 for search', 'Better performance than LIKE');
91
- addObservation(db, 'session-1', 'failed_attempt', 'Regex parser fails on nested braces', 'Stopped at first }');
92
- addObservation(db, 'session-1', 'feature', 'Implemented memory database', null, {
93
- filesInvolved: ['packages/core/src/memory-db.ts'],
94
- planItem: 'P1-001',
95
- });
96
- addObservation(db, 'session-1', 'vr_check', 'VR-BUILD: PASS', null, {
97
- vrType: 'VR-BUILD',
98
- evidence: 'Build completed successfully',
99
- importance: 2,
100
- });
101
- });
102
-
103
- afterEach(() => {
104
- db.close();
105
- if (existsSync(TEST_DB_PATH)) unlinkSync(TEST_DB_PATH);
106
- });
107
-
108
- describe('Tool Definitions', () => {
109
- it('returns 6 memory tool definitions', () => {
110
- const defs = getMemoryToolDefinitions();
111
- expect(defs.length).toBe(6);
112
- const names = defs.map(d => d.name);
113
- expect(names).toContain('massu_memory_search');
114
- expect(names).toContain('massu_memory_timeline');
115
- expect(names).toContain('massu_memory_detail');
116
- expect(names).toContain('massu_memory_sessions');
117
- expect(names).toContain('massu_memory_failures');
118
- expect(names).toContain('massu_memory_ingest');
119
- });
120
- });
121
-
122
- describe('massu_memory_search', () => {
123
- it('searches observations by text', () => {
124
- const result = handleMemoryToolCall('massu_memory_search', { query: 'FTS5' }, db);
125
- const text = result.content[0].text;
126
- expect(text).toContain('FTS5');
127
- expect(text).toContain('Search Results');
128
- });
129
-
130
- it('returns no results message', () => {
131
- const result = handleMemoryToolCall('massu_memory_search', { query: 'nonexistent12345' }, db);
132
- expect(result.content[0].text).toContain('No observations found');
133
- });
134
- });
135
-
136
- describe('massu_memory_timeline', () => {
137
- it('shows chronological context around observation', () => {
138
- const obs = db.prepare('SELECT id FROM observations LIMIT 1').get() as { id: number };
139
- const result = handleMemoryToolCall('massu_memory_timeline', { observation_id: obs.id }, db);
140
- expect(result.content[0].text).toContain('Timeline');
141
- expect(result.content[0].text).toContain('ANCHOR');
142
- });
143
- });
144
-
145
- describe('massu_memory_detail', () => {
146
- it('returns full observation details', () => {
147
- const obs = db.prepare('SELECT id FROM observations WHERE type = ?').all('decision') as { id: number }[];
148
- const ids = obs.map(o => o.id);
149
- const result = handleMemoryToolCall('massu_memory_detail', { ids }, db);
150
- expect(result.content[0].text).toContain('FTS5');
151
- expect(result.content[0].text).toContain('decision');
152
- });
153
- });
154
-
155
- describe('massu_memory_sessions', () => {
156
- it('lists recent sessions', () => {
157
- const result = handleMemoryToolCall('massu_memory_sessions', {}, db);
158
- // Session IDs are truncated to 8 chars in output: "session-..."
159
- expect(result.content[0].text).toContain('session-');
160
- expect(result.content[0].text).toContain('active');
161
- });
162
- });
163
-
164
- describe('massu_memory_failures', () => {
165
- it('lists failed attempts', () => {
166
- const result = handleMemoryToolCall('massu_memory_failures', {}, db);
167
- expect(result.content[0].text).toContain('Regex parser');
168
- expect(result.content[0].text).toContain('DO NOT RETRY');
169
- });
170
-
171
- it('filters by keyword', () => {
172
- const result = handleMemoryToolCall('massu_memory_failures', { query: 'regex' }, db);
173
- expect(result.content[0].text).toContain('Regex');
174
- });
175
- });
176
-
177
- describe('massu_memory_ingest', () => {
178
- it('records a manual observation', () => {
179
- const result = handleMemoryToolCall('massu_memory_ingest', {
180
- type: 'decision',
181
- title: 'Manual decision record',
182
- detail: 'Decided to use approach X',
183
- }, db);
184
- expect(result.content[0].text).toContain('recorded successfully');
185
-
186
- const obs = db.prepare("SELECT * FROM observations WHERE title = 'Manual decision record'").get() as Record<string, unknown>;
187
- expect(obs).toBeTruthy();
188
- expect(obs.type).toBe('decision');
189
- });
190
-
191
- it('rejects invalid type', () => {
192
- const result = handleMemoryToolCall('massu_memory_ingest', {
193
- type: 'invalid_type',
194
- title: 'test',
195
- }, db);
196
- expect(result.content[0].text).toContain('invalid type');
197
- });
198
- });
199
- });
@@ -1,177 +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, vi } from 'vitest';
5
- import Database from 'better-sqlite3';
6
- import { buildMiddlewareTree, isInMiddlewareTree, getMiddlewareTree } from '../middleware-tree.ts';
7
-
8
- // Mock config
9
- vi.mock('../config.ts', () => ({
10
- getConfig: () => ({
11
- toolPrefix: 'massu',
12
- framework: { type: 'typescript', router: 'trpc', orm: 'prisma' },
13
- paths: {
14
- source: 'src',
15
- routers: 'src/server/api/routers',
16
- middleware: 'src/middleware.ts',
17
- },
18
- domains: [],
19
- }),
20
- getProjectRoot: () => '/test/project',
21
- }));
22
-
23
- function createTestDb(): Database.Database {
24
- const db = new Database(':memory:');
25
- db.pragma('journal_mode = WAL');
26
-
27
- db.exec(`
28
- CREATE TABLE IF NOT EXISTS massu_imports (
29
- id INTEGER PRIMARY KEY AUTOINCREMENT,
30
- source_file TEXT NOT NULL,
31
- target_file TEXT NOT NULL,
32
- import_type TEXT NOT NULL,
33
- imported_names TEXT NOT NULL DEFAULT '[]',
34
- line INTEGER NOT NULL DEFAULT 0
35
- );
36
-
37
- CREATE TABLE IF NOT EXISTS massu_middleware_tree (
38
- id INTEGER PRIMARY KEY AUTOINCREMENT,
39
- file TEXT NOT NULL UNIQUE
40
- );
41
- `);
42
-
43
- return db;
44
- }
45
-
46
- describe('Middleware Tree', () => {
47
- let db: Database.Database;
48
-
49
- beforeEach(() => {
50
- db = createTestDb();
51
- });
52
-
53
- afterEach(() => {
54
- db.close();
55
- });
56
-
57
- describe('buildMiddlewareTree', () => {
58
- it('builds middleware tree from imports', () => {
59
- // Seed import edges
60
- db.prepare(`
61
- INSERT INTO massu_imports (source_file, target_file, import_type)
62
- VALUES
63
- ('src/middleware.ts', 'src/lib/auth.ts', 'named'),
64
- ('src/lib/auth.ts', 'src/lib/session.ts', 'named'),
65
- ('src/lib/auth.ts', 'src/utils/crypto.ts', 'named')
66
- `).run();
67
-
68
- const count = buildMiddlewareTree(db);
69
- expect(count).toBeGreaterThan(0);
70
-
71
- const files = getMiddlewareTree(db);
72
- expect(files).toContain('src/middleware.ts');
73
- expect(files).toContain('src/lib/auth.ts');
74
- expect(files).toContain('src/lib/session.ts');
75
- expect(files).toContain('src/utils/crypto.ts');
76
- });
77
-
78
- it('includes transitive imports', () => {
79
- db.prepare(`
80
- INSERT INTO massu_imports (source_file, target_file, import_type)
81
- VALUES
82
- ('src/middleware.ts', 'src/lib/auth.ts', 'named'),
83
- ('src/lib/auth.ts', 'src/lib/session.ts', 'named'),
84
- ('src/lib/session.ts', 'src/lib/database.ts', 'named')
85
- `).run();
86
-
87
- buildMiddlewareTree(db);
88
- const files = getMiddlewareTree(db);
89
- expect(files).toContain('src/lib/database.ts');
90
- });
91
-
92
- it('ignores non-src imports', () => {
93
- db.prepare(`
94
- INSERT INTO massu_imports (source_file, target_file, import_type)
95
- VALUES
96
- ('src/middleware.ts', 'src/lib/auth.ts', 'named'),
97
- ('src/lib/auth.ts', 'node_modules/lodash/index.js', 'named')
98
- `).run();
99
-
100
- buildMiddlewareTree(db);
101
- const files = getMiddlewareTree(db);
102
- expect(files).not.toContain('node_modules/lodash/index.js');
103
- });
104
-
105
- it('clears existing tree before rebuild', () => {
106
- db.prepare(`INSERT INTO massu_middleware_tree (file) VALUES ('old-file.ts')`).run();
107
-
108
- db.prepare(`
109
- INSERT INTO massu_imports (source_file, target_file, import_type)
110
- VALUES ('src/middleware.ts', 'src/lib/new.ts', 'named')
111
- `).run();
112
-
113
- buildMiddlewareTree(db);
114
- const files = getMiddlewareTree(db);
115
- expect(files).not.toContain('old-file.ts');
116
- expect(files).toContain('src/middleware.ts');
117
- });
118
-
119
- it('handles missing middleware path gracefully', () => {
120
- // Note: vi.mock doesn't work inside a test, so we test with configured path
121
- // The actual function returns 0 for null/undefined middleware path, but
122
- // our mock always has middleware configured, so we just verify it doesn't throw
123
- db.prepare(`
124
- INSERT INTO massu_imports (source_file, target_file, import_type)
125
- VALUES ('src/middleware.ts', 'src/lib/auth.ts', 'named')
126
- `).run();
127
-
128
- const count = buildMiddlewareTree(db);
129
- expect(count).toBeGreaterThanOrEqual(0);
130
- });
131
- });
132
-
133
- describe('isInMiddlewareTree', () => {
134
- beforeEach(() => {
135
- db.prepare(`
136
- INSERT INTO massu_imports (source_file, target_file, import_type)
137
- VALUES
138
- ('src/middleware.ts', 'src/lib/auth.ts', 'named'),
139
- ('src/lib/auth.ts', 'src/lib/session.ts', 'named')
140
- `).run();
141
- buildMiddlewareTree(db);
142
- });
143
-
144
- it('returns true for files in tree', () => {
145
- expect(isInMiddlewareTree(db, 'src/middleware.ts')).toBe(true);
146
- expect(isInMiddlewareTree(db, 'src/lib/auth.ts')).toBe(true);
147
- expect(isInMiddlewareTree(db, 'src/lib/session.ts')).toBe(true);
148
- });
149
-
150
- it('returns false for files not in tree', () => {
151
- expect(isInMiddlewareTree(db, 'src/app/page.tsx')).toBe(false);
152
- expect(isInMiddlewareTree(db, 'src/utils/helpers.ts')).toBe(false);
153
- });
154
- });
155
-
156
- describe('getMiddlewareTree', () => {
157
- it('returns all files in tree sorted', () => {
158
- db.prepare(`
159
- INSERT INTO massu_imports (source_file, target_file, import_type)
160
- VALUES
161
- ('src/middleware.ts', 'src/lib/b.ts', 'named'),
162
- ('src/middleware.ts', 'src/lib/a.ts', 'named'),
163
- ('src/middleware.ts', 'src/lib/c.ts', 'named')
164
- `).run();
165
- buildMiddlewareTree(db);
166
-
167
- const files = getMiddlewareTree(db);
168
- expect(files.length).toBe(4); // middleware + 3 imports
169
- expect(files).toEqual([...files].sort()); // Check if sorted
170
- });
171
-
172
- it('returns empty array when tree is empty', () => {
173
- const files = getMiddlewareTree(db);
174
- expect(files).toEqual([]);
175
- });
176
- });
177
- });