@principles/pd-cli 1.145.2 → 1.146.1

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.
@@ -1,5 +1,7 @@
1
1
  /**
2
- * Tests for buildTrajectoryEvidenceFromDb — PRI-341
2
+ * Tests for acquireTrajectoryEvidenceFromDb — PRI-341 extraction behavior,
3
+ * PRI-642 typed classification (the legacy array wrapper was removed with
4
+ * its last production consumer).
3
5
  *
4
6
  * Uses real temporary SQLite DBs (trajectory.db) to validate evidence extraction.
5
7
  */
@@ -9,7 +11,7 @@ import * as fs from 'fs';
9
11
  import * as path from 'path';
10
12
  import * as os from 'os';
11
13
  import { MAX_EVIDENCE_ENTRIES } from '@principles/core/runtime-v2';
12
- import { buildTrajectoryEvidenceFromDb } from '../../src/commands/build-trajectory-evidence.js';
14
+ import { acquireTrajectoryEvidenceFromDb } from '../../src/commands/build-trajectory-evidence.js';
13
15
 
14
16
  // ── Helpers ─────────────────────────────────────────────────────────────────
15
17
 
@@ -127,7 +129,7 @@ function insertToolCall(
127
129
 
128
130
  // ── Tests ───────────────────────────────────────────────────────────────────
129
131
 
130
- describe('buildTrajectoryEvidenceFromDb — PRI-341', () => {
132
+ describe('acquireTrajectoryEvidenceFromDbbehavior via typed API (PRI-341 extraction, PRI-642 wrapper removal)', () => {
131
133
  beforeEach(() => {
132
134
  createStateDir();
133
135
  });
@@ -140,198 +142,261 @@ describe('buildTrajectoryEvidenceFromDb — PRI-341', () => {
140
142
  }
141
143
  });
142
144
 
143
- // 用例 A: trajectory.db contains 3 assistant turns for session '123'
144
- // returns evidence with length ≤ MAX_EVIDENCE_ENTRIES, each entry has sourceRef and note
145
- it('A: returns evidence entries from assistant turns ( MAX_EVIDENCE_ENTRIES)', () => {
145
+ /** Available-class entries, or the unavailable reason for assertions. */
146
+ function acquire(sessionId: string | undefined) {
147
+ return acquireTrajectoryEvidenceFromDb(stateDir, sessionId, tmpDir);
148
+ }
149
+
150
+ function entriesOf(sessionId: string) {
151
+ const result = acquire(sessionId);
152
+ expect(result.status).toBe('available');
153
+ return result.status === 'available' ? result.entries : [];
154
+ }
155
+
156
+ function insertSession(db: Database.Database, sessionId: string): void {
157
+ db.prepare('INSERT INTO sessions (session_id, started_at, updated_at) VALUES (?, ?, ?)')
158
+ .run(sessionId, '2026-01-01T09:00:00Z', '2026-01-01T09:00:00Z');
159
+ }
160
+
161
+ it('returns available entries (≤ MAX_EVIDENCE_ENTRIES) from assistant turns', () => {
146
162
  const db = createTrajectoryDb();
163
+ insertSession(db, '123');
147
164
  insertAssistantTurn(db, '123', 'First assistant response about backups', '2026-01-01T10:00:00Z');
148
165
  insertAssistantTurn(db, '123', 'Second response about validation', '2026-01-01T10:01:00Z');
149
166
  insertAssistantTurn(db, '123', 'Third response about error handling', '2026-01-01T10:02:00Z');
150
167
  db.close();
151
168
 
152
- const evidence = buildTrajectoryEvidenceFromDb(stateDir, '123', tmpDir);
169
+ const entries = entriesOf('123');
153
170
 
154
- expect(evidence.length).toBeGreaterThan(0);
155
- expect(evidence.length).toBeLessThanOrEqual(MAX_EVIDENCE_ENTRIES);
156
- for (const entry of evidence) {
171
+ expect(entries.length).toBeGreaterThan(0);
172
+ expect(entries.length).toBeLessThanOrEqual(MAX_EVIDENCE_ENTRIES);
173
+ for (const entry of entries) {
157
174
  expect(entry.sourceRef).toBeTruthy();
158
175
  expect(entry.note).toBeTruthy();
159
- expect(typeof entry.sourceRef).toBe('string');
160
- expect(typeof entry.note).toBe('string');
161
176
  }
162
177
  });
163
178
 
164
- // 用例 B: sessionId is undefined or trajectory.db doesn't exist
165
- // returns placeholder entry, does not throw, does not return empty array
166
- it('B: returns placeholder when sessionId is undefined', () => {
167
- const evidence = buildTrajectoryEvidenceFromDb(stateDir, undefined, tmpDir);
179
+ it('classifies an undefined session as session_not_found (no placeholder entries)', () => {
180
+ const db = createTrajectoryDb();
181
+ db.close();
182
+ const result = acquire(undefined);
183
+ expect(result.status).toBe('unavailable');
184
+ if (result.status !== 'unavailable') return;
185
+ expect(result.reasonCode).toBe('session_not_found');
186
+ });
168
187
 
169
- expect(evidence.length).toBeGreaterThan(0);
170
- expect(evidence[0].sourceRef).toBe('owner_reported:cli');
171
- expect(evidence[0].note).toBeTruthy();
188
+ it('surfaces user correction turns with correctionDetected=true', () => {
189
+ const db = createTrajectoryDb();
190
+ insertSession(db, '123');
191
+ insertUserTurn(db, '123', 'Please fix the backup logic', true, '2026-01-01T09:59:00Z');
192
+ insertAssistantTurn(db, '123', 'I will fix the backup logic', '2026-01-01T10:00:00Z');
193
+ db.close();
194
+
195
+ const entries = entriesOf('123');
196
+
197
+ const ownerEntry = entries.find(e => e.sourceRef.startsWith('owner_message:'));
198
+ expect(ownerEntry).toBeDefined();
199
+ expect(ownerEntry!.note).toContain('fix the backup logic');
172
200
  });
173
201
 
174
- it('B2: returns placeholder when trajectory.db does not exist', () => {
175
- const evidence = buildTrajectoryEvidenceFromDb(stateDir, 'some-session', tmpDir);
202
+ it('extracts failed tool_calls as evidence entries (PRI-358)', () => {
203
+ const db = createTrajectoryDb();
204
+ insertSession(db, '123');
205
+ insertToolCall(db, '123', 'bash', 'failure', 'non_zero_exit', 1, '2026-01-01T10:00:00Z');
206
+ insertToolCall(db, '123', 'write_file', 'success', null, 0, '2026-01-01T10:01:00Z');
207
+ insertToolCall(db, '123', 'bash', 'failure', 'timeout', 124, '2026-01-01T10:02:00Z');
208
+ db.close();
209
+
210
+ const entries = entriesOf('123');
176
211
 
177
- expect(evidence.length).toBeGreaterThan(0);
178
- expect(evidence[0].sourceRef).toBe('owner_reported:cli');
212
+ const failureEntries = entries.filter(e => e.sourceRef.startsWith('tool_call_failure:'));
213
+ expect(failureEntries.length).toBe(2);
214
+ expect(failureEntries[0].note).toContain('bash');
215
+ expect(failureEntries[0].note).toContain('non_zero_exit');
216
+ expect(failureEntries[1].note).toContain('timeout');
179
217
  });
180
218
 
181
- it('B3: returns placeholder when sessionId is "cli"', () => {
182
- const evidence = buildTrajectoryEvidenceFromDb(stateDir, 'cli', tmpDir);
219
+ it('limits failed tool_calls to 3 entries', () => {
220
+ const db = createTrajectoryDb();
221
+ insertSession(db, '123');
222
+ insertToolCall(db, '123', 'bash', 'failure', 'err1', 1, '2026-01-01T10:00:00Z');
223
+ insertToolCall(db, '123', 'bash', 'failure', 'err2', 2, '2026-01-01T10:01:00Z');
224
+ insertToolCall(db, '123', 'bash', 'failure', 'err3', 3, '2026-01-01T10:02:00Z');
225
+ insertToolCall(db, '123', 'bash', 'failure', 'err4', 4, '2026-01-01T10:03:00Z');
226
+ db.close();
227
+
228
+ const entries = entriesOf('123');
183
229
 
184
- expect(evidence.length).toBeGreaterThan(0);
185
- expect(evidence[0].sourceRef).toBe('owner_reported:cli');
230
+ const failureEntries = entries.filter(e => e.sourceRef.startsWith('tool_call_failure:'));
231
+ expect(failureEntries.length).toBe(3);
186
232
  });
187
233
 
188
- // Additional: user correction turns are surfaced
189
- it('surfaces user correction turns with correctionDetected=true', () => {
234
+ it('handles a missing tool_calls table gracefully (classified unavailable, no throw)', () => {
235
+ const dbPath = path.join(stateDir, 'trajectory.db');
236
+ const db = new Database(dbPath);
237
+ db.exec("CREATE TABLE IF NOT EXISTS sessions (session_id TEXT PRIMARY KEY, started_at TEXT, updated_at TEXT)");
238
+ db.exec("CREATE TABLE IF NOT EXISTS assistant_turns (id INTEGER PRIMARY KEY AUTOINCREMENT, session_id TEXT NOT NULL, sanitized_text TEXT, created_at TEXT NOT NULL)");
239
+ db.exec("CREATE TABLE IF NOT EXISTS user_turns (id INTEGER PRIMARY KEY AUTOINCREMENT, session_id TEXT NOT NULL, turn_index INTEGER NOT NULL DEFAULT 0, raw_excerpt TEXT, correction_detected INTEGER NOT NULL DEFAULT 0, created_at TEXT NOT NULL)");
240
+ db.prepare('INSERT INTO sessions (session_id, started_at, updated_at) VALUES (?, ?, ?)').run('123', '2026-01-01T09:00:00Z', '2026-01-01T09:00:00Z');
241
+ db.close();
242
+
243
+ const result = acquire('123');
244
+ expect(result.status).toBe('unavailable');
245
+ if (result.status !== 'unavailable') return;
246
+ expect(['empty_trajectory', 'evidence_read_failed']).toContain(result.reasonCode);
247
+ });
248
+
249
+ it('includes resultPreview in tool failure evidence note', () => {
190
250
  const db = createTrajectoryDb();
191
- insertUserTurn(db, '123', 'Please fix the backup logic', true, '2026-01-01T09:59:00Z');
192
- insertAssistantTurn(db, '123', 'I will fix the backup logic', '2026-01-01T10:00:00Z');
251
+ insertSession(db, '123');
252
+ insertToolCall(db, '123', 'bash', 'failure', 'ENOENT', 1, '2026-01-01T10:00:00Z', 'Error: no such file or directory');
193
253
  db.close();
194
254
 
195
- const evidence = buildTrajectoryEvidenceFromDb(stateDir, '123', tmpDir);
255
+ const entries = entriesOf('123');
196
256
 
197
- expect(evidence.length).toBeGreaterThan(0);
198
- const ownerEntry = evidence.find(e => e.sourceRef.startsWith('owner_message:'));
199
- expect(ownerEntry).toBeDefined();
200
- expect(ownerEntry!.note).toContain('fix the backup logic');
257
+ const failureEntry = entries.find(e => e.sourceRef.startsWith('tool_call_failure:'));
258
+ expect(failureEntry).toBeDefined();
259
+ expect(failureEntry!.note).toContain('ENOENT');
260
+ expect(failureEntry!.note).toContain('Error: no such file or directory');
261
+ });
262
+
263
+ it('includes truncation warning when stop_reason is length', () => {
264
+ const db = createTrajectoryDb();
265
+ insertSession(db, '123');
266
+ db.prepare("INSERT INTO assistant_turns (session_id, sanitized_text, stop_reason, created_at) VALUES (?, ?, ?, ?)")
267
+ .run('123', 'Partial output truncated...', 'length', '2026-01-01T10:00:00Z');
268
+ db.close();
269
+
270
+ const entries = entriesOf('123');
271
+
272
+ const agentEntry = entries.find(e => e.sourceRef.startsWith('agent_turn:'));
273
+ expect(agentEntry).toBeDefined();
274
+ expect(agentEntry!.note).toContain('Partial output truncated...');
275
+ expect(agentEntry!.note).toContain('[TRUNCATED: output cut off by length limit]');
201
276
  });
202
277
 
203
- // Additional: empty trajectory DB (tables exist but no rows) meaningful placeholder
204
- it('returns trajectory:empty placeholder when DB has no turns for session', () => {
278
+ it('does not include truncation warning when stop_reason is end_turn', () => {
205
279
  const db = createTrajectoryDb();
280
+ insertSession(db, '123');
281
+ db.prepare("INSERT INTO assistant_turns (session_id, sanitized_text, stop_reason, created_at) VALUES (?, ?, ?, ?)")
282
+ .run('123', 'Complete output', 'end_turn', '2026-01-01T10:00:00Z');
206
283
  db.close();
207
284
 
208
- const evidence = buildTrajectoryEvidenceFromDb(stateDir, 'nonexistent-session', tmpDir);
285
+ const entries = entriesOf('123');
209
286
 
210
- expect(evidence.length).toBeGreaterThan(0);
211
- expect(evidence[0].sourceRef).toBe('trajectory:empty');
287
+ const agentEntry = entries.find(e => e.sourceRef.startsWith('agent_turn:'));
288
+ expect(agentEntry).toBeDefined();
289
+ expect(agentEntry!.note).toBe('Complete output');
290
+ expect(agentEntry!.note).not.toContain('[TRUNCATED');
212
291
  });
292
+ });
293
+
294
+ // ── PRI-642 Scope A — typed acquisition from trajectory.db (SPEC §7.3) ───────
213
295
 
214
- // ── PRI-358: Failed tool_calls evidence ────────────────────────────────────
215
-
216
- describe('PRI-358: failed tool_calls evidence', () => {
217
- it('extracts failed tool_calls as evidence entries', () => {
218
- const db = createTrajectoryDb();
219
- insertToolCall(db, '123', 'bash', 'failure', 'non_zero_exit', 1, '2026-01-01T10:00:00Z');
220
- insertToolCall(db, '123', 'write_file', 'success', null, 0, '2026-01-01T10:01:00Z');
221
- insertToolCall(db, '123', 'bash', 'failure', 'timeout', 124, '2026-01-01T10:02:00Z');
222
- db.close();
223
-
224
- const evidence = buildTrajectoryEvidenceFromDb(stateDir, '123', tmpDir);
225
-
226
- const failureEntries = evidence.filter(e => e.sourceRef.startsWith('tool_call_failure:'));
227
- expect(failureEntries.length).toBe(2);
228
- expect(failureEntries[0].note).toContain('bash');
229
- expect(failureEntries[0].note).toContain('non_zero_exit');
230
- expect(failureEntries[1].note).toContain('timeout');
231
- });
232
-
233
- it('does not add tool_call_failure entries when no failures exist', () => {
234
- const db = createTrajectoryDb();
235
- insertToolCall(db, '123', 'bash', 'success', null, 0, '2026-01-01T10:00:00Z');
236
- db.close();
237
-
238
- const evidence = buildTrajectoryEvidenceFromDb(stateDir, '123', tmpDir);
239
-
240
- const failureEntries = evidence.filter(e => e.sourceRef.startsWith('tool_call_failure:'));
241
- expect(failureEntries.length).toBe(0);
242
- });
243
-
244
- it('limits failed tool_calls to 3 entries', () => {
245
- const db = createTrajectoryDb();
246
- insertToolCall(db, '123', 'bash', 'failure', 'err1', 1, '2026-01-01T10:00:00Z');
247
- insertToolCall(db, '123', 'bash', 'failure', 'err2', 2, '2026-01-01T10:01:00Z');
248
- insertToolCall(db, '123', 'bash', 'failure', 'err3', 3, '2026-01-01T10:02:00Z');
249
- insertToolCall(db, '123', 'bash', 'failure', 'err4', 4, '2026-01-01T10:03:00Z');
250
- db.close();
251
-
252
- const evidence = buildTrajectoryEvidenceFromDb(stateDir, '123', tmpDir);
253
-
254
- const failureEntries = evidence.filter(e => e.sourceRef.startsWith('tool_call_failure:'));
255
- expect(failureEntries.length).toBe(3);
256
- });
257
-
258
- it('handles missing tool_calls table gracefully', () => {
259
- // Create DB without tool_calls table
260
- const dbPath = path.join(stateDir, 'trajectory.db');
261
- const db = new Database(dbPath);
262
- db.exec(`
263
- CREATE TABLE IF NOT EXISTS sessions (session_id TEXT PRIMARY KEY, started_at TEXT, updated_at TEXT)
264
- `);
265
- db.exec(`
266
- CREATE TABLE IF NOT EXISTS assistant_turns (
267
- id INTEGER PRIMARY KEY AUTOINCREMENT, session_id TEXT NOT NULL,
268
- sanitized_text TEXT, created_at TEXT NOT NULL
269
- )
270
- `);
271
- db.exec(`
272
- CREATE TABLE IF NOT EXISTS user_turns (
273
- id INTEGER PRIMARY KEY AUTOINCREMENT, session_id TEXT NOT NULL,
274
- turn_index INTEGER NOT NULL DEFAULT 0, raw_excerpt TEXT,
275
- correction_detected INTEGER NOT NULL DEFAULT 0, created_at TEXT NOT NULL
276
- )
277
- `);
278
- db.close();
279
-
280
- const evidence = buildTrajectoryEvidenceFromDb(stateDir, '123', tmpDir);
281
-
282
- // Should not throw, should have some evidence (trajectory:empty or unavailable)
283
- expect(evidence.length).toBeGreaterThan(0);
284
- // Should NOT have tool_call_failure:unavailable since we have no other evidence
285
- // and the table simply doesn't exist (not an error condition worth reporting)
286
- });
296
+ describe('acquireTrajectoryEvidenceFromDb typed CLI acquisition (PRI-642 Scope A)', () => {
297
+ beforeEach(() => {
298
+ createStateDir();
287
299
  });
288
300
 
289
- // ── Trajectory v2 enhancement: resultPreview + stopReason ─────────────────
290
-
291
- describe('trajectory v2 enhancement: resultPreview and stopReason', () => {
292
- it('includes resultPreview in tool failure evidence note', () => {
293
- const db = createTrajectoryDb();
294
- insertToolCall(db, '123', 'bash', 'failure', 'ENOENT', 1, '2026-01-01T10:00:00Z', 'Error: no such file or directory');
295
- db.close();
296
-
297
- const evidence = buildTrajectoryEvidenceFromDb(stateDir, '123', tmpDir);
298
-
299
- const failureEntry = evidence.find(e => e.sourceRef.startsWith('tool_call_failure:'));
300
- expect(failureEntry).toBeDefined();
301
- expect(failureEntry!.note).toContain('ENOENT');
302
- expect(failureEntry!.note).toContain('Error: no such file or directory');
303
- });
304
-
305
- it('includes truncation warning when stop_reason is length', () => {
306
- const db = createTrajectoryDb();
307
- db.prepare(`
308
- INSERT INTO assistant_turns (session_id, sanitized_text, stop_reason, created_at)
309
- VALUES (?, ?, ?, ?)
310
- `).run('123', 'Partial output truncated...', 'length', '2026-01-01T10:00:00Z');
311
- db.close();
312
-
313
- const evidence = buildTrajectoryEvidenceFromDb(stateDir, '123', tmpDir);
314
-
315
- const agentEntry = evidence.find(e => e.sourceRef.startsWith('agent_turn:'));
316
- expect(agentEntry).toBeDefined();
317
- expect(agentEntry!.note).toContain('Partial output truncated...');
318
- expect(agentEntry!.note).toContain('[TRUNCATED: output cut off by length limit]');
319
- });
320
-
321
- it('does not include truncation warning when stop_reason is end_turn', () => {
322
- const db = createTrajectoryDb();
323
- db.prepare(`
324
- INSERT INTO assistant_turns (session_id, sanitized_text, stop_reason, created_at)
325
- VALUES (?, ?, ?, ?)
326
- `).run('123', 'Complete output', 'end_turn', '2026-01-01T10:00:00Z');
327
- db.close();
328
-
329
- const evidence = buildTrajectoryEvidenceFromDb(stateDir, '123', tmpDir);
330
-
331
- const agentEntry = evidence.find(e => e.sourceRef.startsWith('agent_turn:'));
332
- expect(agentEntry).toBeDefined();
333
- expect(agentEntry!.note).toBe('Complete output');
334
- expect(agentEntry!.note).not.toContain('[TRUNCATED');
335
- });
301
+ afterEach(() => {
302
+ try {
303
+ fs.rmSync(tmpDir, { recursive: true, force: true });
304
+ } catch {
305
+ // ignore cleanup errors
306
+ }
307
+ });
308
+
309
+ it('returns available with entries for a session that has turns', async () => {
310
+ const { acquireTrajectoryEvidenceFromDb } = await import('../../src/commands/build-trajectory-evidence.js');
311
+ const db = createTrajectoryDb();
312
+ // Turn writers upsert the sessions row — mirror the real DB shape.
313
+ db.prepare('INSERT INTO sessions (session_id, started_at, updated_at) VALUES (?, ?, ?)')
314
+ .run('real-session', '2026-01-01T09:00:00Z', '2026-01-01T09:00:00Z');
315
+ insertUserTurn(db, 'real-session', 'Owner correction', true, '2026-01-01T09:59:00Z');
316
+ insertAssistantTurn(db, 'real-session', 'assistant text', '2026-01-01T10:00:00Z');
317
+ db.close();
318
+
319
+ const result = acquireTrajectoryEvidenceFromDb(stateDir, 'real-session', tmpDir);
320
+
321
+ expect(result.status).toBe('available');
322
+ if (result.status !== 'available') return;
323
+ expect(result.entries.length).toBeGreaterThan(0);
324
+ expect(result.entries.some(e => e.sourceRef.startsWith('owner_message:'))).toBe(true);
325
+ expect(result.entries.some(e => e.sourceRef.startsWith('agent_turn:'))).toBe(true);
326
+ });
327
+
328
+ it('returns unavailable/session_not_found for a session absent from the sessions table (SPEC 12.1.4)', async () => {
329
+ const { acquireTrajectoryEvidenceFromDb } = await import('../../src/commands/build-trajectory-evidence.js');
330
+ const db = createTrajectoryDb();
331
+ insertAssistantTurn(db, 'other-session', 'unrelated', '2026-01-01T10:00:00Z');
332
+ db.close();
333
+
334
+ const result = acquireTrajectoryEvidenceFromDb(stateDir, 'no-such-session', tmpDir);
335
+
336
+ expect(result.status).toBe('unavailable');
337
+ if (result.status !== 'unavailable') return;
338
+ expect(result.reasonCode).toBe('session_not_found');
339
+ });
340
+
341
+ it('returns unavailable/session_not_found for the "cli" and "unknown" sentinels', async () => {
342
+ const { acquireTrajectoryEvidenceFromDb } = await import('../../src/commands/build-trajectory-evidence.js');
343
+ const db = createTrajectoryDb();
344
+ db.close();
345
+
346
+ const cliResult = acquireTrajectoryEvidenceFromDb(stateDir, 'cli', tmpDir);
347
+ const unknownResult = acquireTrajectoryEvidenceFromDb(stateDir, 'unknown', tmpDir);
348
+
349
+ expect(cliResult.status).toBe('unavailable');
350
+ expect(unknownResult.status).toBe('unavailable');
351
+ if (cliResult.status !== 'unavailable' || unknownResult.status !== 'unavailable') return;
352
+ expect(cliResult.reasonCode).toBe('session_not_found');
353
+ expect(unknownResult.reasonCode).toBe('session_not_found');
354
+ });
355
+
356
+ it('returns unavailable/trajectory_unavailable when trajectory.db is missing', async () => {
357
+ const { acquireTrajectoryEvidenceFromDb } = await import('../../src/commands/build-trajectory-evidence.js');
358
+
359
+ const result = acquireTrajectoryEvidenceFromDb(stateDir, 'some-session', tmpDir);
360
+
361
+ expect(result.status).toBe('unavailable');
362
+ if (result.status !== 'unavailable') return;
363
+ expect(result.reasonCode).toBe('trajectory_unavailable');
364
+ });
365
+
366
+ it('returns unavailable/empty_trajectory when the session exists but has no turns or tool calls', async () => {
367
+ const { acquireTrajectoryEvidenceFromDb } = await import('../../src/commands/build-trajectory-evidence.js');
368
+ const db = createTrajectoryDb();
369
+ db.prepare(`
370
+ INSERT INTO sessions (session_id, started_at, updated_at)
371
+ VALUES (?, ?, ?)
372
+ `).run('quiet-session', '2026-01-01T09:00:00Z', '2026-01-01T09:00:00Z');
373
+ db.close();
374
+
375
+ const result = acquireTrajectoryEvidenceFromDb(stateDir, 'quiet-session', tmpDir);
376
+
377
+ expect(result.status).toBe('unavailable');
378
+ if (result.status !== 'unavailable') return;
379
+ expect(result.reasonCode).toBe('empty_trajectory');
380
+ });
381
+
382
+ it('returns a different reasonCode for unreadable DB vs empty trajectory (exec-prompt item 5)', async () => {
383
+ const { acquireTrajectoryEvidenceFromDb } = await import('../../src/commands/build-trajectory-evidence.js');
384
+ const db = createTrajectoryDb();
385
+ db.close();
386
+ // Corrupt the DB file after close so the read-only open fails.
387
+ const dbPath = path.join(stateDir, 'trajectory.db');
388
+ fs.writeFileSync(dbPath, Buffer.from('this is not a sqlite database at all'));
389
+
390
+ const unreadable = acquireTrajectoryEvidenceFromDb(stateDir, 'some-session', tmpDir);
391
+
392
+ const stateDir2 = createStateDir();
393
+ const db2 = createTrajectoryDb();
394
+ db2.close();
395
+
396
+ expect(unreadable.status).toBe('unavailable');
397
+ if (unreadable.status !== 'unavailable') return;
398
+ // Unreadable DB must not share a reasonCode with a real-but-empty session.
399
+ expect(unreadable.reasonCode).not.toBe('empty_trajectory');
400
+ expect(unreadable.reasonCode).toBe('evidence_read_failed');
336
401
  });
337
402
  });