import { describe, it, afterEach } from 'node:test'; import assert from 'node:assert/strict'; import { join } from 'path'; import { existsSync } from 'fs'; import { runHook, setupTestDir, cleanupTestDir, createStateFile, readJsonl } from './test-helper.mjs'; let dir; afterEach(() => { if (dir) cleanupTestDir(dir); }); describe('session-end', () => { it('finalizes session record and deletes state file', () => { dir = setupTestDir(); const nowEpoch = Math.floor(Date.now() / 1000); createStateFile(dir, 's1', { start_epoch: nowEpoch - 300, start_iso: '2026-01-01T10:00:00Z', tool_count: 5, edit_count: 2, dep_flags: 1, esc_flags: 0, fatigue_flags: 0, val_flags: 1, last_event_epoch: 0, burst_count: 0, last_warning_epoch: 0, }); runHook('session-end.mjs', { session_id: 's1', cwd: '/tmp' }, dir); const records = readJsonl(join(dir, 'sessions.jsonl')); const end = records.find(r => r.end); assert.ok(end); assert.equal(end.session_id, 's1'); assert.equal(end.tool_count, 5); assert.equal(end.edit_count, 2); assert.ok(!existsSync(join(dir, 'state', 's1.json'))); }); it('computes duration correctly', () => { dir = setupTestDir(); const nowEpoch = Math.floor(Date.now() / 1000); createStateFile(dir, 's2', { start_epoch: nowEpoch - 3600, start_iso: '2026-01-01T10:00:00Z', tool_count: 10, edit_count: 3, dep_flags: 0, esc_flags: 0, fatigue_flags: 0, val_flags: 0, last_event_epoch: 0, burst_count: 0, last_warning_epoch: 0, }); runHook('session-end.mjs', { session_id: 's2', cwd: '/tmp' }, dir); const records = readJsonl(join(dir, 'sessions.jsonl')); const end = records.find(r => r.end); assert.ok(end.duration_min >= 59 && end.duration_min <= 61); }); it('preserves flags in final record', () => { dir = setupTestDir(); createStateFile(dir, 's3', { start_epoch: Math.floor(Date.now() / 1000) - 60, start_iso: '2026-01-01T10:00:00Z', tool_count: 1, edit_count: 0, dep_flags: 3, esc_flags: 1, fatigue_flags: 2, val_flags: 0, last_event_epoch: 0, burst_count: 0, last_warning_epoch: 0, }); runHook('session-end.mjs', { session_id: 's3', cwd: '/tmp' }, dir); const records = readJsonl(join(dir, 'sessions.jsonl')); const end = records.find(r => r.end); assert.deepEqual(end.flags, { dependency: 3, escalation: 1, fatigue: 2, validation: 0, pushback: 0 }); }); it('handles missing state file gracefully', () => { dir = setupTestDir(); runHook('session-end.mjs', { session_id: 'missing', cwd: '/tmp' }, dir); const records = readJsonl(join(dir, 'sessions.jsonl')); assert.equal(records.length, 1); assert.equal(records[0].note, 'no_state_file'); }); it('persists pushback_count and coerces v1.1.0 string domain to array', () => { dir = setupTestDir(); createStateFile(dir, 's4', { start_epoch: Math.floor(Date.now() / 1000) - 120, start_iso: '2026-01-01T10:00:00Z', tool_count: 2, edit_count: 1, dep_flags: 0, esc_flags: 0, fatigue_flags: 0, val_flags: 0, pushback_count: 3, domain_context: 'relationship', // v1.1.0 string shape last_event_epoch: 0, burst_count: 0, last_warning_epoch: 0, }); runHook('session-end.mjs', { session_id: 's4', cwd: '/tmp' }, dir); const records = readJsonl(join(dir, 'sessions.jsonl')); const end = records.find(r => r.end); assert.ok(end); assert.equal(end.flags.pushback, 3); // v1.2: end record always carries an array, even when state had a string. assert.deepEqual(end.domain_context, ['relationship']); }); it('writes v1.2 multi-domain array unchanged when state already has array', () => { dir = setupTestDir(); createStateFile(dir, 's4b', { start_epoch: Math.floor(Date.now() / 1000) - 120, start_iso: '2026-01-01T10:00:00Z', tool_count: 2, edit_count: 1, dep_flags: 0, esc_flags: 0, fatigue_flags: 0, val_flags: 0, pushback_count: 1, domain_context: ['relationship', 'health'], last_event_epoch: 0, burst_count: 0, last_warning_epoch: 0, }); runHook('session-end.mjs', { session_id: 's4b', cwd: '/tmp' }, dir); const records = readJsonl(join(dir, 'sessions.jsonl')); const end = records.find(r => r.end); assert.ok(end); assert.deepEqual(end.domain_context, ['relationship', 'health']); }); it('backward-compat: state without pushback_count yields flags.pushback === 0 (not NaN/undefined)', () => { dir = setupTestDir(); createStateFile(dir, 's5', { start_epoch: Math.floor(Date.now() / 1000) - 60, start_iso: '2026-01-01T10:00:00Z', tool_count: 1, edit_count: 0, dep_flags: 0, esc_flags: 0, fatigue_flags: 0, val_flags: 0, // pushback_count and domain_context intentionally absent (v1.0.0 state shape) last_event_epoch: 0, burst_count: 0, last_warning_epoch: 0, }); runHook('session-end.mjs', { session_id: 's5', cwd: '/tmp' }, dir); const records = readJsonl(join(dir, 'sessions.jsonl')); const end = records.find(r => r.end); assert.ok(end); assert.equal(end.flags.pushback, 0); assert.notEqual(end.flags.pushback, undefined); assert.ok(!Number.isNaN(end.flags.pushback)); // v1.2: empty domain becomes [] (not null) — always an array on disk. assert.deepEqual(end.domain_context, []); }); });