- 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>
279 lines
9.9 KiB
JavaScript
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}`
|
|
)
|
|
})
|
|
})
|
|
})
|