Files
brain/tools/router-learning-queue.test.mjs

93 lines
4.5 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { describe, it, expect } from 'vitest';
import {
makeQueueEntry, enqueue, pendingCount, applyApprovalBatch,
renderApprovalSection, statusSignal, saveQueue, loadQueue,
} from './router-learning-queue.mjs';
const cand = (id, kind = 'example') => ({ id, kind, summary: `s-${id}`, why_proposed: `w-${id}` });
describe('enqueue (hard-rule: предложение, НИКОГДА не одобрено само)', () => {
it('новый кандидат → status pending, не approved', () => {
const q = enqueue([], cand('a'));
expect(q).toHaveLength(1);
expect(q[0].status).toBe('pending');
});
it('makeQueueEntry всегда pending (нет авто-одобрения)', () => {
expect(makeQueueEntry(cand('x')).status).toBe('pending');
});
it('pendingCount считает только ожидающие', () => {
const q = enqueue(enqueue([], cand('a')), cand('b'));
expect(pendingCount(q)).toBe(2);
});
});
describe('applyApprovalBatch (одобрение пачкой — ТОЛЬКО по явному решению владельца)', () => {
const base = enqueue(enqueue(enqueue([], cand('1')), cand('2')), cand('3'));
it('approve → в фонд; reject → помечен; defer → остаётся pending', () => {
const r = applyApprovalBatch(base, { approve: ['1'], reject: ['2'], defer: ['3'] });
expect(r.fund.map((e) => e.id)).toEqual(['1']);
expect(r.queue.find((e) => e.id === '2').status).toBe('rejected');
expect(r.queue.find((e) => e.id === '3').status).toBe('pending');
expect(pendingCount(r.queue)).toBe(1);
});
it('пустое решение → ничего не входит в фонд (без «да» НИКАК)', () => {
const r = applyApprovalBatch(base, {});
expect(r.fund).toEqual([]);
expect(pendingCount(r.queue)).toBe(3);
});
it('rejected повторно не «pending» (не предлагается снова)', () => {
const once = applyApprovalBatch(base, { reject: ['1'] });
expect(once.queue.find((e) => e.id === '1').status).toBe('rejected');
expect(pendingCount(once.queue)).toBe(2);
});
it('конфликт approve+reject одного id → НЕ в фонд (reject-приоритет; hard-rule «без явного да НИКАК»)', () => {
const r = applyApprovalBatch(base, { approve: ['1'], reject: ['1'] });
expect(r.fund.map((e) => e.id)).not.toContain('1');
expect(r.queue.find((e) => e.id === '1').status).toBe('rejected');
});
// fix: tools/router-learning-queue.mjs (G, аудит M1-M4) — повторное решение по уже-решённому id не дублирует фонд
it('повторное approve уже одобренной записи не кладёт дубль в фонд', () => {
const r1 = applyApprovalBatch(base, { approve: ['1'] });
expect(r1.fund.length).toBe(1);
const r2 = applyApprovalBatch(r1.queue, { approve: ['1'] });
expect(r2.fund.length).toBe(0);
});
});
describe('renderApprovalSection (/brain-retro «На одобрение»)', () => {
it('перечисляет только pending с id/summary/why', () => {
const q = enqueue(applyApprovalBatch(enqueue(enqueue([], cand('1')), cand('2')), { approve: ['1'] }).queue, cand('3'));
const md = renderApprovalSection(q);
expect(md).toMatch(/На одобрение/);
expect(md).toMatch(/2/);
expect(md).toMatch(/3/);
expect(md).not.toMatch(/s-1/);
});
it('пустая очередь → пометка «нет кандидатов»', () => {
expect(renderApprovalSection([])).toMatch(/нет кандидатов|пусто/i);
});
});
describe('statusSignal (STATUS.md «ждут: N»)', () => {
it('строка с числом ожидающих', () => {
const q = enqueue(enqueue([], cand('a')), cand('b'));
expect(statusSignal(q)).toMatch(/ждут.*2/);
});
});
describe('персист очереди (fs инъектируется)', () => {
function memFs() {
const m = new Map();
return { m, readFileSync: (p) => { if (!m.has(String(p))) { const e = new Error('ENOENT'); e.code = 'ENOENT'; throw e; } return m.get(String(p)); }, writeFileSync: (p, d) => m.set(String(p), String(d)) };
}
it('save→load round-trips', () => {
const fs = memFs();
const q = enqueue([], cand('a'));
saveQueue({ queue: q, path: '/q.json', fsImpl: fs });
expect(loadQueue({ path: '/q.json', fsImpl: fs })).toEqual(q);
});
it('нет файла → []', () => {
expect(loadQueue({ path: '/none', fsImpl: memFs() })).toEqual([]);
});
});