Shumiland/tests/helpers/session.test.js
Vadym Samoilenko 9b41fa447a
Some checks are pending
CI / Type Check (push) Waiting to run
CI / Lint (push) Waiting to run
CI / Unit Tests (push) Waiting to run
Deploy / Build & Push Image (push) Waiting to run
Deploy / Deploy to VPS (push) Blocked by required conditions
feat: complete backend B1-B7 — Payload CMS, ezy payments, leads, deploy
- 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>
2026-05-09 19:14:54 +01:00

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)
})
})
})