7c282242c2
Узел «журнал» скила surfacing-open-questions (спека v6, раздел zhurnal): formatEventLine/parseEventLine, replay с отбросом битой хвостовой строки, activeSessionOf (порог протухания 30 мин — одна активная сессия на тему). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
55 lines
2.6 KiB
JavaScript
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();
|
|
});
|
|
});
|