@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.
- package/README.md +2 -2
- package/dist/hooks/cost-tracker.js +23 -35
- package/dist/hooks/post-edit-context.js +2 -2
- package/dist/hooks/post-tool-use.js +43 -58
- package/dist/hooks/pre-compact.js +23 -38
- package/dist/hooks/pre-delete-check.js +18 -31
- package/dist/hooks/quality-event.js +23 -35
- package/dist/hooks/session-end.js +62 -78
- package/dist/hooks/session-start.js +33 -42
- package/dist/hooks/user-prompt.js +23 -38
- package/package.json +8 -14
- package/src/adr-generator.ts +9 -2
- package/src/analytics.ts +9 -3
- package/src/audit-trail.ts +10 -3
- package/src/cloud-sync.ts +14 -18
- package/src/commands/init.ts +1 -5
- package/src/cost-tracker.ts +11 -6
- package/src/dependency-scorer.ts +9 -2
- package/src/docs-tools.ts +13 -10
- package/src/hooks/post-edit-context.ts +3 -3
- package/src/hooks/session-end.ts +3 -3
- package/src/hooks/session-start.ts +2 -2
- package/src/memory-db.ts +1351 -23
- package/src/memory-tools.ts +14 -15
- package/src/observability-tools.ts +13 -2
- package/src/prompt-analyzer.ts +9 -2
- package/src/regression-detector.ts +9 -3
- package/src/security-scorer.ts +9 -2
- package/src/sentinel-db.ts +43 -88
- package/src/sentinel-tools.ts +8 -11
- package/src/server.ts +1 -2
- package/src/team-knowledge.ts +9 -2
- package/src/tools.ts +771 -35
- package/src/validate-features-runner.ts +0 -1
- package/src/validation-engine.ts +9 -2
- package/dist/cli.js +0 -7890
- package/dist/server.js +0 -7008
- package/src/__tests__/adr-generator.test.ts +0 -260
- package/src/__tests__/analytics.test.ts +0 -282
- package/src/__tests__/audit-trail.test.ts +0 -382
- package/src/__tests__/backfill-sessions.test.ts +0 -690
- package/src/__tests__/cli.test.ts +0 -290
- package/src/__tests__/cloud-sync.test.ts +0 -261
- package/src/__tests__/config-sections.test.ts +0 -359
- package/src/__tests__/config.test.ts +0 -732
- package/src/__tests__/cost-tracker.test.ts +0 -348
- package/src/__tests__/db.test.ts +0 -177
- package/src/__tests__/dependency-scorer.test.ts +0 -325
- package/src/__tests__/docs-integration.test.ts +0 -178
- package/src/__tests__/docs-tools.test.ts +0 -199
- package/src/__tests__/domains.test.ts +0 -236
- package/src/__tests__/hooks.test.ts +0 -221
- package/src/__tests__/import-resolver.test.ts +0 -95
- package/src/__tests__/integration/path-traversal.test.ts +0 -134
- package/src/__tests__/integration/pricing-consistency.test.ts +0 -88
- package/src/__tests__/integration/tool-registration.test.ts +0 -146
- package/src/__tests__/memory-db.test.ts +0 -404
- package/src/__tests__/memory-enhancements.test.ts +0 -316
- package/src/__tests__/memory-tools.test.ts +0 -199
- package/src/__tests__/middleware-tree.test.ts +0 -177
- package/src/__tests__/observability-tools.test.ts +0 -595
- package/src/__tests__/observability.test.ts +0 -437
- package/src/__tests__/observation-extractor.test.ts +0 -167
- package/src/__tests__/page-deps.test.ts +0 -60
- package/src/__tests__/prompt-analyzer.test.ts +0 -298
- package/src/__tests__/regression-detector.test.ts +0 -295
- package/src/__tests__/rules.test.ts +0 -87
- package/src/__tests__/schema-mapper.test.ts +0 -29
- package/src/__tests__/security-scorer.test.ts +0 -238
- package/src/__tests__/security-utils.test.ts +0 -175
- package/src/__tests__/sentinel-db.test.ts +0 -491
- package/src/__tests__/sentinel-scanner.test.ts +0 -750
- package/src/__tests__/sentinel-tools.test.ts +0 -324
- package/src/__tests__/sentinel-types.test.ts +0 -750
- package/src/__tests__/server.test.ts +0 -452
- package/src/__tests__/session-archiver.test.ts +0 -524
- package/src/__tests__/session-state-generator.test.ts +0 -900
- package/src/__tests__/team-knowledge.test.ts +0 -327
- package/src/__tests__/tools.test.ts +0 -340
- package/src/__tests__/transcript-parser.test.ts +0 -195
- package/src/__tests__/trpc-index.test.ts +0 -25
- package/src/__tests__/validate-features-runner.test.ts +0 -517
- package/src/__tests__/validation-engine.test.ts +0 -300
- package/src/core-tools.ts +0 -685
- package/src/memory-queries.ts +0 -804
- package/src/memory-schema.ts +0 -546
- 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
|
-
});
|