Files
brain/tools/oq-ledger.test.mjs
T
Дмитрий 7c282242c2 feat(oq): журнал охотника — формат/разбор/replay/активная-сессия
Узел «журнал» скила surfacing-open-questions (спека v6, раздел zhurnal):
formatEventLine/parseEventLine, replay с отбросом битой хвостовой строки,
activeSessionOf (порог протухания 30 мин — одна активная сессия на тему).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-21 08:18:31 +03:00

55 lines
2.6 KiB
JavaScript

import { describe, it, expect } from 'vitest';
import { formatEventLine, parseEventLine, replay, activeSessionOf } from './oq-ledger.mjs';
describe('formatEventLine / parseEventLine', () => {
it('сериализует событие в одну читаемую .md-строку с провенансом', () => {
const line = formatEventLine({
at: '2026-06-21T12:00:00Z', kind: 'ANSWER', qid: 'q3',
text: 'грузим фоном', provenance: 'внутр:auth.php:42', session: 'S1',
});
expect(line).toBe('- [2026-06-21T12:00:00Z] ANSWER q3: грузим фоном · источник: внутр:auth.php:42 · сессия:S1');
expect(line).not.toContain('\n');
});
it('parseEventLine — обратная операция formatEventLine', () => {
const ev = { at: '2026-06-21T12:00:00Z', kind: 'ANSWER', qid: 'q3', text: 'x', provenance: 'допущение', session: 'S1' };
expect(parseEventLine(formatEventLine(ev))).toEqual(ev);
});
it('parseEventLine возвращает null на непарсящейся строке', () => {
expect(parseEventLine('мусор без формата')).toBeNull();
});
});
describe('replay', () => {
it('разбирает события и ОТБРАСЫВАЕТ неполную последнюю строку (обрыв)', () => {
const raw = [
'- [2026-06-21T12:00:00Z] ANSWER q1: a · источник: воля:ответ#1 · сессия:S1',
'- [2026-06-21T12:01:00Z] CLOSE q1: решён · источник: воля:ответ#1 · сессия:S1',
'- [2026-06-21T12:02:00Z] ANSWER q2: обор',
].join('\n');
const events = replay(raw);
expect(events).toHaveLength(2);
expect(events[1].kind).toBe('CLOSE');
});
it('пустой ввод → пустой список', () => {
expect(replay('')).toEqual([]);
});
});
describe('activeSessionOf', () => {
const nowMs = Date.parse('2026-06-21T12:40:00Z');
it('свежая запись другой сессии → занято (порог 30 мин)', () => {
const raw = '- [2026-06-21T12:20:00Z] ANSWER q1: a · источник: воля:ответ#1 · сессия:S2';
expect(activeSessionOf(raw, { nowMs, staleMinutes: 30 })).toBe('S2');
});
it('протухшая запись (>30 мин) → свободно (null)', () => {
const raw = '- [2026-06-21T12:00:00Z] ANSWER q1: a · источник: воля:ответ#1 · сессия:S2';
expect(activeSessionOf(raw, { nowMs, staleMinutes: 30 })).toBeNull();
});
it('пустой журнал → null', () => {
expect(activeSessionOf('', { nowMs })).toBeNull();
});
});