@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,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
|
-
});
|