- B1: Next.js 15 + Payload CMS 3.0 + Postgres 16, ESLint, Prettier, Husky, Vitest - B2: 9 collections, 6 globals, 12 Page Builder blocks, access control, slugify/revalidate hooks - B3: ezy.com.ua payments, Binotel HMAC webhook, leads API, Telegram bot, Resend email, rate limiting - B4: Tariffs collection with ezy API sync (cron + manual), dynamic pricing source-of-truth - B5: 13 test files covering unit libs and all API routes - B6: Dockerfile multi-stage, docker-compose.prod.yml, nginx.conf SSL, GitHub Actions CI/CD, health endpoint - B7: docs/admin-guide-ua.md (marketer guide), docs/deploy.md (VPS instructions), README quickstart Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
255 lines
8.7 KiB
JavaScript
255 lines
8.7 KiB
JavaScript
/**
|
|
* 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 <id>.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)
|
|
})
|
|
})
|
|
})
|