@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,900 +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 { generateCurrentMd } from '../session-state-generator.ts';
7
-
8
- // Helper to create an in-memory database with the full memory-db schema
9
- // required by generateCurrentMd (sessions, observations, session_summaries, user_prompts)
10
- function createTestDb(): Database.Database {
11
- const db = new Database(':memory:');
12
- db.pragma('journal_mode = WAL');
13
- db.pragma('foreign_keys = ON');
14
-
15
- db.exec(`
16
- CREATE TABLE sessions (
17
- id INTEGER PRIMARY KEY AUTOINCREMENT,
18
- session_id TEXT UNIQUE NOT NULL,
19
- project TEXT NOT NULL DEFAULT 'my-project',
20
- git_branch TEXT,
21
- started_at TEXT NOT NULL,
22
- started_at_epoch INTEGER NOT NULL,
23
- ended_at TEXT,
24
- ended_at_epoch INTEGER,
25
- status TEXT CHECK(status IN ('active', 'completed', 'abandoned')) NOT NULL DEFAULT 'active',
26
- plan_file TEXT,
27
- plan_phase TEXT,
28
- task_id TEXT
29
- );
30
-
31
- CREATE TABLE observations (
32
- id INTEGER PRIMARY KEY AUTOINCREMENT,
33
- session_id TEXT NOT NULL,
34
- type TEXT NOT NULL CHECK(type IN (
35
- 'decision', 'bugfix', 'feature', 'refactor', 'discovery',
36
- 'cr_violation', 'vr_check', 'pattern_compliance', 'failed_attempt',
37
- 'file_change', 'incident_near_miss'
38
- )),
39
- title TEXT NOT NULL,
40
- detail TEXT,
41
- files_involved TEXT DEFAULT '[]',
42
- plan_item TEXT,
43
- cr_rule TEXT,
44
- vr_type TEXT,
45
- evidence TEXT,
46
- importance INTEGER NOT NULL DEFAULT 3 CHECK(importance BETWEEN 1 AND 5),
47
- recurrence_count INTEGER NOT NULL DEFAULT 1,
48
- original_tokens INTEGER DEFAULT 0,
49
- created_at TEXT NOT NULL,
50
- created_at_epoch INTEGER NOT NULL,
51
- FOREIGN KEY(session_id) REFERENCES sessions(session_id) ON DELETE CASCADE
52
- );
53
-
54
- CREATE TABLE session_summaries (
55
- id INTEGER PRIMARY KEY AUTOINCREMENT,
56
- session_id TEXT NOT NULL,
57
- request TEXT,
58
- investigated TEXT,
59
- decisions TEXT,
60
- completed TEXT,
61
- failed_attempts TEXT,
62
- next_steps TEXT,
63
- files_created TEXT DEFAULT '[]',
64
- files_modified TEXT DEFAULT '[]',
65
- verification_results TEXT DEFAULT '{}',
66
- plan_progress TEXT DEFAULT '{}',
67
- created_at TEXT NOT NULL,
68
- created_at_epoch INTEGER NOT NULL,
69
- FOREIGN KEY(session_id) REFERENCES sessions(session_id) ON DELETE CASCADE
70
- );
71
-
72
- CREATE TABLE user_prompts (
73
- id INTEGER PRIMARY KEY AUTOINCREMENT,
74
- session_id TEXT NOT NULL,
75
- prompt_text TEXT NOT NULL,
76
- prompt_number INTEGER NOT NULL DEFAULT 1,
77
- created_at TEXT NOT NULL,
78
- created_at_epoch INTEGER NOT NULL,
79
- FOREIGN KEY(session_id) REFERENCES sessions(session_id) ON DELETE CASCADE
80
- );
81
- `);
82
-
83
- return db;
84
- }
85
-
86
- // Helper to insert a session row directly
87
- function insertSession(
88
- db: Database.Database,
89
- sessionId: string,
90
- opts: {
91
- status?: string;
92
- branch?: string;
93
- planFile?: string;
94
- } = {}
95
- ): void {
96
- const now = new Date().toISOString();
97
- const epoch = Math.floor(Date.now() / 1000);
98
- db.prepare(`
99
- INSERT INTO sessions (session_id, git_branch, plan_file, status, started_at, started_at_epoch)
100
- VALUES (?, ?, ?, ?, ?, ?)
101
- `).run(
102
- sessionId,
103
- opts.branch ?? null,
104
- opts.planFile ?? null,
105
- opts.status ?? 'active',
106
- now,
107
- epoch
108
- );
109
- }
110
-
111
- // Helper to insert an observation row directly
112
- function insertObservation(
113
- db: Database.Database,
114
- sessionId: string,
115
- type: string,
116
- title: string,
117
- detail: string | null = null,
118
- filesInvolved: string[] = [],
119
- epochOffset: number = 0
120
- ): void {
121
- const epoch = Math.floor(Date.now() / 1000) + epochOffset;
122
- db.prepare(`
123
- INSERT INTO observations (session_id, type, title, detail, files_involved, importance, created_at, created_at_epoch)
124
- VALUES (?, ?, ?, ?, ?, 3, ?, ?)
125
- `).run(sessionId, type, title, detail, JSON.stringify(filesInvolved), new Date(epoch * 1000).toISOString(), epoch);
126
- }
127
-
128
- // Helper to insert a session summary
129
- function insertSummary(
130
- db: Database.Database,
131
- sessionId: string,
132
- opts: {
133
- completed?: string;
134
- nextSteps?: string;
135
- planProgress?: Record<string, string>;
136
- } = {}
137
- ): void {
138
- const now = new Date().toISOString();
139
- const epoch = Math.floor(Date.now() / 1000);
140
- db.prepare(`
141
- INSERT INTO session_summaries (session_id, completed, next_steps, plan_progress, created_at, created_at_epoch)
142
- VALUES (?, ?, ?, ?, ?, ?)
143
- `).run(
144
- sessionId,
145
- opts.completed ?? null,
146
- opts.nextSteps ?? null,
147
- JSON.stringify(opts.planProgress ?? {}),
148
- now,
149
- epoch
150
- );
151
- }
152
-
153
- // Helper to insert a user prompt
154
- function insertPrompt(db: Database.Database, sessionId: string, text: string, promptNumber: number = 1): void {
155
- const now = new Date().toISOString();
156
- const epoch = Math.floor(Date.now() / 1000);
157
- db.prepare(`
158
- INSERT INTO user_prompts (session_id, prompt_text, prompt_number, created_at, created_at_epoch)
159
- VALUES (?, ?, ?, ?, ?)
160
- `).run(sessionId, text, promptNumber, now, epoch);
161
- }
162
-
163
- describe('session-state-generator', () => {
164
- let db: Database.Database;
165
-
166
- beforeEach(() => {
167
- db = createTestDb();
168
- });
169
-
170
- afterEach(() => {
171
- db.close();
172
- });
173
-
174
- // ============================================================
175
- // No session
176
- // ============================================================
177
-
178
- describe('missing session', () => {
179
- it('returns fallback content when session does not exist', () => {
180
- const result = generateCurrentMd(db, 'nonexistent-session');
181
- expect(result).toBe('# Session State\n\nNo active session found.\n');
182
- });
183
- });
184
-
185
- // ============================================================
186
- // Empty session (no observations, no summary, no prompt)
187
- // ============================================================
188
-
189
- describe('empty session', () => {
190
- const SESSION_ID = 'empty-session';
191
-
192
- beforeEach(() => {
193
- insertSession(db, SESSION_ID, { branch: 'main', status: 'active' });
194
- });
195
-
196
- it('returns markdown content', () => {
197
- const result = generateCurrentMd(db, SESSION_ID);
198
- expect(result).toContain('# Session State');
199
- });
200
-
201
- it('includes the session id', () => {
202
- const result = generateCurrentMd(db, SESSION_ID);
203
- expect(result).toContain(SESSION_ID);
204
- });
205
-
206
- it('includes git branch', () => {
207
- const result = generateCurrentMd(db, SESSION_ID);
208
- expect(result).toContain('main');
209
- });
210
-
211
- it('shows IN PROGRESS for active status', () => {
212
- const result = generateCurrentMd(db, SESSION_ID);
213
- expect(result).toContain('IN PROGRESS');
214
- });
215
-
216
- it('uses Unknown task when no prompt exists', () => {
217
- const result = generateCurrentMd(db, SESSION_ID);
218
- expect(result).toContain('Unknown task');
219
- });
220
-
221
- it('does not include COMPLETED WORK section without observations', () => {
222
- const result = generateCurrentMd(db, SESSION_ID);
223
- expect(result).not.toContain('## COMPLETED WORK');
224
- });
225
-
226
- it('does not include FAILED ATTEMPTS section without observations', () => {
227
- const result = generateCurrentMd(db, SESSION_ID);
228
- expect(result).not.toContain('## FAILED ATTEMPTS');
229
- });
230
-
231
- it('does not include VERIFICATION EVIDENCE section without observations', () => {
232
- const result = generateCurrentMd(db, SESSION_ID);
233
- expect(result).not.toContain('## VERIFICATION EVIDENCE');
234
- });
235
-
236
- it('does not include PLAN DOCUMENT section without plan_file', () => {
237
- const result = generateCurrentMd(db, SESSION_ID);
238
- expect(result).not.toContain('## PLAN DOCUMENT');
239
- });
240
-
241
- it('includes the separator line', () => {
242
- const result = generateCurrentMd(db, SESSION_ID);
243
- expect(result).toContain('---');
244
- });
245
-
246
- it('includes Last Updated field', () => {
247
- const result = generateCurrentMd(db, SESSION_ID);
248
- expect(result).toContain('**Last Updated**');
249
- });
250
- });
251
-
252
- // ============================================================
253
- // Session status variants
254
- // ============================================================
255
-
256
- describe('session status', () => {
257
- it('shows COMPLETED for completed status', () => {
258
- insertSession(db, 'completed-session', { status: 'completed' });
259
- const result = generateCurrentMd(db, 'completed-session');
260
- expect(result).toContain('COMPLETED');
261
- });
262
-
263
- it('shows ABANDONED for abandoned status', () => {
264
- insertSession(db, 'abandoned-session', { status: 'abandoned' });
265
- const result = generateCurrentMd(db, 'abandoned-session');
266
- expect(result).toContain('ABANDONED');
267
- });
268
- });
269
-
270
- // ============================================================
271
- // User prompt as task summary
272
- // ============================================================
273
-
274
- describe('user prompt / task summary', () => {
275
- const SESSION_ID = 'prompt-session';
276
-
277
- beforeEach(() => {
278
- insertSession(db, SESSION_ID);
279
- });
280
-
281
- it('uses first user prompt as task summary', () => {
282
- insertPrompt(db, SESSION_ID, 'Fix the authentication bug in login page', 1);
283
- const result = generateCurrentMd(db, SESSION_ID);
284
- expect(result).toContain('Fix the authentication bug in login page');
285
- });
286
-
287
- it('truncates prompt text at 100 characters', () => {
288
- const longPrompt = 'A'.repeat(150);
289
- insertPrompt(db, SESSION_ID, longPrompt, 1);
290
- const result = generateCurrentMd(db, SESSION_ID);
291
- // The 100-char slice of the long prompt should appear
292
- expect(result).toContain('A'.repeat(100));
293
- // But not the 101st character portion (since it's sliced)
294
- expect(result).not.toContain('A'.repeat(101));
295
- });
296
-
297
- it('normalises newlines in prompt text', () => {
298
- insertPrompt(db, SESSION_ID, 'Line one\nLine two\nLine three', 1);
299
- const result = generateCurrentMd(db, SESSION_ID);
300
- // Newlines are replaced with spaces
301
- expect(result).toContain('Line one Line two Line three');
302
- });
303
-
304
- it('only uses the first prompt (prompt_number = 1)', () => {
305
- insertPrompt(db, SESSION_ID, 'Second prompt that should not appear', 2);
306
- insertPrompt(db, SESSION_ID, 'First prompt that should appear', 1);
307
- const result = generateCurrentMd(db, SESSION_ID);
308
- expect(result).toContain('First prompt that should appear');
309
- expect(result).not.toContain('Second prompt that should not appear');
310
- });
311
- });
312
-
313
- // ============================================================
314
- // Observation types
315
- // ============================================================
316
-
317
- describe('feature observations', () => {
318
- const SESSION_ID = 'feature-session';
319
-
320
- beforeEach(() => {
321
- insertSession(db, SESSION_ID);
322
- });
323
-
324
- it('renders COMPLETED WORK section for feature observations', () => {
325
- insertObservation(db, SESSION_ID, 'feature', 'Add user profile endpoint');
326
- const result = generateCurrentMd(db, SESSION_ID);
327
- expect(result).toContain('## COMPLETED WORK');
328
- });
329
-
330
- it('does not list feature observations as file entries', () => {
331
- // feature type does not map to Files Created/Modified
332
- insertObservation(db, SESSION_ID, 'feature', 'Add user profile endpoint');
333
- const result = generateCurrentMd(db, SESSION_ID);
334
- // COMPLETED WORK section present, but no file table rows for this observation
335
- expect(result).toContain('## COMPLETED WORK');
336
- });
337
- });
338
-
339
- describe('bugfix observations', () => {
340
- const SESSION_ID = 'bugfix-session';
341
-
342
- beforeEach(() => {
343
- insertSession(db, SESSION_ID);
344
- });
345
-
346
- it('renders COMPLETED WORK section for bugfix observations', () => {
347
- insertObservation(db, SESSION_ID, 'bugfix', 'Fix null pointer in auth flow');
348
- const result = generateCurrentMd(db, SESSION_ID);
349
- expect(result).toContain('## COMPLETED WORK');
350
- });
351
- });
352
-
353
- describe('refactor observations', () => {
354
- const SESSION_ID = 'refactor-session';
355
-
356
- beforeEach(() => {
357
- insertSession(db, SESSION_ID);
358
- });
359
-
360
- it('renders COMPLETED WORK section for refactor observations', () => {
361
- insertObservation(db, SESSION_ID, 'refactor', 'Extract helper functions into utils.ts');
362
- const result = generateCurrentMd(db, SESSION_ID);
363
- expect(result).toContain('## COMPLETED WORK');
364
- });
365
- });
366
-
367
- describe('file_change observations', () => {
368
- const SESSION_ID = 'filechange-session';
369
-
370
- beforeEach(() => {
371
- insertSession(db, SESSION_ID);
372
- });
373
-
374
- it('renders COMPLETED WORK section for file_change observations', () => {
375
- insertObservation(db, SESSION_ID, 'file_change', 'Created: src/utils.ts', null, ['src/utils.ts']);
376
- const result = generateCurrentMd(db, SESSION_ID);
377
- expect(result).toContain('## COMPLETED WORK');
378
- });
379
-
380
- it('shows Files Created table for file_change with title starting with Created', () => {
381
- insertObservation(db, SESSION_ID, 'file_change', 'Created/wrote: src/helpers.ts', null, ['src/helpers.ts']);
382
- const result = generateCurrentMd(db, SESSION_ID);
383
- expect(result).toContain('### Files Created');
384
- expect(result).toContain('src/helpers.ts');
385
- });
386
-
387
- it('shows Files Modified table for file_change with title starting with Edited', () => {
388
- insertObservation(db, SESSION_ID, 'file_change', 'Edited: src/server.ts', null, ['src/server.ts']);
389
- const result = generateCurrentMd(db, SESSION_ID);
390
- expect(result).toContain('### Files Modified');
391
- expect(result).toContain('src/server.ts');
392
- });
393
-
394
- it('deduplicates files in Files Modified table', () => {
395
- insertObservation(db, SESSION_ID, 'file_change', 'Edited: src/server.ts', null, ['src/server.ts'], 0);
396
- insertObservation(db, SESSION_ID, 'file_change', 'Edited: src/server.ts', null, ['src/server.ts'], 1);
397
- const result = generateCurrentMd(db, SESSION_ID);
398
- // Count occurrences of the file path in the output
399
- const matches = result.match(/`src\/server\.ts`/g) ?? [];
400
- expect(matches.length).toBe(1);
401
- });
402
-
403
- it('falls back to title-derived file name when files_involved is empty', () => {
404
- insertObservation(db, SESSION_ID, 'file_change', 'Created/wrote: src/config.ts', null, []);
405
- const result = generateCurrentMd(db, SESSION_ID);
406
- expect(result).toContain('### Files Created');
407
- // Falls back to title.replace('Created/wrote: ', '')
408
- expect(result).toContain('src/config.ts');
409
- });
410
-
411
- it('uses first entry in files_involved array when present', () => {
412
- insertObservation(db, SESSION_ID, 'file_change', 'Created: something', null, ['src/actual-file.ts', 'other.ts']);
413
- const result = generateCurrentMd(db, SESSION_ID);
414
- expect(result).toContain('src/actual-file.ts');
415
- });
416
- });
417
-
418
- describe('decision observations', () => {
419
- const SESSION_ID = 'decision-session';
420
-
421
- beforeEach(() => {
422
- insertSession(db, SESSION_ID);
423
- });
424
-
425
- it('renders Key Decisions section for decision observations', () => {
426
- insertObservation(db, SESSION_ID, 'decision', 'Use FTS5 for search indexing');
427
- const result = generateCurrentMd(db, SESSION_ID);
428
- expect(result).toContain('### Key Decisions');
429
- expect(result).toContain('Use FTS5 for search indexing');
430
- });
431
-
432
- it('renders multiple decisions as list items', () => {
433
- insertObservation(db, SESSION_ID, 'decision', 'Decision A', null, [], 0);
434
- insertObservation(db, SESSION_ID, 'decision', 'Decision B', null, [], 1);
435
- const result = generateCurrentMd(db, SESSION_ID);
436
- expect(result).toContain('- Decision A');
437
- expect(result).toContain('- Decision B');
438
- });
439
-
440
- it('does not render Key Decisions section when no decision observations exist', () => {
441
- insertObservation(db, SESSION_ID, 'feature', 'Some feature');
442
- const result = generateCurrentMd(db, SESSION_ID);
443
- expect(result).not.toContain('### Key Decisions');
444
- });
445
- });
446
-
447
- describe('failed_attempt observations', () => {
448
- const SESSION_ID = 'failed-session';
449
-
450
- beforeEach(() => {
451
- insertSession(db, SESSION_ID);
452
- });
453
-
454
- it('renders FAILED ATTEMPTS section', () => {
455
- insertObservation(db, SESSION_ID, 'failed_attempt', 'Regex approach fails on nested braces');
456
- const result = generateCurrentMd(db, SESSION_ID);
457
- expect(result).toContain('## FAILED ATTEMPTS (DO NOT RETRY)');
458
- expect(result).toContain('Regex approach fails on nested braces');
459
- });
460
-
461
- it('includes detail for failed attempts', () => {
462
- insertObservation(db, SESSION_ID, 'failed_attempt', 'Parser fails', 'Stops at first } character');
463
- const result = generateCurrentMd(db, SESSION_ID);
464
- expect(result).toContain('Stops at first } character');
465
- });
466
-
467
- it('truncates detail to 200 characters', () => {
468
- const longDetail = 'X'.repeat(300);
469
- insertObservation(db, SESSION_ID, 'failed_attempt', 'Some failure', longDetail);
470
- const result = generateCurrentMd(db, SESSION_ID);
471
- // 200 X's should be present
472
- expect(result).toContain('X'.repeat(200));
473
- // But not 201 X's
474
- expect(result).not.toContain('X'.repeat(201));
475
- });
476
-
477
- it('renders multiple failed attempts as list items', () => {
478
- insertObservation(db, SESSION_ID, 'failed_attempt', 'Approach A failed', null, [], 0);
479
- insertObservation(db, SESSION_ID, 'failed_attempt', 'Approach B failed', null, [], 1);
480
- const result = generateCurrentMd(db, SESSION_ID);
481
- expect(result).toContain('- Approach A failed');
482
- expect(result).toContain('- Approach B failed');
483
- });
484
-
485
- it('omits detail line when detail is null/empty', () => {
486
- insertObservation(db, SESSION_ID, 'failed_attempt', 'Silent failure', null);
487
- const result = generateCurrentMd(db, SESSION_ID);
488
- expect(result).toContain('- Silent failure');
489
- // No indented detail line
490
- const lines = result.split('\n');
491
- const failureIdx = lines.findIndex(l => l.includes('- Silent failure'));
492
- expect(failureIdx).toBeGreaterThan(-1);
493
- // Next non-empty line should not be an indented detail
494
- const nextLine = lines[failureIdx + 1] ?? '';
495
- expect(nextLine.startsWith(' ')).toBe(false);
496
- });
497
- });
498
-
499
- describe('vr_check observations', () => {
500
- const SESSION_ID = 'vrcheck-session';
501
-
502
- beforeEach(() => {
503
- insertSession(db, SESSION_ID);
504
- });
505
-
506
- it('renders VERIFICATION EVIDENCE section', () => {
507
- insertObservation(db, SESSION_ID, 'vr_check', 'VR-BUILD: npm run build exits 0');
508
- const result = generateCurrentMd(db, SESSION_ID);
509
- expect(result).toContain('## VERIFICATION EVIDENCE');
510
- expect(result).toContain('VR-BUILD: npm run build exits 0');
511
- });
512
-
513
- it('renders multiple vr_checks as list items', () => {
514
- insertObservation(db, SESSION_ID, 'vr_check', 'VR-BUILD passed', null, [], 0);
515
- insertObservation(db, SESSION_ID, 'vr_check', 'VR-TEST passed', null, [], 1);
516
- const result = generateCurrentMd(db, SESSION_ID);
517
- expect(result).toContain('- VR-BUILD passed');
518
- expect(result).toContain('- VR-TEST passed');
519
- });
520
-
521
- it('does not render VERIFICATION EVIDENCE without vr_check observations', () => {
522
- insertObservation(db, SESSION_ID, 'decision', 'Some decision');
523
- const result = generateCurrentMd(db, SESSION_ID);
524
- expect(result).not.toContain('## VERIFICATION EVIDENCE');
525
- });
526
- });
527
-
528
- // ============================================================
529
- // Mixed observation types
530
- // ============================================================
531
-
532
- describe('mixed observation types', () => {
533
- const SESSION_ID = 'mixed-session';
534
-
535
- beforeEach(() => {
536
- insertSession(db, SESSION_ID, { branch: 'feature/auth', status: 'active' });
537
- });
538
-
539
- it('renders all relevant sections when all observation types are present', () => {
540
- insertObservation(db, SESSION_ID, 'feature', 'Add OAuth support', null, [], 0);
541
- insertObservation(db, SESSION_ID, 'bugfix', 'Fix token refresh', null, [], 1);
542
- insertObservation(db, SESSION_ID, 'refactor', 'Extract auth utils', null, [], 2);
543
- insertObservation(db, SESSION_ID, 'file_change', 'Created/wrote: src/auth.ts', null, ['src/auth.ts'], 3);
544
- insertObservation(db, SESSION_ID, 'file_change', 'Edited: src/server.ts', null, ['src/server.ts'], 4);
545
- insertObservation(db, SESSION_ID, 'decision', 'Use JWT over sessions', null, [], 5);
546
- insertObservation(db, SESSION_ID, 'failed_attempt', 'Cookie approach failed', 'Cross-domain issues', [], 6);
547
- insertObservation(db, SESSION_ID, 'vr_check', 'VR-BUILD passed', null, [], 7);
548
-
549
- const result = generateCurrentMd(db, SESSION_ID);
550
-
551
- expect(result).toContain('## COMPLETED WORK');
552
- expect(result).toContain('### Files Created');
553
- expect(result).toContain('### Files Modified');
554
- expect(result).toContain('### Key Decisions');
555
- expect(result).toContain('## FAILED ATTEMPTS (DO NOT RETRY)');
556
- expect(result).toContain('## VERIFICATION EVIDENCE');
557
- });
558
-
559
- it('observations not in completed types do not contribute to Files Created/Modified', () => {
560
- // decision and failed_attempt are not in ['feature', 'bugfix', 'refactor', 'file_change']
561
- insertObservation(db, SESSION_ID, 'decision', 'Some decision');
562
- insertObservation(db, SESSION_ID, 'failed_attempt', 'Some failure');
563
- const result = generateCurrentMd(db, SESSION_ID);
564
- expect(result).not.toContain('### Files Created');
565
- expect(result).not.toContain('### Files Modified');
566
- });
567
- });
568
-
569
- // ============================================================
570
- // Session summaries
571
- // ============================================================
572
-
573
- describe('session summary - completed text', () => {
574
- const SESSION_ID = 'summary-session';
575
-
576
- beforeEach(() => {
577
- insertSession(db, SESSION_ID);
578
- });
579
-
580
- it('renders completed text from summary in COMPLETED WORK section', () => {
581
- // Need at least one completed-type observation to trigger the section
582
- insertObservation(db, SESSION_ID, 'feature', 'Some feature');
583
- insertSummary(db, SESSION_ID, { completed: 'Implemented full OAuth flow with refresh tokens' });
584
- const result = generateCurrentMd(db, SESSION_ID);
585
- expect(result).toContain('Implemented full OAuth flow with refresh tokens');
586
- });
587
-
588
- it('renders COMPLETED WORK even when only summary has completed field', () => {
589
- insertSummary(db, SESSION_ID, { completed: 'Completed the main task' });
590
- const result = generateCurrentMd(db, SESSION_ID);
591
- expect(result).toContain('## COMPLETED WORK');
592
- expect(result).toContain('Completed the main task');
593
- });
594
-
595
- it('omits COMPLETED WORK section when no observations and no summary', () => {
596
- const result = generateCurrentMd(db, SESSION_ID);
597
- expect(result).not.toContain('## COMPLETED WORK');
598
- });
599
- });
600
-
601
- describe('session summary - next steps', () => {
602
- const SESSION_ID = 'nextsteps-session';
603
-
604
- beforeEach(() => {
605
- insertSession(db, SESSION_ID);
606
- });
607
-
608
- it('renders PENDING section from summary next_steps', () => {
609
- insertSummary(db, SESSION_ID, { nextSteps: '- Run VR-TEST\n- Deploy to staging' });
610
- const result = generateCurrentMd(db, SESSION_ID);
611
- expect(result).toContain('## PENDING');
612
- expect(result).toContain('- Run VR-TEST');
613
- });
614
-
615
- it('does not render PENDING section when no next_steps', () => {
616
- insertSummary(db, SESSION_ID, { completed: 'Done' });
617
- const result = generateCurrentMd(db, SESSION_ID);
618
- expect(result).not.toContain('## PENDING');
619
- });
620
- });
621
-
622
- // ============================================================
623
- // Plan progress
624
- // ============================================================
625
-
626
- describe('plan progress', () => {
627
- const SESSION_ID = 'plan-session';
628
-
629
- beforeEach(() => {
630
- insertSession(db, SESSION_ID, {
631
- planFile: 'docs/plans/2026-02-17-auth-refactor.md',
632
- status: 'active',
633
- });
634
- });
635
-
636
- it('renders PLAN DOCUMENT section when plan_file is set', () => {
637
- const result = generateCurrentMd(db, SESSION_ID);
638
- expect(result).toContain('## PLAN DOCUMENT');
639
- expect(result).toContain('docs/plans/2026-02-17-auth-refactor.md');
640
- });
641
-
642
- it('shows plan progress from summary', () => {
643
- insertSummary(db, SESSION_ID, {
644
- planProgress: {
645
- 'P1-001': 'complete',
646
- 'P1-002': 'complete',
647
- 'P1-003': 'in_progress',
648
- },
649
- });
650
- const result = generateCurrentMd(db, SESSION_ID);
651
- expect(result).toContain('2/3 items complete');
652
- });
653
-
654
- it('shows 0/N when no items are complete', () => {
655
- insertSummary(db, SESSION_ID, {
656
- planProgress: {
657
- 'P1-001': 'pending',
658
- 'P1-002': 'in_progress',
659
- },
660
- });
661
- const result = generateCurrentMd(db, SESSION_ID);
662
- expect(result).toContain('0/2 items complete');
663
- });
664
-
665
- it('shows N/N when all items are complete', () => {
666
- insertSummary(db, SESSION_ID, {
667
- planProgress: {
668
- 'P1-001': 'complete',
669
- 'P1-002': 'complete',
670
- },
671
- });
672
- const result = generateCurrentMd(db, SESSION_ID);
673
- expect(result).toContain('2/2 items complete');
674
- });
675
-
676
- it('does not render progress line when plan_progress is empty', () => {
677
- insertSummary(db, SESSION_ID, { planProgress: {} });
678
- const result = generateCurrentMd(db, SESSION_ID);
679
- expect(result).toContain('## PLAN DOCUMENT');
680
- expect(result).not.toContain('items complete');
681
- });
682
-
683
- it('does not render plan progress when there is no summary', () => {
684
- const result = generateCurrentMd(db, SESSION_ID);
685
- expect(result).toContain('## PLAN DOCUMENT');
686
- expect(result).not.toContain('items complete');
687
- });
688
-
689
- it('handles malformed plan_progress JSON gracefully', () => {
690
- // Insert summary with raw invalid JSON in plan_progress
691
- const now = new Date().toISOString();
692
- const epoch = Math.floor(Date.now() / 1000);
693
- db.prepare(`
694
- INSERT INTO session_summaries (session_id, plan_progress, created_at, created_at_epoch)
695
- VALUES (?, ?, ?, ?)
696
- `).run(SESSION_ID, 'not-valid-json', now, epoch);
697
-
698
- // Should not throw and should still render the plan document section
699
- const result = generateCurrentMd(db, SESSION_ID);
700
- expect(result).toContain('## PLAN DOCUMENT');
701
- expect(result).not.toContain('items complete');
702
- });
703
- });
704
-
705
- describe('session without plan_file', () => {
706
- it('does not render PLAN DOCUMENT section', () => {
707
- insertSession(db, 'no-plan-session', { status: 'active' });
708
- const result = generateCurrentMd(db, 'no-plan-session');
709
- expect(result).not.toContain('## PLAN DOCUMENT');
710
- });
711
- });
712
-
713
- // ============================================================
714
- // Formatted output / date formatting
715
- // ============================================================
716
-
717
- describe('formatted output', () => {
718
- const SESSION_ID = 'format-session';
719
-
720
- beforeEach(() => {
721
- insertSession(db, SESSION_ID, { branch: 'main', status: 'active' });
722
- });
723
-
724
- it('includes a formatted date in the heading', () => {
725
- const result = generateCurrentMd(db, SESSION_ID);
726
- // formatDate produces "Month Day, Year" style
727
- const months = ['January', 'February', 'March', 'April', 'May', 'June',
728
- 'July', 'August', 'September', 'October', 'November', 'December'];
729
- const currentMonth = months[new Date().getMonth()];
730
- expect(result).toContain(currentMonth);
731
- });
732
-
733
- it('heading is on the first line', () => {
734
- const result = generateCurrentMd(db, SESSION_ID);
735
- const firstLine = result.split('\n')[0];
736
- expect(firstLine).toMatch(/^# Session State - .+/);
737
- });
738
-
739
- it('auto-generated annotation is present', () => {
740
- const result = generateCurrentMd(db, SESSION_ID);
741
- expect(result).toContain('auto-generated from massu-memory');
742
- });
743
-
744
- it('status line contains task summary', () => {
745
- insertPrompt(db, SESSION_ID, 'Implement new feature', 1);
746
- const result = generateCurrentMd(db, SESSION_ID);
747
- expect(result).toContain('**Status**:');
748
- expect(result).toContain('Implement new feature');
749
- });
750
-
751
- it('Branch field is present', () => {
752
- const result = generateCurrentMd(db, SESSION_ID);
753
- expect(result).toContain('**Branch**: main');
754
- });
755
-
756
- it('Branch shows unknown when git_branch is null', () => {
757
- insertSession(db, 'no-branch-session', { status: 'active' });
758
- const result = generateCurrentMd(db, 'no-branch-session');
759
- expect(result).toContain('**Branch**: unknown');
760
- });
761
-
762
- it('file table rows use backtick-wrapped paths', () => {
763
- insertObservation(db, SESSION_ID, 'file_change', 'Created/wrote: src/index.ts', null, ['src/index.ts']);
764
- const result = generateCurrentMd(db, SESSION_ID);
765
- expect(result).toContain('`src/index.ts`');
766
- });
767
-
768
- it('Files Created table has correct header', () => {
769
- insertObservation(db, SESSION_ID, 'file_change', 'Created/wrote: src/foo.ts', null, ['src/foo.ts']);
770
- const result = generateCurrentMd(db, SESSION_ID);
771
- expect(result).toContain('| File | Purpose |');
772
- expect(result).toContain('|------|---------|');
773
- });
774
-
775
- it('Files Modified table has correct header', () => {
776
- insertObservation(db, SESSION_ID, 'file_change', 'Edited: src/bar.ts', null, ['src/bar.ts']);
777
- const result = generateCurrentMd(db, SESSION_ID);
778
- expect(result).toContain('| File | Change |');
779
- expect(result).toContain('|------|--------|');
780
- });
781
- });
782
-
783
- // ============================================================
784
- // Multiple summaries — only latest is used
785
- // ============================================================
786
-
787
- describe('multiple summaries', () => {
788
- const SESSION_ID = 'multi-summary-session';
789
-
790
- beforeEach(() => {
791
- insertSession(db, SESSION_ID, { planFile: 'docs/plans/2026-02-17-test.md' });
792
- });
793
-
794
- it('uses only the most recent summary (by created_at_epoch DESC)', () => {
795
- const now = Math.floor(Date.now() / 1000);
796
- // Insert older summary first
797
- db.prepare(`
798
- INSERT INTO session_summaries (session_id, next_steps, plan_progress, created_at, created_at_epoch)
799
- VALUES (?, ?, ?, ?, ?)
800
- `).run(SESSION_ID, 'Old next step', '{}', new Date((now - 100) * 1000).toISOString(), now - 100);
801
- // Insert newer summary
802
- db.prepare(`
803
- INSERT INTO session_summaries (session_id, next_steps, plan_progress, created_at, created_at_epoch)
804
- VALUES (?, ?, ?, ?, ?)
805
- `).run(SESSION_ID, 'New next step', '{}', new Date(now * 1000).toISOString(), now);
806
-
807
- const result = generateCurrentMd(db, SESSION_ID);
808
- expect(result).toContain('New next step');
809
- expect(result).not.toContain('Old next step');
810
- });
811
- });
812
-
813
- // ============================================================
814
- // Comprehensive scenario
815
- // ============================================================
816
-
817
- describe('comprehensive scenario', () => {
818
- const SESSION_ID = 'full-scenario-session';
819
-
820
- beforeEach(() => {
821
- insertSession(db, SESSION_ID, {
822
- branch: 'feature/session-state',
823
- status: 'active',
824
- planFile: 'docs/plans/2026-02-17-session-state.md',
825
- });
826
- insertPrompt(db, SESSION_ID, 'Generate CURRENT.md from memory database automatically', 1);
827
-
828
- // Various observation types
829
- insertObservation(db, SESSION_ID, 'feature', 'Implement generateCurrentMd function', null, [], 0);
830
- insertObservation(db, SESSION_ID, 'file_change', 'Created/wrote: src/session-state-generator.ts', null, ['src/session-state-generator.ts'], 1);
831
- insertObservation(db, SESSION_ID, 'file_change', 'Edited: src/tools.ts', null, ['src/tools.ts'], 2);
832
- insertObservation(db, SESSION_ID, 'decision', 'Query sessions table via session_id directly', null, [], 3);
833
- insertObservation(db, SESSION_ID, 'decision', 'Use ORDER BY created_at_epoch for observation ordering', null, [], 4);
834
- insertObservation(db, SESSION_ID, 'failed_attempt', 'Attempted to use getMemoryDb in tests', 'Config dependency caused failures', [], 5);
835
- insertObservation(db, SESSION_ID, 'vr_check', 'VR-BUILD: npm run build exits 0', null, [], 6);
836
- insertObservation(db, SESSION_ID, 'vr_check', 'VR-TEST: all 880 tests pass', null, [], 7);
837
-
838
- insertSummary(db, SESSION_ID, {
839
- completed: 'generateCurrentMd fully implemented and tested',
840
- nextSteps: '- Run /massu-commit\n- Push to remote',
841
- planProgress: {
842
- 'P5-001': 'complete',
843
- 'P5-002': 'in_progress',
844
- 'P5-003': 'pending',
845
- },
846
- });
847
- });
848
-
849
- it('produces a well-formed markdown document', () => {
850
- const result = generateCurrentMd(db, SESSION_ID);
851
-
852
- // Header
853
- expect(result).toMatch(/^# Session State - /m);
854
-
855
- // Metadata
856
- expect(result).toContain('**Session ID**: full-scenario-session');
857
- expect(result).toContain('**Branch**: feature/session-state');
858
- expect(result).toContain('IN PROGRESS');
859
- expect(result).toContain('Generate CURRENT.md from memory database automatically');
860
-
861
- // Completed work
862
- expect(result).toContain('## COMPLETED WORK');
863
- expect(result).toContain('generateCurrentMd fully implemented and tested');
864
- expect(result).toContain('### Files Created');
865
- expect(result).toContain('src/session-state-generator.ts');
866
- expect(result).toContain('### Files Modified');
867
- expect(result).toContain('src/tools.ts');
868
-
869
- // Key decisions
870
- expect(result).toContain('### Key Decisions');
871
- expect(result).toContain('- Query sessions table via session_id directly');
872
- expect(result).toContain('- Use ORDER BY created_at_epoch for observation ordering');
873
-
874
- // Failed attempts
875
- expect(result).toContain('## FAILED ATTEMPTS (DO NOT RETRY)');
876
- expect(result).toContain('Attempted to use getMemoryDb in tests');
877
- expect(result).toContain('Config dependency caused failures');
878
-
879
- // Verification evidence
880
- expect(result).toContain('## VERIFICATION EVIDENCE');
881
- expect(result).toContain('- VR-BUILD: npm run build exits 0');
882
- expect(result).toContain('- VR-TEST: all 880 tests pass');
883
-
884
- // Pending
885
- expect(result).toContain('## PENDING');
886
- expect(result).toContain('- Run /massu-commit');
887
-
888
- // Plan document
889
- expect(result).toContain('## PLAN DOCUMENT');
890
- expect(result).toContain('docs/plans/2026-02-17-session-state.md');
891
- expect(result).toContain('1/3 items complete');
892
- });
893
-
894
- it('output is a string ending with newline (from lines.join)', () => {
895
- const result = generateCurrentMd(db, SESSION_ID);
896
- expect(typeof result).toBe('string');
897
- expect(result.length).toBeGreaterThan(0);
898
- });
899
- });
900
- });