@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,524 +0,0 @@
1
- // Copyright (c) 2026 Massu. All rights reserved.
2
- // Licensed under BSL 1.1 - see LICENSE file for details.
3
-
4
- import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
5
- import Database from 'better-sqlite3';
6
-
7
- // ============================================================
8
- // Mocks — must be declared before module imports
9
- // ============================================================
10
-
11
- // Mock fs so we never touch the real filesystem
12
- vi.mock('fs', () => ({
13
- existsSync: vi.fn(),
14
- readFileSync: vi.fn(),
15
- writeFileSync: vi.fn(),
16
- mkdirSync: vi.fn(),
17
- renameSync: vi.fn(),
18
- }));
19
-
20
- // Mock config so we get a stable, deterministic project root
21
- vi.mock('../config.ts', () => ({
22
- getProjectRoot: vi.fn(() => '/test/project'),
23
- getConfig: vi.fn(() => ({
24
- toolPrefix: 'massu',
25
- project: { name: 'test-project', root: '/test/project' },
26
- framework: { type: 'typescript', router: 'none', orm: 'none', ui: 'none' },
27
- paths: { source: 'src', aliases: {} },
28
- domains: [],
29
- rules: [],
30
- })),
31
- resetConfig: vi.fn(),
32
- }));
33
-
34
- // Mock session-state-generator so we control what generateCurrentMd returns
35
- vi.mock('../session-state-generator.ts', () => ({
36
- generateCurrentMd: vi.fn(() => '# Session State - February 17, 2026\n\n**Task**: test task\n'),
37
- }));
38
-
39
- // ============================================================
40
- // Imports — after mocks are declared
41
- // ============================================================
42
-
43
- import {
44
- existsSync,
45
- readFileSync,
46
- writeFileSync,
47
- mkdirSync,
48
- renameSync,
49
- } from 'fs';
50
- import { archiveAndRegenerate } from '../session-archiver.ts';
51
- import { generateCurrentMd } from '../session-state-generator.ts';
52
-
53
- // ============================================================
54
- // Typed mock helpers
55
- // ============================================================
56
-
57
- const mockExistsSync = existsSync as ReturnType<typeof vi.fn>;
58
- const mockReadFileSync = readFileSync as ReturnType<typeof vi.fn>;
59
- const mockWriteFileSync = writeFileSync as ReturnType<typeof vi.fn>;
60
- const mockMkdirSync = mkdirSync as ReturnType<typeof vi.fn>;
61
- const mockRenameSync = renameSync as ReturnType<typeof vi.fn>;
62
- const mockGenerateCurrentMd = generateCurrentMd as ReturnType<typeof vi.fn>;
63
-
64
- // ============================================================
65
- // Test DB factory — minimal schema needed by generateCurrentMd
66
- // (even though we mock that function, the db arg must be valid)
67
- // ============================================================
68
-
69
- function createTestDb(): Database.Database {
70
- const db = new Database(':memory:');
71
- db.pragma('journal_mode = WAL');
72
- db.pragma('foreign_keys = ON');
73
- db.exec(`
74
- CREATE TABLE IF NOT EXISTS sessions (
75
- id INTEGER PRIMARY KEY AUTOINCREMENT,
76
- session_id TEXT UNIQUE NOT NULL,
77
- project TEXT NOT NULL DEFAULT 'my-project',
78
- git_branch TEXT,
79
- started_at TEXT NOT NULL,
80
- started_at_epoch INTEGER NOT NULL,
81
- ended_at TEXT,
82
- ended_at_epoch INTEGER,
83
- status TEXT NOT NULL DEFAULT 'active',
84
- plan_file TEXT,
85
- plan_phase TEXT,
86
- task_id TEXT
87
- );
88
- CREATE TABLE IF NOT EXISTS observations (
89
- id INTEGER PRIMARY KEY AUTOINCREMENT,
90
- session_id TEXT NOT NULL,
91
- type TEXT NOT NULL,
92
- title TEXT NOT NULL,
93
- detail TEXT,
94
- files_involved TEXT DEFAULT '[]',
95
- plan_item TEXT,
96
- cr_rule TEXT,
97
- vr_type TEXT,
98
- evidence TEXT,
99
- importance INTEGER NOT NULL DEFAULT 3,
100
- recurrence_count INTEGER NOT NULL DEFAULT 1,
101
- original_tokens INTEGER DEFAULT 0,
102
- created_at TEXT NOT NULL,
103
- created_at_epoch INTEGER NOT NULL
104
- );
105
- CREATE TABLE IF NOT EXISTS session_summaries (
106
- id INTEGER PRIMARY KEY AUTOINCREMENT,
107
- session_id TEXT NOT NULL,
108
- request TEXT, investigated TEXT, decisions TEXT, completed TEXT,
109
- failed_attempts TEXT, next_steps TEXT,
110
- files_created TEXT DEFAULT '[]', files_modified TEXT DEFAULT '[]',
111
- verification_results TEXT DEFAULT '{}', plan_progress TEXT DEFAULT '{}',
112
- created_at TEXT NOT NULL, created_at_epoch INTEGER NOT NULL
113
- );
114
- CREATE TABLE IF NOT EXISTS user_prompts (
115
- id INTEGER PRIMARY KEY AUTOINCREMENT,
116
- session_id TEXT NOT NULL,
117
- prompt_text TEXT NOT NULL,
118
- prompt_number INTEGER NOT NULL DEFAULT 1,
119
- created_at TEXT NOT NULL, created_at_epoch INTEGER NOT NULL
120
- );
121
- CREATE TABLE IF NOT EXISTS memory_meta (key TEXT PRIMARY KEY, value TEXT NOT NULL);
122
- `);
123
- return db;
124
- }
125
-
126
- // ============================================================
127
- // Tests
128
- // ============================================================
129
-
130
- describe('session-archiver', () => {
131
- let db: Database.Database;
132
-
133
- // These paths are derived from the mocked getProjectRoot() = '/test/project'
134
- const CURRENT_MD = '/test/project/.claude/session-state/CURRENT.md';
135
- const ARCHIVE_DIR = '/test/project/.claude/session-state/archive';
136
- const NEW_CONTENT = '# Session State - February 17, 2026\n\n**Task**: test task\n';
137
-
138
- beforeEach(() => {
139
- db = createTestDb();
140
- vi.clearAllMocks();
141
- // Default: generateCurrentMd returns deterministic content
142
- mockGenerateCurrentMd.mockReturnValue(NEW_CONTENT);
143
- });
144
-
145
- afterEach(() => {
146
- db.close();
147
- });
148
-
149
- // ============================================================
150
- // archiveAndRegenerate — CURRENT.md does not exist
151
- // ============================================================
152
-
153
- describe('archiveAndRegenerate — CURRENT.md does not exist', () => {
154
- beforeEach(() => {
155
- // CURRENT.md does not exist; the parent dir also does not exist
156
- mockExistsSync.mockImplementation((p: string) => {
157
- // Neither the file nor the parent dir exist initially
158
- return false;
159
- });
160
- });
161
-
162
- it('does not archive when CURRENT.md is missing', () => {
163
- const result = archiveAndRegenerate(db, 'session-1');
164
-
165
- expect(result.archived).toBe(false);
166
- expect(result.archivePath).toBeUndefined();
167
- });
168
-
169
- it('calls generateCurrentMd with the db and sessionId', () => {
170
- archiveAndRegenerate(db, 'session-1');
171
-
172
- expect(mockGenerateCurrentMd).toHaveBeenCalledOnce();
173
- expect(mockGenerateCurrentMd).toHaveBeenCalledWith(db, 'session-1');
174
- });
175
-
176
- it('creates the parent directory when it does not exist', () => {
177
- archiveAndRegenerate(db, 'session-1');
178
-
179
- // mkdirSync should be called for the parent dir of CURRENT.md
180
- expect(mockMkdirSync).toHaveBeenCalledWith(
181
- '/test/project/.claude/session-state',
182
- { recursive: true }
183
- );
184
- });
185
-
186
- it('writes the new CURRENT.md', () => {
187
- archiveAndRegenerate(db, 'session-1');
188
-
189
- expect(mockWriteFileSync).toHaveBeenCalledWith(
190
- CURRENT_MD,
191
- NEW_CONTENT,
192
- 'utf-8'
193
- );
194
- });
195
-
196
- it('returns the new content', () => {
197
- const result = archiveAndRegenerate(db, 'session-1');
198
-
199
- expect(result.newContent).toBe(NEW_CONTENT);
200
- });
201
- });
202
-
203
- // ============================================================
204
- // archiveAndRegenerate — CURRENT.md exists but is nearly empty
205
- // ============================================================
206
-
207
- describe('archiveAndRegenerate — CURRENT.md exists but is too short to archive', () => {
208
- beforeEach(() => {
209
- mockExistsSync.mockImplementation((_p: string) => true);
210
- // Fewer than 10 non-whitespace characters — should not archive
211
- mockReadFileSync.mockReturnValue('hi\n');
212
- });
213
-
214
- it('does not archive trivially short content', () => {
215
- const result = archiveAndRegenerate(db, 'session-1');
216
-
217
- expect(result.archived).toBe(false);
218
- expect(result.archivePath).toBeUndefined();
219
- expect(mockRenameSync).not.toHaveBeenCalled();
220
- });
221
-
222
- it('still writes a new CURRENT.md', () => {
223
- archiveAndRegenerate(db, 'session-1');
224
-
225
- expect(mockWriteFileSync).toHaveBeenCalledWith(CURRENT_MD, NEW_CONTENT, 'utf-8');
226
- });
227
- });
228
-
229
- // ============================================================
230
- // archiveAndRegenerate — CURRENT.md exists with real content
231
- // ============================================================
232
-
233
- describe('archiveAndRegenerate — CURRENT.md exists with archivable content', () => {
234
- const EXISTING_CONTENT = [
235
- '# Session State - January 30, 2026',
236
- '',
237
- '**Last Updated**: 2026-01-30 10:00:00 (auto-generated from massu-memory)',
238
- '**Status**: IN PROGRESS - implement auth module',
239
- '**Task**: implement the authentication module',
240
- '**Session ID**: old-session',
241
- '**Branch**: main',
242
- ].join('\n');
243
-
244
- beforeEach(() => {
245
- mockExistsSync.mockImplementation((_p: string) => true);
246
- mockReadFileSync.mockReturnValue(EXISTING_CONTENT);
247
- });
248
-
249
- it('sets archived to true', () => {
250
- const result = archiveAndRegenerate(db, 'new-session');
251
-
252
- expect(result.archived).toBe(true);
253
- });
254
-
255
- it('returns an archivePath', () => {
256
- const result = archiveAndRegenerate(db, 'new-session');
257
-
258
- expect(result.archivePath).toBeDefined();
259
- expect(result.archivePath).toContain(ARCHIVE_DIR);
260
- expect(result.archivePath!.endsWith('.md')).toBe(true);
261
- });
262
-
263
- it('archive filename contains the ISO date from the content', () => {
264
- const result = archiveAndRegenerate(db, 'new-session');
265
-
266
- // isoMatch in extractArchiveInfo picks the first YYYY-MM-DD in content
267
- // which is 2026-01-30 (from the **Last Updated** line)
268
- expect(result.archivePath).toContain('2026-01-30');
269
- });
270
-
271
- it('archive filename contains a slug derived from the Task field', () => {
272
- const result = archiveAndRegenerate(db, 'new-session');
273
-
274
- // Task is "implement the authentication module" -> "implement-the-authentication-module"
275
- expect(result.archivePath).toContain('implement-the-authentication-module');
276
- });
277
-
278
- it('calls renameSync to atomically move the file', () => {
279
- const result = archiveAndRegenerate(db, 'new-session');
280
-
281
- expect(mockRenameSync).toHaveBeenCalledOnce();
282
- expect(mockRenameSync).toHaveBeenCalledWith(CURRENT_MD, result.archivePath);
283
- });
284
-
285
- it('does not call writeFileSync for the archive (rename is used)', () => {
286
- archiveAndRegenerate(db, 'new-session');
287
-
288
- // writeFileSync should only be called once — for the new CURRENT.md
289
- const writeCallArgs = (mockWriteFileSync as ReturnType<typeof vi.fn>).mock.calls;
290
- expect(writeCallArgs.every(([p]: [string]) => p === CURRENT_MD)).toBe(true);
291
- });
292
-
293
- it('writes the new CURRENT.md content after archiving', () => {
294
- archiveAndRegenerate(db, 'new-session');
295
-
296
- expect(mockWriteFileSync).toHaveBeenCalledWith(CURRENT_MD, NEW_CONTENT, 'utf-8');
297
- });
298
-
299
- it('does not create archive directory when it already exists', () => {
300
- // existsSync returns true for everything including archive dir
301
- archiveAndRegenerate(db, 'new-session');
302
-
303
- expect(mockMkdirSync).not.toHaveBeenCalledWith(ARCHIVE_DIR, expect.anything());
304
- });
305
- });
306
-
307
- // ============================================================
308
- // archiveAndRegenerate — archive directory does not exist yet
309
- // ============================================================
310
-
311
- describe('archiveAndRegenerate — archive directory needs to be created', () => {
312
- const EXISTING_CONTENT = '# Session State - February 1, 2026\n\n**Task**: add feature\n**Status**: IN PROGRESS - add feature\n';
313
-
314
- beforeEach(() => {
315
- mockExistsSync.mockImplementation((p: string) => {
316
- if (p === CURRENT_MD) return true;
317
- if (p === ARCHIVE_DIR) return false;
318
- // Parent dir of CURRENT.md (/test/project/.claude/session-state) exists
319
- return true;
320
- });
321
- mockReadFileSync.mockReturnValue(EXISTING_CONTENT);
322
- });
323
-
324
- it('creates the archive directory with recursive flag', () => {
325
- archiveAndRegenerate(db, 'session-1');
326
-
327
- expect(mockMkdirSync).toHaveBeenCalledWith(ARCHIVE_DIR, { recursive: true });
328
- });
329
- });
330
-
331
- // ============================================================
332
- // archiveAndRegenerate — renameSync fails (cross-device scenario)
333
- // ============================================================
334
-
335
- describe('archiveAndRegenerate — renameSync throws (cross-device fallback)', () => {
336
- const EXISTING_CONTENT = '# Session State - February 10, 2026\n\n**Task**: cross device task\n';
337
-
338
- beforeEach(() => {
339
- mockExistsSync.mockImplementation((p: string) => {
340
- if (p === CURRENT_MD) return true;
341
- return true;
342
- });
343
- mockReadFileSync.mockReturnValue(EXISTING_CONTENT);
344
- mockRenameSync.mockImplementation(() => {
345
- throw new Error('EXDEV: cross-device link not permitted');
346
- });
347
- });
348
-
349
- it('falls back to writeFileSync when rename throws', () => {
350
- const result = archiveAndRegenerate(db, 'session-1');
351
-
352
- // archived should still be true because the fallback copy worked
353
- expect(result.archived).toBe(true);
354
- // writeFileSync should have been called for the archive copy AND for the new CURRENT.md
355
- const writeCallPaths = (mockWriteFileSync as ReturnType<typeof vi.fn>).mock.calls.map(
356
- ([p]: [string]) => p
357
- );
358
- // First call should be the archive path (copy fallback)
359
- expect(writeCallPaths[0]).toBe(result.archivePath);
360
- // Second call should write the new CURRENT.md
361
- expect(writeCallPaths[1]).toBe(CURRENT_MD);
362
- });
363
-
364
- it('writes existing content to the archive path in the fallback', () => {
365
- const result = archiveAndRegenerate(db, 'session-1');
366
-
367
- expect(mockWriteFileSync).toHaveBeenCalledWith(result.archivePath, EXISTING_CONTENT);
368
- });
369
- });
370
-
371
- // ============================================================
372
- // extractArchiveInfo — tested indirectly via archiveAndRegenerate
373
- // ============================================================
374
-
375
- describe('extractArchiveInfo — date extraction', () => {
376
- function runWithContent(content: string) {
377
- mockExistsSync.mockImplementation((p: string) => (p === CURRENT_MD ? true : true));
378
- mockReadFileSync.mockReturnValue(content);
379
- return archiveAndRegenerate(db, 'session-x');
380
- }
381
-
382
- beforeEach(() => {
383
- vi.clearAllMocks();
384
- mockGenerateCurrentMd.mockReturnValue(NEW_CONTENT);
385
- });
386
-
387
- it('extracts ISO date from **Last Updated** line (iso date wins over header date)', () => {
388
- const content = [
389
- '# Session State - January 30, 2026',
390
- '**Last Updated**: 2026-01-30 (auto-generated)',
391
- '**Task**: something meaningful enough to parse',
392
- ].join('\n');
393
- const result = runWithContent(content);
394
-
395
- expect(result.archivePath).toContain('2026-01-30');
396
- });
397
-
398
- it('extracts date from "# Session State - Month Day, Year" header when no ISO date present', () => {
399
- // Content with no ISO date pattern (no YYYY-MM-DD anywhere) but with the header
400
- const content = [
401
- '# Session State - March 5, 2026',
402
- '**Task**: build a thing long enough to matter here',
403
- ].join('\n');
404
- const result = runWithContent(content);
405
-
406
- expect(result.archivePath).toContain('2026-03-05');
407
- });
408
-
409
- it('falls back to today\'s ISO date when no date pattern found', () => {
410
- const today = new Date().toISOString().split('T')[0];
411
- const content = 'This content has no date at all and is long enough to be archived by the function logic.';
412
- const result = runWithContent(content);
413
-
414
- expect(result.archivePath).toContain(today);
415
- });
416
- });
417
-
418
- describe('extractArchiveInfo — slug extraction', () => {
419
- function runWithContent(content: string) {
420
- mockExistsSync.mockImplementation((_p: string) => true);
421
- mockReadFileSync.mockReturnValue(content);
422
- return archiveAndRegenerate(db, 'session-x');
423
- }
424
-
425
- beforeEach(() => {
426
- vi.clearAllMocks();
427
- mockGenerateCurrentMd.mockReturnValue(NEW_CONTENT);
428
- });
429
-
430
- it('uses Task field for slug when present', () => {
431
- const content = [
432
- '# Session State - February 17, 2026',
433
- '**Task**: Implement the OAuth2 flow',
434
- '**Status**: IN PROGRESS - something else',
435
- ].join('\n');
436
- const result = runWithContent(content);
437
-
438
- expect(result.archivePath).toContain('implement-the-oauth2-flow');
439
- });
440
-
441
- it('falls back to Status description when Task is absent', () => {
442
- // Note: the status regex is \w+ (single word) before the dash,
443
- // so "IN PROGRESS" would not match — use a single-word status like "ACTIVE"
444
- const content = [
445
- '# Session State - February 17, 2026',
446
- '**Status**: ACTIVE - refactor the database layer',
447
- ].join('\n');
448
- const result = runWithContent(content);
449
-
450
- expect(result.archivePath).toContain('refactor-the-database-layer');
451
- });
452
-
453
- it('uses "session" slug when neither Task nor Status description is found', () => {
454
- const content = [
455
- '# Session State - February 17, 2026',
456
- 'No task or status fields present in this document at all.',
457
- ].join('\n');
458
- const result = runWithContent(content);
459
-
460
- expect(result.archivePath).toContain('-session');
461
- });
462
-
463
- it('lowercases and hyphenates special characters in slug', () => {
464
- const content = [
465
- '# Session State - February 17, 2026',
466
- '**Task**: Fix the Auth/JWT (token) issue!',
467
- ].join('\n');
468
- const result = runWithContent(content);
469
-
470
- // Special chars become hyphens, leading/trailing hyphens removed
471
- expect(result.archivePath).toContain('fix-the-auth-jwt-token-issue');
472
- });
473
-
474
- it('truncates slug to 50 characters', () => {
475
- const longTask = 'A'.repeat(60);
476
- const content = [
477
- '# Session State - February 17, 2026',
478
- `**Task**: ${longTask}`,
479
- ].join('\n');
480
- const result = runWithContent(content);
481
-
482
- const filename = result.archivePath!.split('/').pop()!;
483
- // date (10) + '-' (1) + slug (<=50) + '.md' (3) = <=64 chars
484
- const slug = filename.replace(/^\d{4}-\d{2}-\d{2}-/, '').replace(/\.md$/, '');
485
- expect(slug.length).toBeLessThanOrEqual(50);
486
- });
487
- });
488
-
489
- // ============================================================
490
- // Return value contract
491
- // ============================================================
492
-
493
- describe('return value contract', () => {
494
- it('always returns { archived, newContent } even when nothing existed', () => {
495
- mockExistsSync.mockReturnValue(false);
496
-
497
- const result = archiveAndRegenerate(db, 'fresh-session');
498
-
499
- expect(result).toHaveProperty('archived');
500
- expect(result).toHaveProperty('newContent');
501
- expect(typeof result.archived).toBe('boolean');
502
- expect(typeof result.newContent).toBe('string');
503
- });
504
-
505
- it('newContent always matches what generateCurrentMd returns', () => {
506
- const customContent = '# Custom Content\n';
507
- mockGenerateCurrentMd.mockReturnValue(customContent);
508
- mockExistsSync.mockReturnValue(false);
509
-
510
- const result = archiveAndRegenerate(db, 'any-session');
511
-
512
- expect(result.newContent).toBe(customContent);
513
- });
514
-
515
- it('archivePath is undefined when archived is false', () => {
516
- mockExistsSync.mockReturnValue(false);
517
-
518
- const result = archiveAndRegenerate(db, 'any-session');
519
-
520
- expect(result.archived).toBe(false);
521
- expect(result.archivePath).toBeUndefined();
522
- });
523
- });
524
- });