Shumiland/tests/helpers/statusline.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

279 lines
9.9 KiB
JavaScript

/**
* Tests for .claude/helpers/statusline.js
*
* statusline.js has no exports (only a main block), so all tests run it as a
* subprocess and inspect stdout. The --json flag makes output machine-readable.
*
* Run: node --test tests/helpers/statusline.test.js
*
* Coverage gaps addressed:
* - generateJSON: all required top-level keys present, valid lastUpdated date
* - performance targets included in JSON output
* - v3Progress thresholds: pattern count drives domainsCompleted (0→5)
* - security status: PENDING (no files), IN_PROGRESS (1-2), CLEAN (3+)
* - generateStatusline (default): non-empty, contains branding + sections
* - --compact flag: outputs minified JSON on one line
* - getUserInfo: git missing → defaults (name, modelName)
* - getLearningStats: no db → patterns/sessions/trajectories all 0
* - Script exits 0 in all modes
*
* NOTE: Functions that are not exported (getUserInfo, getLearningStats,
* getV3Progress, getSecurityStatus, getSwarmStatus, progressBar) are verified
* indirectly via the JSON output structure.
*/
'use strict'
const { describe, it, before, after, beforeEach } = require('node:test')
const assert = require('node:assert/strict')
const { spawnSync } = require('node:child_process')
const fs = require('node:fs')
const path = require('node:path')
const os = require('node:os')
const SCRIPT = path.resolve(__dirname, '../../.claude/helpers/statusline.js')
function run(args = [], { cwd = null } = {}) {
return spawnSync(process.execPath, [SCRIPT, ...args], {
encoding: 'utf-8',
timeout: 10000,
cwd: cwd || process.cwd(),
})
}
// Strip ANSI escape codes
function stripAnsi(str) {
return str.replace(/\x1b\[[0-9;]*m/g, '')
}
describe('statusline.js', () => {
let tmpDir
before(() => {
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'sl-test-'))
})
after(() => {
fs.rmSync(tmpDir, { recursive: true, force: true })
})
beforeEach(() => {
for (const entry of fs.readdirSync(tmpDir)) {
fs.rmSync(path.join(tmpDir, entry), { recursive: true, force: true })
}
})
// ── Exit codes ──────────────────────────────────────────────────────────────
describe('exit codes', () => {
it('exits 0 in default (statusline) mode', () => {
const r = run([])
assert.equal(r.status, 0, `stderr: ${r.stderr}`)
})
it('exits 0 in --json mode', () => {
const r = run(['--json'])
assert.equal(r.status, 0, `stderr: ${r.stderr}`)
})
it('exits 0 in --compact mode', () => {
const r = run(['--compact'])
assert.equal(r.status, 0, `stderr: ${r.stderr}`)
})
})
// ── generateJSON (--json flag) ──────────────────────────────────────────────
describe('--json output', () => {
it('produces valid JSON', () => {
const r = run(['--json'])
assert.doesNotThrow(() => JSON.parse(r.stdout), `stdout: ${r.stdout.substring(0, 200)}`)
})
it('contains all required top-level keys', () => {
const r = run(['--json'])
const json = JSON.parse(r.stdout)
const required = [
'user',
'v3Progress',
'security',
'swarm',
'system',
'performance',
'lastUpdated',
]
for (const key of required) {
assert.ok(key in json, `Missing key: ${key}`)
}
})
it('lastUpdated is a valid ISO 8601 date', () => {
const r = run(['--json'])
const json = JSON.parse(r.stdout)
assert.ok(!isNaN(new Date(json.lastUpdated).getTime()), `Invalid date: ${json.lastUpdated}`)
})
it('performance object contains all three targets', () => {
const r = run(['--json'])
const json = JSON.parse(r.stdout)
assert.ok(json.performance.flashAttentionTarget)
assert.ok(json.performance.searchImprovement)
assert.ok(json.performance.memoryReduction)
})
it('v3Progress has domainsCompleted and dddProgress', () => {
const r = run(['--json'])
const json = JSON.parse(r.stdout)
assert.ok('domainsCompleted' in json.v3Progress)
assert.ok('dddProgress' in json.v3Progress)
assert.ok('totalDomains' in json.v3Progress)
})
it('security has status, cvesFixed, totalCves', () => {
const r = run(['--json'])
const json = JSON.parse(r.stdout)
assert.ok('status' in json.security)
assert.ok('cvesFixed' in json.security)
assert.ok('totalCves' in json.security)
})
it('user has name and modelName', () => {
const r = run(['--json'])
const json = JSON.parse(r.stdout)
assert.ok(typeof json.user.name === 'string')
assert.ok(typeof json.user.modelName === 'string')
})
})
// ── --compact flag ──────────────────────────────────────────────────────────
describe('--compact output', () => {
it('produces minified JSON on a single line', () => {
const r = run(['--compact'])
const lines = r.stdout.trim().split('\n')
assert.equal(lines.length, 1, 'compact output must be one line')
})
it('compact JSON is parseable', () => {
const r = run(['--compact'])
assert.doesNotThrow(() => JSON.parse(r.stdout))
})
})
// ── Default statusline output ───────────────────────────────────────────────
describe('default statusline output', () => {
it('produces non-empty output', () => {
const r = run([])
assert.ok(r.stdout.trim().length > 0, 'output should not be empty')
})
it('contains RuFlo branding', () => {
const r = run([])
const clean = stripAnsi(r.stdout)
assert.ok(clean.includes('RuFlo'), `stdout (stripped): ${clean.substring(0, 200)}`)
})
it('contains DDD section', () => {
const r = run([])
const clean = stripAnsi(r.stdout)
assert.ok(clean.includes('DDD'), `stdout: ${clean.substring(0, 300)}`)
})
it('contains Swarm section', () => {
const r = run([])
const clean = stripAnsi(r.stdout)
assert.ok(clean.includes('Swarm'), `stdout: ${clean.substring(0, 300)}`)
})
it('contains Security section', () => {
const r = run([])
const clean = stripAnsi(r.stdout)
assert.ok(
clean.includes('Security') || clean.includes('CVE'),
`stdout: ${clean.substring(0, 400)}`
)
})
})
// ── getSecurityStatus via --json (tested indirectly via CWD) ───────────────
describe('security status (via CWD with scan files)', () => {
it('reports PENDING when no scan dirs exist', () => {
const r = run(['--json'], { cwd: tmpDir })
const json = JSON.parse(r.stdout)
assert.equal(json.security.status, 'PENDING')
assert.equal(json.security.cvesFixed, 0)
})
it('reports IN_PROGRESS when 1 scan file exists', () => {
const scanDir = path.join(tmpDir, '.claude', 'security-scans')
fs.mkdirSync(scanDir, { recursive: true })
fs.writeFileSync(path.join(scanDir, 'scan-1.json'), '{}')
const r = run(['--json'], { cwd: tmpDir })
const json = JSON.parse(r.stdout)
assert.equal(json.security.status, 'IN_PROGRESS')
assert.equal(json.security.cvesFixed, 1)
})
it('reports CLEAN when 3+ scan files exist', () => {
const scanDir = path.join(tmpDir, '.claude', 'security-scans')
fs.mkdirSync(scanDir, { recursive: true })
for (let i = 0; i < 3; i++) {
fs.writeFileSync(path.join(scanDir, `scan-${i}.json`), '{}')
}
const r = run(['--json'], { cwd: tmpDir })
const json = JSON.parse(r.stdout)
assert.equal(json.security.status, 'CLEAN')
})
})
// ── getV3Progress thresholds (via --json + db size) ────────────────────────
describe('v3Progress domain thresholds (via CWD with db files)', () => {
// patterns = floor(sizeKB / 2); sizeKB = patternCount * 2
const cases = [
[0, 0], // 0 patterns → 0 domains
[10, 1], // 10 patterns (20KB db) → 1 domain
[50, 2], // 50 patterns (100KB) → 2 domains
[100, 3], // 100 patterns (200KB) → 3 domains
[200, 4], // 200 patterns (400KB) → 4 domains
[500, 5], // 500 patterns (1MB) → 5 domains
]
for (const [patternCount, expectedDomains] of cases) {
it(`${patternCount} patterns → ${expectedDomains} domain(s)`, () => {
if (patternCount > 0) {
const swarmDir = path.join(tmpDir, '.swarm')
fs.mkdirSync(swarmDir, { recursive: true })
fs.writeFileSync(path.join(swarmDir, 'memory.db'), Buffer.alloc(patternCount * 2 * 1024))
}
const r = run(['--json'], { cwd: tmpDir })
const json = JSON.parse(r.stdout)
assert.equal(
json.v3Progress.domainsCompleted,
expectedDomains,
`for ${patternCount} patterns`
)
})
}
})
// ── getLearningStats: session file counting ─────────────────────────────────
describe('learning stats — session files', () => {
it('counts JSON session files in .claude/sessions', () => {
const sessDir = path.join(tmpDir, '.claude', 'sessions')
fs.mkdirSync(sessDir, { recursive: true })
fs.writeFileSync(path.join(sessDir, 's1.json'), '{}')
fs.writeFileSync(path.join(sessDir, 's2.json'), '{}')
const r = run(['--json'], { cwd: tmpDir })
const json = JSON.parse(r.stdout)
// sessionsCompleted is derived from session file count
assert.ok(
json.v3Progress.sessionsCompleted >= 2,
`sessions: ${json.v3Progress.sessionsCompleted}`
)
})
})
})