@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,595 +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 {
7
- isObservabilityTool,
8
- getObservabilityToolDefinitions,
9
- handleObservabilityToolCall,
10
- } from '../observability-tools.ts';
11
- import {
12
- createSession,
13
- addConversationTurn,
14
- addToolCallDetail,
15
- } from '../memory-db.ts';
16
-
17
- // Mock config to use 'massu' as tool prefix
18
- vi.mock('../config.ts', () => ({
19
- getConfig: () => ({ toolPrefix: 'massu' }),
20
- }));
21
-
22
- /**
23
- * Create an in-memory test database with the schema needed by observability tools.
24
- * Mirrors the relevant tables from initMemorySchema() in memory-db.ts.
25
- */
26
- function createTestDb(): Database.Database {
27
- const db = new Database(':memory:');
28
- db.pragma('journal_mode = WAL');
29
- db.pragma('foreign_keys = ON');
30
-
31
- // Sessions table
32
- db.exec(`
33
- CREATE TABLE IF NOT EXISTS sessions (
34
- id INTEGER PRIMARY KEY AUTOINCREMENT,
35
- session_id TEXT UNIQUE NOT NULL,
36
- project TEXT NOT NULL DEFAULT 'my-project',
37
- git_branch TEXT,
38
- started_at TEXT NOT NULL,
39
- started_at_epoch INTEGER NOT NULL,
40
- ended_at TEXT,
41
- ended_at_epoch INTEGER,
42
- status TEXT CHECK(status IN ('active', 'completed', 'abandoned')) NOT NULL DEFAULT 'active',
43
- plan_file TEXT,
44
- plan_phase TEXT,
45
- task_id TEXT
46
- );
47
- `);
48
-
49
- // Observations table (needed for getObservabilityDbSize)
50
- db.exec(`
51
- CREATE TABLE IF NOT EXISTS observations (
52
- id INTEGER PRIMARY KEY AUTOINCREMENT,
53
- session_id TEXT NOT NULL,
54
- type TEXT NOT NULL CHECK(type IN (
55
- 'decision', 'bugfix', 'feature', 'refactor', 'discovery',
56
- 'cr_violation', 'vr_check', 'pattern_compliance', 'failed_attempt',
57
- 'file_change', 'incident_near_miss'
58
- )),
59
- title TEXT NOT NULL,
60
- detail TEXT,
61
- files_involved TEXT DEFAULT '[]',
62
- plan_item TEXT,
63
- cr_rule TEXT,
64
- vr_type TEXT,
65
- evidence TEXT,
66
- importance INTEGER NOT NULL DEFAULT 3 CHECK(importance BETWEEN 1 AND 5),
67
- recurrence_count INTEGER NOT NULL DEFAULT 1,
68
- original_tokens INTEGER DEFAULT 0,
69
- created_at TEXT NOT NULL,
70
- created_at_epoch INTEGER NOT NULL,
71
- FOREIGN KEY(session_id) REFERENCES sessions(session_id) ON DELETE CASCADE
72
- );
73
- `);
74
-
75
- // Memory meta table (needed for incremental parsing state)
76
- db.exec(`
77
- CREATE TABLE IF NOT EXISTS memory_meta (
78
- key TEXT PRIMARY KEY,
79
- value TEXT NOT NULL
80
- );
81
- `);
82
-
83
- // Conversation turns table
84
- db.exec(`
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
-
107
- // Tool call details table
108
- db.exec(`
109
- CREATE TABLE IF NOT EXISTS tool_call_details (
110
- id INTEGER PRIMARY KEY AUTOINCREMENT,
111
- session_id TEXT NOT NULL,
112
- turn_number INTEGER NOT NULL,
113
- tool_name TEXT NOT NULL,
114
- tool_input_summary TEXT,
115
- tool_input_size INTEGER,
116
- tool_output_size INTEGER,
117
- tool_success INTEGER DEFAULT 1,
118
- duration_ms INTEGER,
119
- files_involved TEXT,
120
- created_at TEXT DEFAULT (datetime('now')),
121
- created_at_epoch INTEGER DEFAULT (unixepoch()),
122
- FOREIGN KEY (session_id) REFERENCES sessions(session_id)
123
- );
124
-
125
- CREATE INDEX IF NOT EXISTS idx_tcd_session ON tool_call_details(session_id);
126
- CREATE INDEX IF NOT EXISTS idx_tcd_tool ON tool_call_details(tool_name);
127
- CREATE INDEX IF NOT EXISTS idx_tcd_created ON tool_call_details(created_at DESC);
128
- `);
129
-
130
- // FTS5 for conversation turns
131
- try {
132
- db.exec(`
133
- CREATE VIRTUAL TABLE IF NOT EXISTS conversation_turns_fts USING fts5(
134
- user_prompt,
135
- assistant_response,
136
- content=conversation_turns,
137
- content_rowid=id
138
- );
139
- `);
140
- } catch (_e) {
141
- // ignore
142
- }
143
-
144
- db.exec(`
145
- CREATE TRIGGER IF NOT EXISTS ct_fts_insert AFTER INSERT ON conversation_turns BEGIN
146
- INSERT INTO conversation_turns_fts(rowid, user_prompt, assistant_response)
147
- VALUES (new.id, new.user_prompt, new.assistant_response);
148
- END;
149
-
150
- CREATE TRIGGER IF NOT EXISTS ct_fts_delete AFTER DELETE ON conversation_turns BEGIN
151
- INSERT INTO conversation_turns_fts(conversation_turns_fts, rowid, user_prompt, assistant_response)
152
- VALUES ('delete', old.id, old.user_prompt, old.assistant_response);
153
- END;
154
-
155
- CREATE TRIGGER IF NOT EXISTS ct_fts_update AFTER UPDATE ON conversation_turns BEGIN
156
- INSERT INTO conversation_turns_fts(conversation_turns_fts, rowid, user_prompt, assistant_response)
157
- VALUES ('delete', old.id, old.user_prompt, old.assistant_response);
158
- INSERT INTO conversation_turns_fts(rowid, user_prompt, assistant_response)
159
- VALUES (new.id, new.user_prompt, new.assistant_response);
160
- END;
161
- `);
162
-
163
- return db;
164
- }
165
-
166
- /** Helper: extract text from a ToolResult */
167
- function getText(result: { content: { type: 'text'; text: string }[] }): string {
168
- return result.content[0].text;
169
- }
170
-
171
- describe('observability-tools (3-function pattern)', () => {
172
- let db: Database.Database;
173
-
174
- beforeEach(() => {
175
- db = createTestDb();
176
- createSession(db, 'session-001');
177
- });
178
-
179
- afterEach(() => {
180
- db.close();
181
- });
182
-
183
- // ============================================================
184
- // 1. isObservabilityTool()
185
- // ============================================================
186
- describe('isObservabilityTool', () => {
187
- it('returns true for all 4 prefixed tool names', () => {
188
- expect(isObservabilityTool('massu_session_replay')).toBe(true);
189
- expect(isObservabilityTool('massu_prompt_analysis')).toBe(true);
190
- expect(isObservabilityTool('massu_tool_patterns')).toBe(true);
191
- expect(isObservabilityTool('massu_session_stats')).toBe(true);
192
- });
193
-
194
- it('returns true for base names without prefix', () => {
195
- expect(isObservabilityTool('session_replay')).toBe(true);
196
- expect(isObservabilityTool('prompt_analysis')).toBe(true);
197
- expect(isObservabilityTool('tool_patterns')).toBe(true);
198
- expect(isObservabilityTool('session_stats')).toBe(true);
199
- });
200
-
201
- it('returns false for unknown tool names', () => {
202
- expect(isObservabilityTool('massu_quality_score')).toBe(false);
203
- expect(isObservabilityTool('massu_cost_session')).toBe(false);
204
- expect(isObservabilityTool('random_tool')).toBe(false);
205
- expect(isObservabilityTool('')).toBe(false);
206
- expect(isObservabilityTool('massu_session_replay_extra')).toBe(false);
207
- });
208
- });
209
-
210
- // ============================================================
211
- // 2. getObservabilityToolDefinitions()
212
- // ============================================================
213
- describe('getObservabilityToolDefinitions', () => {
214
- it('returns exactly 4 tool definitions', () => {
215
- const defs = getObservabilityToolDefinitions();
216
- expect(defs.length).toBe(4);
217
- });
218
-
219
- it('all definitions have required fields (name, description, inputSchema)', () => {
220
- const defs = getObservabilityToolDefinitions();
221
- for (const def of defs) {
222
- expect(def.name).toBeTruthy();
223
- expect(def.description).toBeTruthy();
224
- expect(def.inputSchema).toBeTruthy();
225
- expect(def.inputSchema.type).toBe('object');
226
- expect(def.inputSchema.properties).toBeDefined();
227
- }
228
- });
229
-
230
- it('tool names use the configured prefix', () => {
231
- const defs = getObservabilityToolDefinitions();
232
- const names = defs.map(d => d.name);
233
- expect(names).toContain('massu_session_replay');
234
- expect(names).toContain('massu_prompt_analysis');
235
- expect(names).toContain('massu_tool_patterns');
236
- expect(names).toContain('massu_session_stats');
237
- });
238
-
239
- it('session_replay requires session_id', () => {
240
- const defs = getObservabilityToolDefinitions();
241
- const replay = defs.find(d => d.name === 'massu_session_replay');
242
- expect(replay).toBeDefined();
243
- expect(replay!.inputSchema.required).toEqual(['session_id']);
244
- });
245
-
246
- it('other tools have empty required arrays', () => {
247
- const defs = getObservabilityToolDefinitions();
248
- const nonReplay = defs.filter(d => d.name !== 'massu_session_replay');
249
- for (const def of nonReplay) {
250
- expect(def.inputSchema.required).toEqual([]);
251
- }
252
- });
253
- });
254
-
255
- // ============================================================
256
- // 3. handleObservabilityToolCall - session_replay
257
- // ============================================================
258
- describe('handleObservabilityToolCall - session_replay', () => {
259
- it('returns replay for session with turns', () => {
260
- addConversationTurn(db, 'session-001', 1, 'How do I fix the bug?', 'Check the middleware...', null, 2, 50, 200);
261
- addConversationTurn(db, 'session-001', 2, 'What about tests?', 'Run vitest to verify...', null, 1, 30, 100);
262
-
263
- const result = handleObservabilityToolCall('massu_session_replay', { session_id: 'session-001' }, db);
264
- const text = getText(result);
265
-
266
- expect(text).toContain('Session Replay');
267
- expect(text).toContain('Turn 1');
268
- expect(text).toContain('Turn 2');
269
- expect(text).toContain('How do I fix the bug?');
270
- expect(text).toContain('Check the middleware...');
271
- expect(text).toContain('Turns: 2');
272
- });
273
-
274
- it('returns error when session_id is missing', () => {
275
- const result = handleObservabilityToolCall('massu_session_replay', {}, db);
276
- const text = getText(result);
277
-
278
- expect(text).toContain('Error: session_id is required');
279
- });
280
-
281
- it('returns message when no turns found for session', () => {
282
- const result = handleObservabilityToolCall('massu_session_replay', { session_id: 'non-existent-session' }, db);
283
- const text = getText(result);
284
-
285
- expect(text).toContain('No conversation turns found');
286
- expect(text).toContain('session-end hook');
287
- });
288
-
289
- it('includes tool call details when include_tool_calls is true', () => {
290
- const toolCalls = JSON.stringify([
291
- { name: 'Read', input_summary: 'Read src/index.ts', is_error: false },
292
- { name: 'Bash', input_summary: '$ npm test', is_error: true },
293
- ]);
294
- addConversationTurn(db, 'session-001', 1, 'Fix the tests', 'Let me check...', toolCalls, 2, 50, 200);
295
-
296
- const result = handleObservabilityToolCall('massu_session_replay', {
297
- session_id: 'session-001',
298
- include_tool_calls: true,
299
- }, db);
300
- const text = getText(result);
301
-
302
- expect(text).toContain('Tool Calls');
303
- expect(text).toContain('Read: Read src/index.ts');
304
- expect(text).toContain('Bash: $ npm test');
305
- expect(text).toContain('[ERROR]');
306
- });
307
-
308
- it('does not include tool call details when include_tool_calls is false', () => {
309
- const toolCalls = JSON.stringify([
310
- { name: 'Read', input_summary: 'Read src/index.ts', is_error: false },
311
- ]);
312
- addConversationTurn(db, 'session-001', 1, 'Fix the tests', 'Let me check...', toolCalls, 1, 50, 200);
313
-
314
- const result = handleObservabilityToolCall('massu_session_replay', {
315
- session_id: 'session-001',
316
- include_tool_calls: false,
317
- }, db);
318
- const text = getText(result);
319
-
320
- expect(text).not.toContain('Tool Calls');
321
- expect(text).not.toContain('Read: Read src/index.ts');
322
- });
323
-
324
- it('respects turn_from and turn_to range filters', () => {
325
- addConversationTurn(db, 'session-001', 1, 'Turn one prompt', 'Response 1', null, 0, 10, 20);
326
- addConversationTurn(db, 'session-001', 2, 'Turn two prompt', 'Response 2', null, 0, 10, 20);
327
- addConversationTurn(db, 'session-001', 3, 'Turn three prompt', 'Response 3', null, 0, 10, 20);
328
-
329
- const result = handleObservabilityToolCall('massu_session_replay', {
330
- session_id: 'session-001',
331
- turn_from: 2,
332
- turn_to: 3,
333
- }, db);
334
- const text = getText(result);
335
-
336
- expect(text).toContain('Turn 2');
337
- expect(text).toContain('Turn 3');
338
- expect(text).not.toContain('Turn one prompt');
339
- expect(text).toContain('Turns: 2');
340
- });
341
- });
342
-
343
- // ============================================================
344
- // 4. handleObservabilityToolCall - prompt_analysis
345
- // ============================================================
346
- describe('handleObservabilityToolCall - prompt_analysis', () => {
347
- it('shows recent prompts when no query is provided', () => {
348
- addConversationTurn(db, 'session-001', 1, 'Fix the authentication bug', 'Check JWT tokens...', null, 3, 50, 200);
349
- addConversationTurn(db, 'session-001', 2, 'Run the database migration', 'Use prisma migrate...', null, 1, 30, 100);
350
-
351
- const result = handleObservabilityToolCall('massu_prompt_analysis', {}, db);
352
- const text = getText(result);
353
-
354
- expect(text).toContain('Recent Prompts');
355
- expect(text).toContain('Fix the authentication bug');
356
- expect(text).toContain('Run the database migration');
357
- });
358
-
359
- it('searches prompts using FTS5 query', () => {
360
- addConversationTurn(db, 'session-001', 1, 'Fix the authentication middleware', 'Check JWT tokens...', null, 0, 50, 200);
361
- addConversationTurn(db, 'session-001', 2, 'Run the database migration', 'Use prisma migrate...', null, 0, 30, 100);
362
-
363
- const result = handleObservabilityToolCall('massu_prompt_analysis', { query: 'authentication' }, db);
364
- const text = getText(result);
365
-
366
- expect(text).toContain('Prompt Search');
367
- expect(text).toContain('authentication');
368
- expect(text).toContain('1 results');
369
- });
370
-
371
- it('returns no-results message for FTS query with no matches', () => {
372
- addConversationTurn(db, 'session-001', 1, 'Fix the bug', 'Response here', null, 0, 10, 20);
373
-
374
- const result = handleObservabilityToolCall('massu_prompt_analysis', { query: 'nonexistent_term_xyz' }, db);
375
- const text = getText(result);
376
-
377
- expect(text).toContain('No prompts found matching');
378
- });
379
-
380
- it('returns no-data message when database is empty', () => {
381
- const result = handleObservabilityToolCall('massu_prompt_analysis', {}, db);
382
- const text = getText(result);
383
-
384
- expect(text).toContain('No conversation turns recorded yet');
385
- });
386
-
387
- it('respects the limit parameter', () => {
388
- for (let i = 1; i <= 5; i++) {
389
- addConversationTurn(db, 'session-001', i, `Prompt number ${i}`, `Response ${i}`, null, 0, 10, 20);
390
- }
391
-
392
- const result = handleObservabilityToolCall('massu_prompt_analysis', { limit: 2 }, db);
393
- const text = getText(result);
394
-
395
- // Should contain "Recent Prompts" header plus table header + 2 data rows
396
- const dataRows = text.split('\n').filter(line => line.startsWith('|') && !line.startsWith('|--') && !line.includes('Session'));
397
- expect(dataRows.length).toBeLessThanOrEqual(2);
398
- });
399
- });
400
-
401
- // ============================================================
402
- // 5. handleObservabilityToolCall - tool_patterns
403
- // ============================================================
404
- describe('handleObservabilityToolCall - tool_patterns', () => {
405
- it('groups by tool by default', () => {
406
- addToolCallDetail(db, 'session-001', 1, 'Read', 'Read file1', 50, 1000, true);
407
- addToolCallDetail(db, 'session-001', 1, 'Read', 'Read file2', 60, 2000, true);
408
- addToolCallDetail(db, 'session-001', 2, 'Edit', 'Edit file1', 200, 50, true);
409
- addToolCallDetail(db, 'session-001', 2, 'Bash', '$ npm test', 30, 500, false);
410
-
411
- const result = handleObservabilityToolCall('massu_tool_patterns', {}, db);
412
- const text = getText(result);
413
-
414
- expect(text).toContain('Tool Usage Patterns (grouped by tool)');
415
- expect(text).toContain('Read');
416
- expect(text).toContain('Edit');
417
- expect(text).toContain('Bash');
418
- expect(text).toContain('Success Rate');
419
- });
420
-
421
- it('groups by session when group_by is session', () => {
422
- createSession(db, 'session-002');
423
- addToolCallDetail(db, 'session-001', 1, 'Read', 'Read file', 50, 1000, true);
424
- addToolCallDetail(db, 'session-002', 1, 'Edit', 'Edit file', 200, 50, true);
425
-
426
- const result = handleObservabilityToolCall('massu_tool_patterns', { group_by: 'session' }, db);
427
- const text = getText(result);
428
-
429
- expect(text).toContain('Tool Usage Patterns (grouped by session)');
430
- expect(text).toContain('Unique Tools');
431
- });
432
-
433
- it('groups by day when group_by is day', () => {
434
- addToolCallDetail(db, 'session-001', 1, 'Read', 'Read file', 50, 1000, true);
435
- addToolCallDetail(db, 'session-001', 2, 'Edit', 'Edit file', 200, 50, true);
436
-
437
- const result = handleObservabilityToolCall('massu_tool_patterns', { group_by: 'day' }, db);
438
- const text = getText(result);
439
-
440
- expect(text).toContain('Tool Usage Patterns (grouped by day)');
441
- expect(text).toContain('Day');
442
- });
443
-
444
- it('returns no-data message when no tool usage exists', () => {
445
- const result = handleObservabilityToolCall('massu_tool_patterns', {}, db);
446
- const text = getText(result);
447
-
448
- expect(text).toContain('No tool usage data recorded yet');
449
- });
450
-
451
- it('shows success rate calculation for tool group', () => {
452
- // 2 successes and 1 failure for Read
453
- addToolCallDetail(db, 'session-001', 1, 'Read', 'Read file1', 50, 1000, true);
454
- addToolCallDetail(db, 'session-001', 1, 'Read', 'Read file2', 60, 2000, true);
455
- addToolCallDetail(db, 'session-001', 2, 'Read', 'Read file3', 40, 0, false);
456
-
457
- const result = handleObservabilityToolCall('massu_tool_patterns', {}, db);
458
- const text = getText(result);
459
-
460
- // 2 out of 3 = 67%
461
- expect(text).toContain('67%');
462
- });
463
- });
464
-
465
- // ============================================================
466
- // 6. handleObservabilityToolCall - session_stats
467
- // ============================================================
468
- describe('handleObservabilityToolCall - session_stats', () => {
469
- it('returns detailed stats for a single session', () => {
470
- addConversationTurn(db, 'session-001', 1, 'Question 1', 'Answer 1', null, 2, 50, 200);
471
- addConversationTurn(db, 'session-001', 2, 'Question 2', 'Answer 2', null, 3, 40, 150);
472
- addToolCallDetail(db, 'session-001', 1, 'Read', 'file', 50, 1000, true);
473
-
474
- const result = handleObservabilityToolCall('massu_session_stats', { session_id: 'session-001' }, db);
475
- const text = getText(result);
476
-
477
- expect(text).toContain('Session Statistics');
478
- expect(text).toContain('Session: session-001');
479
- expect(text).toContain('Turns');
480
- expect(text).toContain('Total Tool Calls');
481
- expect(text).toContain('Database Size');
482
- });
483
-
484
- it('returns multi-session summary table when no session_id', () => {
485
- createSession(db, 'session-002');
486
- addConversationTurn(db, 'session-001', 1, 'Q1', 'A1', null, 1, 10, 20);
487
- addConversationTurn(db, 'session-002', 1, 'Q1', 'A1', null, 2, 10, 20);
488
-
489
- const result = handleObservabilityToolCall('massu_session_stats', {}, db);
490
- const text = getText(result);
491
-
492
- expect(text).toContain('Session Statistics');
493
- // Multi-session table headers
494
- expect(text).toContain('Status');
495
- expect(text).toContain('Turns');
496
- expect(text).toContain('Tool Calls');
497
- expect(text).toContain('Prompt Tokens');
498
- expect(text).toContain('Response Tokens');
499
- // Database size section
500
- expect(text).toContain('Database Size');
501
- expect(text).toContain('Conversation turns');
502
- expect(text).toContain('Tool call details');
503
- });
504
-
505
- it('returns no-data message when no sessions exist for multi-session view', () => {
506
- // Remove the session created in beforeEach to get an empty state
507
- db.prepare('DELETE FROM sessions').run();
508
-
509
- const result = handleObservabilityToolCall('massu_session_stats', {}, db);
510
- const text = getText(result);
511
-
512
- expect(text).toContain('No session stats available');
513
- });
514
-
515
- it('returns zero counts for non-existent session_id', () => {
516
- const result = handleObservabilityToolCall('massu_session_stats', { session_id: 'non-existent' }, db);
517
- const text = getText(result);
518
-
519
- // getSessionStats always returns a record for single session query, even if session does not exist
520
- expect(text).toContain('Session Statistics');
521
- expect(text).toContain('Status');
522
- expect(text).toContain('Turns');
523
- });
524
-
525
- it('includes tool breakdown for single session', () => {
526
- addConversationTurn(db, 'session-001', 1, 'Q1', 'A1', null, 3, 50, 200);
527
- addToolCallDetail(db, 'session-001', 1, 'Read', 'file1', 50, 1000, true);
528
- addToolCallDetail(db, 'session-001', 1, 'Edit', 'file2', 200, 50, true);
529
- addToolCallDetail(db, 'session-001', 1, 'Read', 'file3', 60, 800, true);
530
-
531
- const result = handleObservabilityToolCall('massu_session_stats', { session_id: 'session-001' }, db);
532
- const text = getText(result);
533
-
534
- expect(text).toContain('Tool Breakdown');
535
- expect(text).toContain('Read');
536
- expect(text).toContain('Edit');
537
- });
538
-
539
- it('includes database size monitoring info', () => {
540
- addConversationTurn(db, 'session-001', 1, 'Q', 'A', null, 0, 10, 20);
541
- addToolCallDetail(db, 'session-001', 1, 'Read', 'file', 50, 1000, true);
542
-
543
- const result = handleObservabilityToolCall('massu_session_stats', {}, db);
544
- const text = getText(result);
545
-
546
- expect(text).toContain('Database Size');
547
- expect(text).toContain('Conversation turns: 1');
548
- expect(text).toContain('Tool call details: 1');
549
- expect(text).toContain('Observations: 0');
550
- expect(text).toContain('MB');
551
- });
552
- });
553
-
554
- // ============================================================
555
- // 7. handleObservabilityToolCall - unknown tool & error handling
556
- // ============================================================
557
- describe('handleObservabilityToolCall - edge cases', () => {
558
- it('returns error for unknown tool name', () => {
559
- const result = handleObservabilityToolCall('massu_unknown_obs_tool', {}, db);
560
- const text = getText(result);
561
-
562
- expect(text).toContain('Unknown observability tool');
563
- expect(text).toContain('massu_unknown_obs_tool');
564
- });
565
-
566
- it('catches and reports thrown errors', () => {
567
- // Close the database to force an error when a handler tries to query
568
- const closedDb = new Database(':memory:');
569
- closedDb.close();
570
-
571
- const result = handleObservabilityToolCall('massu_session_replay', { session_id: 'test' }, closedDb);
572
- const text = getText(result);
573
-
574
- expect(text).toContain('Error in massu_session_replay');
575
- });
576
-
577
- it('handles session_replay with empty session_id string', () => {
578
- const result = handleObservabilityToolCall('massu_session_replay', { session_id: '' }, db);
579
- const text = getText(result);
580
-
581
- expect(text).toContain('Error: session_id is required');
582
- });
583
-
584
- it('works with base name (no prefix) for handler routing', () => {
585
- addConversationTurn(db, 'session-001', 1, 'Hello', 'Hi there', null, 0, 10, 20);
586
-
587
- // Pass just the base name without prefix -- the handler strips prefix if present
588
- const result = handleObservabilityToolCall('session_replay', { session_id: 'session-001' }, db);
589
- const text = getText(result);
590
-
591
- expect(text).toContain('Session Replay');
592
- expect(text).toContain('Hello');
593
- });
594
- });
595
- });