- 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>
219 lines
7.8 KiB
JavaScript
219 lines
7.8 KiB
JavaScript
/**
|
|
* Tests for .claude/helpers/memory.js
|
|
*
|
|
* Run: node --test tests/helpers/memory.test.js
|
|
*
|
|
* Coverage gaps addressed:
|
|
* - loadMemory: missing file, corrupted JSON, valid file
|
|
* - saveMemory: directory creation, JSON format
|
|
* - commands.get: with key, without key, missing key
|
|
* - commands.set: missing key arg, _updated timestamp, overwrite
|
|
* - commands.delete: missing key arg, non-existent key
|
|
* - commands.keys: filters _-prefixed meta keys
|
|
* - commands.clear: resets to empty object
|
|
* - Error handling: corrupted JSON masked silently
|
|
*/
|
|
|
|
'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')
|
|
|
|
// Helper to load the module with a custom CWD so MEMORY_FILE points to a tmp dir
|
|
function loadModule(cwd) {
|
|
// Clear require cache so MEMORY_DIR/MEMORY_FILE are re-evaluated each time
|
|
const modPath = require.resolve('../../.claude/helpers/memory.js')
|
|
delete require.cache[modPath]
|
|
const origCwd = process.cwd
|
|
process.cwd = () => cwd
|
|
try {
|
|
const mod = require('../../.claude/helpers/memory.js')
|
|
return mod
|
|
} finally {
|
|
process.cwd = origCwd
|
|
delete require.cache[modPath]
|
|
}
|
|
}
|
|
|
|
describe('memory.js', () => {
|
|
let tmpDir
|
|
let commands
|
|
let memoryFile
|
|
|
|
before(() => {
|
|
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'mem-test-'))
|
|
memoryFile = path.join(tmpDir, '.claude-flow', 'data', 'memory.json')
|
|
})
|
|
|
|
after(() => {
|
|
fs.rmSync(tmpDir, { recursive: true, force: true })
|
|
})
|
|
|
|
beforeEach(() => {
|
|
// Reset memory file before each test
|
|
const dir = path.dirname(memoryFile)
|
|
if (fs.existsSync(dir)) fs.rmSync(dir, { recursive: true })
|
|
commands = loadModule(tmpDir)
|
|
})
|
|
|
|
// ── loadMemory ─────────────────────────────────────────────────────────────
|
|
|
|
describe('loadMemory (via commands.get)', () => {
|
|
it('returns {} when memory file does not exist', () => {
|
|
const result = commands.get(undefined)
|
|
assert.deepEqual(result, {})
|
|
})
|
|
|
|
it('returns {} and does not throw when file contains corrupted JSON', () => {
|
|
const dir = path.dirname(memoryFile)
|
|
fs.mkdirSync(dir, { recursive: true })
|
|
fs.writeFileSync(memoryFile, '{ not valid json !!!')
|
|
// Should not throw; silently returns {}
|
|
const result = commands.get(undefined)
|
|
assert.deepEqual(result, {})
|
|
})
|
|
|
|
it('parses and returns stored memory when file is valid JSON', () => {
|
|
const dir = path.dirname(memoryFile)
|
|
fs.mkdirSync(dir, { recursive: true })
|
|
fs.writeFileSync(memoryFile, JSON.stringify({ foo: 'bar' }, null, 2))
|
|
const result = commands.get('foo')
|
|
assert.equal(result, 'bar')
|
|
})
|
|
|
|
// EDGE CASE: empty file (0 bytes) should not crash
|
|
it('returns {} when file is empty', () => {
|
|
const dir = path.dirname(memoryFile)
|
|
fs.mkdirSync(dir, { recursive: true })
|
|
fs.writeFileSync(memoryFile, '')
|
|
const result = commands.get(undefined)
|
|
assert.deepEqual(result, {})
|
|
})
|
|
})
|
|
|
|
// ── saveMemory (via commands.set) ──────────────────────────────────────────
|
|
|
|
describe('saveMemory (via commands.set)', () => {
|
|
it('creates the data directory if it does not exist', () => {
|
|
commands.set('k', 'v')
|
|
assert.ok(fs.existsSync(path.dirname(memoryFile)))
|
|
})
|
|
|
|
it('writes pretty-printed JSON to disk', () => {
|
|
commands.set('hello', 'world')
|
|
const raw = fs.readFileSync(memoryFile, 'utf-8')
|
|
const parsed = JSON.parse(raw)
|
|
assert.equal(parsed.hello, 'world')
|
|
// Pretty-printed → contains newlines
|
|
assert.ok(raw.includes('\n'))
|
|
})
|
|
})
|
|
|
|
// ── commands.get ───────────────────────────────────────────────────────────
|
|
|
|
describe('commands.get', () => {
|
|
it('returns the whole memory object when no key is provided', () => {
|
|
commands.set('a', 1)
|
|
commands.set('b', 2)
|
|
const result = commands.get(undefined)
|
|
assert.equal(result.a, 1)
|
|
assert.equal(result.b, 2)
|
|
})
|
|
|
|
it('returns undefined for a key that was never set', () => {
|
|
const result = commands.get('nonexistent')
|
|
assert.equal(result, undefined)
|
|
})
|
|
|
|
it('returns the value for an existing key', () => {
|
|
commands.set('mykey', 'myvalue')
|
|
const result = commands.get('mykey')
|
|
assert.equal(result, 'myvalue')
|
|
})
|
|
})
|
|
|
|
// ── commands.set ───────────────────────────────────────────────────────────
|
|
|
|
describe('commands.set', () => {
|
|
it('returns undefined and does not write when key is missing', () => {
|
|
const result = commands.set(undefined, 'value')
|
|
assert.equal(result, undefined)
|
|
assert.ok(!fs.existsSync(memoryFile))
|
|
})
|
|
|
|
it('writes a _updated ISO timestamp alongside the value', () => {
|
|
const before = Date.now()
|
|
commands.set('ts-test', 'v')
|
|
const after = Date.now()
|
|
const raw = JSON.parse(fs.readFileSync(memoryFile, 'utf-8'))
|
|
const ts = new Date(raw._updated).getTime()
|
|
assert.ok(ts >= before && ts <= after + 100)
|
|
})
|
|
|
|
it('overwrites an existing key without losing other keys', () => {
|
|
commands.set('x', 'first')
|
|
commands.set('y', 'other')
|
|
commands.set('x', 'second')
|
|
assert.equal(commands.get('x'), 'second')
|
|
assert.equal(commands.get('y'), 'other')
|
|
})
|
|
})
|
|
|
|
// ── commands.delete ────────────────────────────────────────────────────────
|
|
|
|
describe('commands.delete', () => {
|
|
it('logs error and does not write when key is missing', () => {
|
|
const result = commands.delete(undefined)
|
|
assert.equal(result, undefined)
|
|
assert.ok(!fs.existsSync(memoryFile))
|
|
})
|
|
|
|
it('removes an existing key from memory', () => {
|
|
commands.set('to-remove', 'val')
|
|
commands.delete('to-remove')
|
|
assert.equal(commands.get('to-remove'), undefined)
|
|
})
|
|
|
|
it('is a no-op and does not throw when deleting a non-existent key', () => {
|
|
commands.set('keep', 'val')
|
|
assert.doesNotThrow(() => commands.delete('ghost'))
|
|
assert.equal(commands.get('keep'), 'val')
|
|
})
|
|
})
|
|
|
|
// ── commands.keys ──────────────────────────────────────────────────────────
|
|
|
|
describe('commands.keys', () => {
|
|
it('returns only user-defined keys, excluding _-prefixed meta keys', () => {
|
|
commands.set('alpha', 1)
|
|
commands.set('beta', 2)
|
|
const keys = commands.keys()
|
|
assert.ok(keys.includes('alpha'))
|
|
assert.ok(keys.includes('beta'))
|
|
assert.ok(!keys.includes('_updated'), '_updated must be filtered out')
|
|
})
|
|
|
|
it('returns an empty array when memory is empty', () => {
|
|
const keys = commands.keys()
|
|
assert.deepEqual(keys, [])
|
|
})
|
|
})
|
|
|
|
// ── commands.clear ─────────────────────────────────────────────────────────
|
|
|
|
describe('commands.clear', () => {
|
|
it('replaces all memory with an empty object', () => {
|
|
commands.set('a', 1)
|
|
commands.set('b', 2)
|
|
commands.clear()
|
|
assert.deepEqual(commands.get(undefined), {})
|
|
})
|
|
|
|
it('does not throw when memory is already empty', () => {
|
|
assert.doesNotThrow(() => commands.clear())
|
|
})
|
|
})
|
|
})
|