/** * Tests for .claude/helpers/session.js * * Run: node --test tests/helpers/session.test.js * * Coverage gaps addressed: * - start: session shape, unique IDs, directory creation * - restore: no file → null, adds restoredAt * - end: no file → null, archives file, removes current, duration calc * - status: no file → null, calculates live duration * - update: no file → null, sets context[key], adds updatedAt * - get: no file → null, with/without key * - metric: increments known metric, ignores unknown metric, no session → null */ 'use strict' const { describe, it, before, after, beforeEach } = require('node:test') const assert = require('node:assert/strict') const fs = require('node:fs') const path = require('node:path') const os = require('node:os') function loadModule(cwd) { const modPath = require.resolve('../../.claude/helpers/session.js') delete require.cache[modPath] const origCwd = process.cwd process.cwd = () => cwd try { return require('../../.claude/helpers/session.js') } finally { process.cwd = origCwd delete require.cache[modPath] } } describe('session.js', () => { let tmpDir let commands let sessionFile before(() => { tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'sess-test-')) sessionFile = path.join(tmpDir, '.claude-flow', 'sessions', 'current.json') }) after(() => { fs.rmSync(tmpDir, { recursive: true, force: true }) }) beforeEach(() => { const dir = path.dirname(sessionFile) if (fs.existsSync(dir)) fs.rmSync(dir, { recursive: true }) commands = loadModule(tmpDir) }) // ── start ────────────────────────────────────────────────────────────────── describe('start()', () => { it('creates the sessions directory if absent', () => { commands.start() assert.ok(fs.existsSync(path.dirname(sessionFile))) }) it('writes a current.json with the expected shape', () => { const session = commands.start() assert.ok(session.id.startsWith('session-')) assert.ok(session.startedAt) assert.deepEqual(session.metrics, { edits: 0, commands: 0, tasks: 0, errors: 0 }) assert.deepEqual(session.context, {}) }) it('generates a unique ID each call (timestamp-based)', async () => { const s1 = commands.start() await new Promise((r) => setTimeout(r, 5)) const s2 = commands.start() assert.notEqual(s1.id, s2.id) }) it('persists the session to disk', () => { commands.start() const raw = JSON.parse(fs.readFileSync(sessionFile, 'utf-8')) assert.ok(raw.id) }) }) // ── restore ──────────────────────────────────────────────────────────────── describe('restore()', () => { it('returns null when no current.json exists', () => { const result = commands.restore() assert.equal(result, null) }) it('returns the session and adds restoredAt', () => { commands.start() const session = commands.restore() assert.ok(session) assert.ok(session.restoredAt, 'restoredAt should be set') }) it('persists restoredAt back to disk', () => { commands.start() commands.restore() const raw = JSON.parse(fs.readFileSync(sessionFile, 'utf-8')) assert.ok(raw.restoredAt) }) }) // ── end ──────────────────────────────────────────────────────────────────── describe('end()', () => { it('returns null when no active session exists', () => { const result = commands.end() assert.equal(result, null) }) it('removes current.json after ending', () => { commands.start() commands.end() assert.ok(!fs.existsSync(sessionFile)) }) it('archives session to .json in the sessions directory', () => { const session = commands.start() commands.end() const archivePath = path.join(path.dirname(sessionFile), `${session.id}.json`) assert.ok(fs.existsSync(archivePath)) }) it('sets endedAt and a non-negative duration', () => { commands.start() const ended = commands.end() assert.ok(ended.endedAt) assert.ok(ended.duration >= 0) }) it('duration is roughly correct (within 500ms of elapsed time)', async () => { const start = Date.now() commands.start() await new Promise((r) => setTimeout(r, 50)) const ended = commands.end() const elapsed = Date.now() - start assert.ok(ended.duration <= elapsed + 50) }) }) // ── status ───────────────────────────────────────────────────────────────── describe('status()', () => { it('returns null when no active session', () => { assert.equal(commands.status(), null) }) it('returns session data without modifying the file', () => { commands.start() const before = fs.readFileSync(sessionFile, 'utf-8') commands.status() const after = fs.readFileSync(sessionFile, 'utf-8') assert.equal(before, after) }) }) // ── update ───────────────────────────────────────────────────────────────── describe('update()', () => { it('returns null when no active session', () => { assert.equal(commands.update('k', 'v'), null) }) it('sets a key in session.context', () => { commands.start() commands.update('myKey', 'myVal') const raw = JSON.parse(fs.readFileSync(sessionFile, 'utf-8')) assert.equal(raw.context.myKey, 'myVal') }) it('sets updatedAt timestamp', () => { commands.start() const before = Date.now() commands.update('x', 'y') const raw = JSON.parse(fs.readFileSync(sessionFile, 'utf-8')) const ts = new Date(raw.updatedAt).getTime() assert.ok(ts >= before) }) }) // ── get ──────────────────────────────────────────────────────────────────── describe('get()', () => { it('returns null when no session file exists', () => { assert.equal(commands.get('anything'), null) }) it('returns the full context object when no key provided', () => { commands.start() commands.update('a', 1) const ctx = commands.get(undefined) assert.deepEqual(ctx, { a: 1 }) }) it('returns a specific context value by key', () => { commands.start() commands.update('foo', 'bar') assert.equal(commands.get('foo'), 'bar') }) it('returns undefined for a key not in context', () => { commands.start() assert.equal(commands.get('ghost'), undefined) }) }) // ── metric ───────────────────────────────────────────────────────────────── describe('metric()', () => { it('returns null when no active session', () => { assert.equal(commands.metric('edits'), null) }) it('increments a known metric', () => { commands.start() commands.metric('edits') const raw = JSON.parse(fs.readFileSync(sessionFile, 'utf-8')) assert.equal(raw.metrics.edits, 1) }) it('increments the same metric multiple times correctly', () => { commands.start() commands.metric('tasks') commands.metric('tasks') commands.metric('tasks') const raw = JSON.parse(fs.readFileSync(sessionFile, 'utf-8')) assert.equal(raw.metrics.tasks, 3) }) it('does not increment an unknown/undefined metric key', () => { commands.start() commands.metric('unknown_metric') const raw = JSON.parse(fs.readFileSync(sessionFile, 'utf-8')) // unknown_metric should not exist (condition: metrics[name] !== undefined) assert.equal(raw.metrics.unknown_metric, undefined) }) it('does not affect other metrics when incrementing one', () => { commands.start() commands.metric('errors') const raw = JSON.parse(fs.readFileSync(sessionFile, 'utf-8')) assert.equal(raw.metrics.edits, 0) assert.equal(raw.metrics.commands, 0) assert.equal(raw.metrics.tasks, 0) assert.equal(raw.metrics.errors, 1) }) }) })