@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,437 +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, writeFileSync } from 'fs';
8
- import {
9
- createSession,
10
- addConversationTurn,
11
- addToolCallDetail,
12
- getConversationTurns,
13
- searchConversationTurns,
14
- getToolPatterns,
15
- getSessionStats,
16
- getObservabilityDbSize,
17
- pruneOldConversationTurns,
18
- getLastProcessedLine,
19
- setLastProcessedLine,
20
- } from '../memory-db.ts';
21
- import { parseTranscriptFrom } from '../transcript-parser.ts';
22
-
23
- // Test database path
24
- const TEST_DB_PATH = resolve(__dirname, '../test-observability.db');
25
-
26
- /**
27
- * Create a test database with the full memory schema including observability tables.
28
- * Mirrors initMemorySchema() from memory-db.ts.
29
- */
30
- function createTestDb(): Database.Database {
31
- if (existsSync(TEST_DB_PATH)) {
32
- unlinkSync(TEST_DB_PATH);
33
- }
34
-
35
- const db = new Database(TEST_DB_PATH);
36
- db.pragma('journal_mode = WAL');
37
- db.pragma('foreign_keys = ON');
38
-
39
- // Base schema
40
- db.exec(`
41
- CREATE TABLE IF NOT EXISTS sessions (
42
- id INTEGER PRIMARY KEY AUTOINCREMENT,
43
- session_id TEXT UNIQUE NOT NULL,
44
- project TEXT NOT NULL DEFAULT 'my-project',
45
- git_branch TEXT,
46
- started_at TEXT NOT NULL,
47
- started_at_epoch INTEGER NOT NULL,
48
- ended_at TEXT,
49
- ended_at_epoch INTEGER,
50
- status TEXT CHECK(status IN ('active', 'completed', 'abandoned')) NOT NULL DEFAULT 'active',
51
- plan_file TEXT,
52
- plan_phase TEXT,
53
- task_id TEXT
54
- );
55
-
56
- CREATE TABLE IF NOT EXISTS observations (
57
- id INTEGER PRIMARY KEY AUTOINCREMENT,
58
- session_id TEXT NOT NULL,
59
- type TEXT NOT NULL CHECK(type IN (
60
- 'decision', 'bugfix', 'feature', 'refactor', 'discovery',
61
- 'cr_violation', 'vr_check', 'pattern_compliance', 'failed_attempt',
62
- 'file_change', 'incident_near_miss'
63
- )),
64
- title TEXT NOT NULL,
65
- detail TEXT,
66
- files_involved TEXT DEFAULT '[]',
67
- plan_item TEXT,
68
- cr_rule TEXT,
69
- vr_type TEXT,
70
- evidence TEXT,
71
- importance INTEGER NOT NULL DEFAULT 3 CHECK(importance BETWEEN 1 AND 5),
72
- recurrence_count INTEGER NOT NULL DEFAULT 1,
73
- original_tokens INTEGER DEFAULT 0,
74
- created_at TEXT NOT NULL,
75
- created_at_epoch INTEGER NOT NULL,
76
- FOREIGN KEY(session_id) REFERENCES sessions(session_id) ON DELETE CASCADE
77
- );
78
-
79
- CREATE TABLE IF NOT EXISTS memory_meta (
80
- key TEXT PRIMARY KEY,
81
- value TEXT NOT NULL
82
- );
83
-
84
- -- Observability tables (P1-001, P1-002)
85
- CREATE TABLE IF NOT EXISTS conversation_turns (
86
- id INTEGER PRIMARY KEY AUTOINCREMENT,
87
- session_id TEXT NOT NULL,
88
- turn_number INTEGER NOT NULL,
89
- user_prompt TEXT NOT NULL,
90
- assistant_response TEXT,
91
- tool_calls_json TEXT,
92
- tool_call_count INTEGER DEFAULT 0,
93
- model_used TEXT,
94
- duration_ms INTEGER,
95
- prompt_tokens INTEGER,
96
- response_tokens INTEGER,
97
- created_at TEXT DEFAULT (datetime('now')),
98
- created_at_epoch INTEGER DEFAULT (unixepoch()),
99
- FOREIGN KEY (session_id) REFERENCES sessions(session_id)
100
- );
101
-
102
- CREATE INDEX IF NOT EXISTS idx_ct_session ON conversation_turns(session_id);
103
- CREATE INDEX IF NOT EXISTS idx_ct_created ON conversation_turns(created_at DESC);
104
- CREATE INDEX IF NOT EXISTS idx_ct_turn ON conversation_turns(session_id, turn_number);
105
-
106
- CREATE TABLE IF NOT EXISTS tool_call_details (
107
- id INTEGER PRIMARY KEY AUTOINCREMENT,
108
- session_id TEXT NOT NULL,
109
- turn_number INTEGER NOT NULL,
110
- tool_name TEXT NOT NULL,
111
- tool_input_summary TEXT,
112
- tool_input_size INTEGER,
113
- tool_output_size INTEGER,
114
- tool_success INTEGER DEFAULT 1,
115
- duration_ms INTEGER,
116
- files_involved TEXT,
117
- created_at TEXT DEFAULT (datetime('now')),
118
- created_at_epoch INTEGER DEFAULT (unixepoch()),
119
- FOREIGN KEY (session_id) REFERENCES sessions(session_id)
120
- );
121
-
122
- CREATE INDEX IF NOT EXISTS idx_tcd_session ON tool_call_details(session_id);
123
- CREATE INDEX IF NOT EXISTS idx_tcd_tool ON tool_call_details(tool_name);
124
- CREATE INDEX IF NOT EXISTS idx_tcd_created ON tool_call_details(created_at DESC);
125
- `);
126
-
127
- // FTS5 for conversation turns (P1-003)
128
- try {
129
- db.exec(`
130
- CREATE VIRTUAL TABLE IF NOT EXISTS conversation_turns_fts USING fts5(
131
- user_prompt,
132
- assistant_response,
133
- content=conversation_turns,
134
- content_rowid=id
135
- );
136
- `);
137
- } catch (_e) {
138
- // ignore
139
- }
140
-
141
- db.exec(`
142
- CREATE TRIGGER IF NOT EXISTS ct_fts_insert AFTER INSERT ON conversation_turns BEGIN
143
- INSERT INTO conversation_turns_fts(rowid, user_prompt, assistant_response)
144
- VALUES (new.id, new.user_prompt, new.assistant_response);
145
- END;
146
-
147
- CREATE TRIGGER IF NOT EXISTS ct_fts_delete AFTER DELETE ON conversation_turns BEGIN
148
- INSERT INTO conversation_turns_fts(conversation_turns_fts, rowid, user_prompt, assistant_response)
149
- VALUES ('delete', old.id, old.user_prompt, old.assistant_response);
150
- END;
151
-
152
- CREATE TRIGGER IF NOT EXISTS ct_fts_update AFTER UPDATE ON conversation_turns BEGIN
153
- INSERT INTO conversation_turns_fts(conversation_turns_fts, rowid, user_prompt, assistant_response)
154
- VALUES ('delete', old.id, old.user_prompt, old.assistant_response);
155
- INSERT INTO conversation_turns_fts(rowid, user_prompt, assistant_response)
156
- VALUES (new.id, new.user_prompt, new.assistant_response);
157
- END;
158
- `);
159
-
160
- return db;
161
- }
162
-
163
- describe('Observability', () => {
164
- let db: Database.Database;
165
-
166
- beforeEach(() => {
167
- db = createTestDb();
168
- // Create a test session
169
- createSession(db, 'test-session-001');
170
- });
171
-
172
- afterEach(() => {
173
- db.close();
174
- if (existsSync(TEST_DB_PATH)) {
175
- unlinkSync(TEST_DB_PATH);
176
- }
177
- });
178
-
179
- describe('Schema Creation', () => {
180
- it('creates conversation_turns table', () => {
181
- const tables = db.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name='conversation_turns'").all() as { name: string }[];
182
- expect(tables.length).toBe(1);
183
- });
184
-
185
- it('creates tool_call_details table', () => {
186
- const tables = db.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name='tool_call_details'").all() as { name: string }[];
187
- expect(tables.length).toBe(1);
188
- });
189
-
190
- it('creates conversation_turns_fts virtual table', () => {
191
- const tables = db.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name LIKE 'conversation_turns_fts%'").all() as { name: string }[];
192
- expect(tables.length).toBeGreaterThan(0);
193
- });
194
- });
195
-
196
- describe('addConversationTurn', () => {
197
- it('inserts and retrieves a conversation turn', () => {
198
- const id = addConversationTurn(
199
- db, 'test-session-001', 1,
200
- 'How do I fix the login bug?',
201
- 'You need to check the auth middleware for the session expiry...',
202
- JSON.stringify([{ name: 'Read', input_summary: 'Read src/middleware.ts', is_error: false }]),
203
- 1, 50, 200
204
- );
205
-
206
- expect(id).toBeGreaterThan(0);
207
-
208
- const turns = getConversationTurns(db, 'test-session-001');
209
- expect(turns.length).toBe(1);
210
- expect(turns[0].turn_number).toBe(1);
211
- expect(turns[0].user_prompt).toBe('How do I fix the login bug?');
212
- expect(turns[0].tool_call_count).toBe(1);
213
- });
214
-
215
- it('truncates assistant_response to 10000 chars', () => {
216
- const longResponse = 'A'.repeat(15000);
217
- addConversationTurn(db, 'test-session-001', 1, 'test', longResponse, null, 0, 10, 3750);
218
-
219
- const turns = getConversationTurns(db, 'test-session-001');
220
- expect(turns[0].assistant_response!.length).toBe(10000);
221
- });
222
-
223
- it('supports turn range filtering', () => {
224
- addConversationTurn(db, 'test-session-001', 1, 'Turn 1', 'Response 1', null, 0, 10, 20);
225
- addConversationTurn(db, 'test-session-001', 2, 'Turn 2', 'Response 2', null, 0, 10, 20);
226
- addConversationTurn(db, 'test-session-001', 3, 'Turn 3', 'Response 3', null, 0, 10, 20);
227
-
228
- const filtered = getConversationTurns(db, 'test-session-001', { turnFrom: 2, turnTo: 3 });
229
- expect(filtered.length).toBe(2);
230
- expect(filtered[0].turn_number).toBe(2);
231
- expect(filtered[1].turn_number).toBe(3);
232
- });
233
- });
234
-
235
- describe('addToolCallDetail', () => {
236
- it('inserts and retrieves tool call details', () => {
237
- addToolCallDetail(db, 'test-session-001', 1, 'Read', 'Read src/index.ts', 50, 1200, true, ['src/index.ts']);
238
- addToolCallDetail(db, 'test-session-001', 1, 'Edit', 'Edit src/index.ts', 200, 50, true, ['src/index.ts']);
239
- addToolCallDetail(db, 'test-session-001', 1, 'Bash', '$ npm run build', 30, 5000, false);
240
-
241
- const details = db.prepare('SELECT * FROM tool_call_details WHERE session_id = ? ORDER BY id').all('test-session-001') as Array<Record<string, unknown>>;
242
- expect(details.length).toBe(3);
243
- expect(details[0].tool_name).toBe('Read');
244
- expect(details[0].tool_success).toBe(1);
245
- expect(details[2].tool_name).toBe('Bash');
246
- expect(details[2].tool_success).toBe(0);
247
- });
248
-
249
- it('truncates tool_input_summary to 500 chars', () => {
250
- const longSummary = 'X'.repeat(1000);
251
- addToolCallDetail(db, 'test-session-001', 1, 'Read', longSummary, 1000, 100, true);
252
-
253
- const details = db.prepare('SELECT tool_input_summary FROM tool_call_details WHERE session_id = ?').all('test-session-001') as Array<{ tool_input_summary: string }>;
254
- expect(details[0].tool_input_summary.length).toBe(500);
255
- });
256
- });
257
-
258
- describe('FTS5 Search', () => {
259
- it('searches conversation turns by prompt text', () => {
260
- addConversationTurn(db, 'test-session-001', 1, 'Fix the authentication middleware', 'Check the JWT token...', null, 0, 50, 100);
261
- addConversationTurn(db, 'test-session-001', 2, 'Run the database migration', 'Use npx prisma migrate...', null, 0, 30, 80);
262
-
263
- const results = searchConversationTurns(db, 'authentication');
264
- expect(results.length).toBe(1);
265
- expect(results[0].user_prompt).toContain('authentication');
266
- });
267
-
268
- it('filters by session_id', () => {
269
- createSession(db, 'test-session-002');
270
- addConversationTurn(db, 'test-session-001', 1, 'Fix the bug', 'Response 1', null, 0, 10, 20);
271
- addConversationTurn(db, 'test-session-002', 1, 'Fix the bug', 'Response 2', null, 0, 10, 20);
272
-
273
- const results = searchConversationTurns(db, 'bug', { sessionId: 'test-session-002' });
274
- expect(results.length).toBe(1);
275
- expect(results[0].session_id).toBe('test-session-002');
276
- });
277
-
278
- it('filters by min_tool_calls', () => {
279
- addConversationTurn(db, 'test-session-001', 1, 'Simple question about code', 'Answer', null, 0, 10, 20);
280
- addConversationTurn(db, 'test-session-001', 2, 'Complex question about code', 'Answer with tools', '[]', 5, 10, 200);
281
-
282
- const results = searchConversationTurns(db, 'question', { minToolCalls: 3 });
283
- expect(results.length).toBe(1);
284
- expect(results[0].tool_call_count).toBe(5);
285
- });
286
- });
287
-
288
- describe('Tool Patterns', () => {
289
- it('aggregates tool usage by tool name', () => {
290
- addToolCallDetail(db, 'test-session-001', 1, 'Read', 'Read file1', 50, 1000, true);
291
- addToolCallDetail(db, 'test-session-001', 1, 'Read', 'Read file2', 60, 2000, true);
292
- addToolCallDetail(db, 'test-session-001', 2, 'Edit', 'Edit file1', 200, 50, true);
293
- addToolCallDetail(db, 'test-session-001', 2, 'Bash', '$ npm test', 30, 500, false);
294
-
295
- const patterns = getToolPatterns(db, { groupBy: 'tool' });
296
- expect(patterns.length).toBe(3);
297
-
298
- const readPattern = patterns.find(p => p.tool_name === 'Read');
299
- expect(readPattern).toBeDefined();
300
- expect(readPattern!.call_count).toBe(2);
301
- expect(readPattern!.successes).toBe(2);
302
-
303
- const bashPattern = patterns.find(p => p.tool_name === 'Bash');
304
- expect(bashPattern!.failures).toBe(1);
305
- });
306
-
307
- it('groups by session', () => {
308
- createSession(db, 'test-session-002');
309
- addToolCallDetail(db, 'test-session-001', 1, 'Read', 'Read file', 50, 1000, true);
310
- addToolCallDetail(db, 'test-session-002', 1, 'Read', 'Read file', 50, 2000, true);
311
- addToolCallDetail(db, 'test-session-002', 1, 'Edit', 'Edit file', 200, 50, true);
312
-
313
- const patterns = getToolPatterns(db, { groupBy: 'session' });
314
- expect(patterns.length).toBe(2);
315
- });
316
- });
317
-
318
- describe('Session Stats', () => {
319
- it('returns session statistics', () => {
320
- addConversationTurn(db, 'test-session-001', 1, 'Question 1', 'Answer 1', null, 2, 50, 200);
321
- addConversationTurn(db, 'test-session-001', 2, 'Question 2', 'Answer 2', null, 3, 40, 150);
322
- addToolCallDetail(db, 'test-session-001', 1, 'Read', 'file', 50, 1000, true);
323
-
324
- const stats = getSessionStats(db, { sessionId: 'test-session-001' });
325
- expect(stats.length).toBe(1);
326
- expect(stats[0].turn_count).toBe(2);
327
- expect(stats[0].total_tool_calls).toBe(5); // 2 + 3
328
- });
329
-
330
- it('returns multi-session overview', () => {
331
- createSession(db, 'test-session-002');
332
- addConversationTurn(db, 'test-session-001', 1, 'Q1', 'A1', null, 1, 10, 20);
333
- addConversationTurn(db, 'test-session-002', 1, 'Q1', 'A1', null, 2, 10, 20);
334
-
335
- const stats = getSessionStats(db, { limit: 10 });
336
- expect(stats.length).toBe(2);
337
- });
338
- });
339
-
340
- describe('Database Size Monitoring', () => {
341
- it('returns accurate counts', () => {
342
- addConversationTurn(db, 'test-session-001', 1, 'Q', 'A', null, 0, 10, 20);
343
- addToolCallDetail(db, 'test-session-001', 1, 'Read', 'file', 50, 1000, true);
344
-
345
- const size = getObservabilityDbSize(db);
346
- expect(size.conversation_turns_count).toBe(1);
347
- expect(size.tool_call_details_count).toBe(1);
348
- expect(size.estimated_size_mb).toBeGreaterThanOrEqual(0);
349
- });
350
- });
351
-
352
- describe('Pruning', () => {
353
- it('deletes old conversation turns and tool call details', () => {
354
- // Insert records with old timestamps
355
- db.prepare(`
356
- INSERT INTO conversation_turns (session_id, turn_number, user_prompt, created_at_epoch)
357
- VALUES (?, ?, ?, ?)
358
- `).run('test-session-001', 1, 'Old question', Math.floor(Date.now() / 1000) - (100 * 86400)); // 100 days ago
359
-
360
- db.prepare(`
361
- INSERT INTO tool_call_details (session_id, turn_number, tool_name, created_at_epoch)
362
- VALUES (?, ?, ?, ?)
363
- `).run('test-session-001', 1, 'Read', Math.floor(Date.now() / 1000) - (100 * 86400));
364
-
365
- // Insert recent records
366
- addConversationTurn(db, 'test-session-001', 2, 'Recent question', 'Answer', null, 0, 10, 20);
367
- addToolCallDetail(db, 'test-session-001', 2, 'Edit', 'file', 50, 100, true);
368
-
369
- const result = pruneOldConversationTurns(db, 90);
370
- expect(result.turnsDeleted).toBe(1);
371
- expect(result.detailsDeleted).toBe(1);
372
-
373
- // Verify recent records remain
374
- const remaining = db.prepare('SELECT COUNT(*) as c FROM conversation_turns').get() as { c: number };
375
- expect(remaining.c).toBe(1);
376
- });
377
- });
378
-
379
- describe('Incremental Parsing State', () => {
380
- it('tracks last processed line', () => {
381
- expect(getLastProcessedLine(db, 'test-session-001')).toBe(0);
382
-
383
- setLastProcessedLine(db, 'test-session-001', 150);
384
- expect(getLastProcessedLine(db, 'test-session-001')).toBe(150);
385
-
386
- // Update existing value
387
- setLastProcessedLine(db, 'test-session-001', 300);
388
- expect(getLastProcessedLine(db, 'test-session-001')).toBe(300);
389
- });
390
-
391
- it('tracks different sessions independently', () => {
392
- setLastProcessedLine(db, 'test-session-001', 100);
393
- setLastProcessedLine(db, 'test-session-002', 200);
394
-
395
- expect(getLastProcessedLine(db, 'test-session-001')).toBe(100);
396
- expect(getLastProcessedLine(db, 'test-session-002')).toBe(200);
397
- });
398
- });
399
-
400
- describe('parseTranscriptFrom', () => {
401
- const MOCK_TRANSCRIPT_PATH = resolve(__dirname, '../test-transcript.jsonl');
402
-
403
- afterEach(() => {
404
- if (existsSync(MOCK_TRANSCRIPT_PATH)) {
405
- unlinkSync(MOCK_TRANSCRIPT_PATH);
406
- }
407
- });
408
-
409
- it('parses from a specific line', async () => {
410
- const lines = [
411
- JSON.stringify({ type: 'user', message: { role: 'user', content: [{ type: 'text', text: 'First message' }] } }),
412
- JSON.stringify({ type: 'assistant', message: { role: 'assistant', content: [{ type: 'text', text: 'First response' }] } }),
413
- JSON.stringify({ type: 'user', message: { role: 'user', content: [{ type: 'text', text: 'Second message' }] } }),
414
- JSON.stringify({ type: 'assistant', message: { role: 'assistant', content: [{ type: 'text', text: 'Second response' }] } }),
415
- ];
416
- writeFileSync(MOCK_TRANSCRIPT_PATH, lines.join('\n') + '\n');
417
-
418
- // Parse from line 2 (skip first 2 lines)
419
- const { entries, totalLines } = await parseTranscriptFrom(MOCK_TRANSCRIPT_PATH, 2);
420
- expect(totalLines).toBe(4);
421
- expect(entries.length).toBe(2);
422
- expect(entries[0].type).toBe('user');
423
- expect(entries[1].type).toBe('assistant');
424
- });
425
-
426
- it('returns empty entries when no new lines', async () => {
427
- const lines = [
428
- JSON.stringify({ type: 'user', message: { role: 'user', content: [{ type: 'text', text: 'Only message' }] } }),
429
- ];
430
- writeFileSync(MOCK_TRANSCRIPT_PATH, lines.join('\n') + '\n');
431
-
432
- const { entries, totalLines } = await parseTranscriptFrom(MOCK_TRANSCRIPT_PATH, 1);
433
- expect(totalLines).toBe(1);
434
- expect(entries.length).toBe(0);
435
- });
436
- });
437
- });
@@ -1,167 +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 } from 'vitest';
5
- import {
6
- isNoisyToolCall,
7
- extractObservationsFromEntries,
8
- classifyRealTimeToolCall,
9
- detectPlanProgress,
10
- } from '../observation-extractor.ts';
11
- import type { ParsedToolCall, TranscriptEntry } from '../transcript-parser.ts';
12
-
13
- // P7-003: Observation Extractor Tests
14
-
15
- describe('Observation Extractor', () => {
16
- describe('isNoisyToolCall', () => {
17
- it('filters Glob calls', () => {
18
- const seenReads = new Set<string>();
19
- expect(isNoisyToolCall({
20
- toolName: 'Glob', toolUseId: '1', input: { pattern: '**/*.ts' }, result: 'file1.ts\nfile2.ts',
21
- }, seenReads)).toBe(true);
22
- });
23
-
24
- it('filters Grep calls', () => {
25
- const seenReads = new Set<string>();
26
- expect(isNoisyToolCall({
27
- toolName: 'Grep', toolUseId: '1', input: { pattern: 'test' }, result: 'match',
28
- }, seenReads)).toBe(true);
29
- });
30
-
31
- it('filters duplicate Read calls', () => {
32
- const seenReads = new Set<string>();
33
- const readCall: ParsedToolCall = {
34
- toolName: 'Read', toolUseId: '1', input: { file_path: '/path/file.ts' }, result: 'content',
35
- };
36
- expect(isNoisyToolCall(readCall, seenReads)).toBe(false); // First read is kept
37
- expect(isNoisyToolCall({ ...readCall, toolUseId: '2' }, seenReads)).toBe(true); // Duplicate filtered
38
- });
39
-
40
- it('filters node_modules reads', () => {
41
- const seenReads = new Set<string>();
42
- expect(isNoisyToolCall({
43
- toolName: 'Read', toolUseId: '1', input: { file_path: '/path/node_modules/pkg/index.js' }, result: 'code',
44
- }, seenReads)).toBe(true);
45
- });
46
-
47
- it('filters trivial Bash commands', () => {
48
- const seenReads = new Set<string>();
49
- expect(isNoisyToolCall({
50
- toolName: 'Bash', toolUseId: '1', input: { command: 'ls -la' }, result: 'files',
51
- }, seenReads)).toBe(true);
52
- expect(isNoisyToolCall({
53
- toolName: 'Bash', toolUseId: '2', input: { command: 'pwd' }, result: '/path',
54
- }, seenReads)).toBe(true);
55
- });
56
-
57
- it('keeps Edit/Write calls', () => {
58
- const seenReads = new Set<string>();
59
- expect(isNoisyToolCall({
60
- toolName: 'Edit', toolUseId: '1', input: { file_path: '/path/file.ts' }, result: 'edited',
61
- }, seenReads)).toBe(false);
62
- expect(isNoisyToolCall({
63
- toolName: 'Write', toolUseId: '2', input: { file_path: '/path/new.ts' }, result: 'written',
64
- }, seenReads)).toBe(false);
65
- });
66
-
67
- it('filters empty results', () => {
68
- const seenReads = new Set<string>();
69
- expect(isNoisyToolCall({
70
- toolName: 'Bash', toolUseId: '1', input: { command: 'npm install' }, result: '',
71
- }, seenReads)).toBe(true);
72
- });
73
- });
74
-
75
- describe('classifyRealTimeToolCall', () => {
76
- it('classifies Edit as file_change', () => {
77
- const seenReads = new Set<string>();
78
- const result = classifyRealTimeToolCall('Edit', { file_path: '/path/file.ts' }, 'success', seenReads);
79
- expect(result).not.toBeNull();
80
- expect(result!.type).toBe('file_change');
81
- expect(result!.title).toContain('Edited');
82
- });
83
-
84
- it('classifies Write as file_change', () => {
85
- const seenReads = new Set<string>();
86
- const result = classifyRealTimeToolCall('Write', { file_path: '/path/new.ts' }, 'created', seenReads);
87
- expect(result).not.toBeNull();
88
- expect(result!.type).toBe('file_change');
89
- expect(result!.title).toContain('Created');
90
- });
91
-
92
- it('classifies npm test as vr_check', () => {
93
- const seenReads = new Set<string>();
94
- const result = classifyRealTimeToolCall('Bash', { command: 'npm test' }, 'All 50 tests passed', seenReads);
95
- expect(result).not.toBeNull();
96
- expect(result!.type).toBe('vr_check');
97
- expect(result!.title).toContain('PASS');
98
- });
99
-
100
- it('classifies git commit as feature/bugfix', () => {
101
- const seenReads = new Set<string>();
102
- const result = classifyRealTimeToolCall('Bash', { command: 'git commit -m "feat: add new feature"' }, 'committed', seenReads);
103
- expect(result).not.toBeNull();
104
- expect(result!.type).toBe('feature');
105
- });
106
-
107
- it('returns null for noisy calls', () => {
108
- const seenReads = new Set<string>();
109
- const result = classifyRealTimeToolCall('Glob', { pattern: '**/*.ts' }, 'files', seenReads);
110
- expect(result).toBeNull();
111
- });
112
- });
113
-
114
- describe('detectPlanProgress', () => {
115
- it('detects plan items marked complete', () => {
116
- const progress = detectPlanProgress('P1-001: COMPLETE\nP2-003: PASS');
117
- expect(progress.length).toBe(2);
118
- expect(progress[0].planItem).toBe('P1-001');
119
- expect(progress[0].status).toBe('complete');
120
- });
121
-
122
- it('returns empty for no matches', () => {
123
- const progress = detectPlanProgress('No plan items here');
124
- expect(progress.length).toBe(0);
125
- });
126
- });
127
-
128
- describe('extractObservationsFromEntries', () => {
129
- it('extracts observations from a mix of entries', () => {
130
- const entries: TranscriptEntry[] = [
131
- {
132
- type: 'assistant',
133
- message: {
134
- role: 'assistant',
135
- content: [
136
- { type: 'tool_use', id: 'tool-1', name: 'Write', input: { file_path: '/path/new.ts', content: 'code' } },
137
- ],
138
- },
139
- },
140
- {
141
- type: 'user',
142
- message: {
143
- role: 'user',
144
- content: [
145
- { type: 'tool_result', tool_use_id: 'tool-1', content: 'File created successfully' },
146
- ],
147
- },
148
- },
149
- {
150
- type: 'assistant',
151
- message: {
152
- role: 'assistant',
153
- content: [
154
- { type: 'text', text: 'I decided to use esbuild for better performance.' },
155
- ],
156
- },
157
- },
158
- ];
159
-
160
- const observations = extractObservationsFromEntries(entries);
161
- expect(observations.length).toBeGreaterThanOrEqual(2); // file_change + decision
162
- const types = observations.map(o => o.type);
163
- expect(types).toContain('file_change');
164
- expect(types).toContain('decision');
165
- });
166
- });
167
- });
@@ -1,60 +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 } from 'vitest';
5
- import { deriveRoute, derivePortal } from '../page-deps.ts';
6
-
7
- describe('deriveRoute', () => {
8
- it('derives root route', () => {
9
- expect(deriveRoute('src/app/page.tsx')).toBe('/');
10
- });
11
-
12
- it('derives simple route', () => {
13
- expect(deriveRoute('src/app/orders/page.tsx')).toBe('/orders');
14
- });
15
-
16
- it('derives nested route', () => {
17
- expect(deriveRoute('src/app/orders/[id]/page.tsx')).toBe('/orders/[id]');
18
- });
19
-
20
- it('derives deeply nested route', () => {
21
- expect(deriveRoute('src/app/admin/settings/security/page.tsx')).toBe('/admin/settings/security');
22
- });
23
-
24
- it('handles dynamic segments', () => {
25
- expect(deriveRoute('src/app/products/[id]/edit/page.tsx')).toBe('/products/[id]/edit');
26
- });
27
- });
28
-
29
- describe('derivePortal', () => {
30
- // Without accessScopes in config, derivePortal returns the first path segment
31
-
32
- it('identifies admin portal', () => {
33
- expect(derivePortal('/admin/settings')).toBe('admin');
34
- });
35
-
36
- it('uses first path segment as scope', () => {
37
- expect(derivePortal('/portal/orders')).toBe('portal');
38
- });
39
-
40
- it('identifies designer portal', () => {
41
- expect(derivePortal('/designer/projects')).toBe('designer');
42
- });
43
-
44
- it('identifies factory portal', () => {
45
- expect(derivePortal('/factory/orders')).toBe('factory');
46
- });
47
-
48
- it('identifies QC portal', () => {
49
- expect(derivePortal('/qc/inspections')).toBe('qc');
50
- });
51
-
52
- it('uses first segment for production routes', () => {
53
- expect(derivePortal('/production/orders')).toBe('production');
54
- });
55
-
56
- it('uses first segment for top-level routes', () => {
57
- expect(derivePortal('/orders')).toBe('orders');
58
- expect(derivePortal('/products')).toBe('products');
59
- });
60
- });