From 5b280fc59a51eaa68bb00d5a3d92e39556e4e400 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=94=D0=BC=D0=B8=D1=82=D1=80=D0=B8=D0=B9?= Date: Thu, 25 Jun 2026 14:20:27 +0300 Subject: [PATCH] test(secretary): gravity / reopen / history-compression mechanics (decision #8) --- tools/secretary-mechanics.test.mjs | 57 ++++++++++++++++++++++++++++++ 1 file changed, 57 insertions(+) create mode 100644 tools/secretary-mechanics.test.mjs diff --git a/tools/secretary-mechanics.test.mjs b/tools/secretary-mechanics.test.mjs new file mode 100644 index 0000000..fec5b59 --- /dev/null +++ b/tools/secretary-mechanics.test.mjs @@ -0,0 +1,57 @@ +import { describe, it, expect } from 'vitest'; +import { applyTend } from './secretary-gardener.mjs'; +import { applyResults } from './secretary-apply.mjs'; + +// Механика, которую на 2-спановых прогонах не проверяли (НАХОДКИ, решение №8). Тесты бьют по реальным +// функциям конвейера A (applyTend/applyResults), а не по воркеру — это поведение разбора, не оркестрации. + +describe('mechanics: reopen / обратный каскад', () => { + it('закрытая ветка снова открывается и снимается зачёркивание (несущее решение изменилось)', () => { + const proto = { hidden: [{ id: 'СВ-1', lens: 'Л4', status: 'закрыт', struck: true, text: 'дыра', born: 3, lastTouch: 5 }] }; + applyTend(proto, [{ id: 'СВ-1', action: 'reopen', why: 'несущее решение изменилось' }], 8); + const sv = proto.hidden[0]; + expect(sv.status).toBe('открыт'); + expect(sv.struck).toBe(false); + expect(sv.lastTouch).toBe(8); + expect(sv.born).toBe(3); // корень сохранён — не потеряли историю + }); +}); + +describe('mechanics: сжатие длинной истории (сворачивание ≠ удаление)', () => { + it('на масштабе: закрытые ветки сворачиваются (struck), но остаются с источником (born)', () => { + // 12 живых веток; закрываем половину с пруфом. + const hidden = Array.from({ length: 12 }, (_, i) => ({ + id: `СВ-${i + 1}`, lens: 'Л1', status: 'открыт', struck: false, + text: `нет выдержек кода ${i}`, born: i + 1, lastTouch: i + 1, + })); + const proto = { hidden }; + const tend = hidden.filter((_, i) => i % 2 === 0).map((h) => ({ id: h.id, action: 'close', proof: `код:${h.born}` })); + applyTend(proto, tend, 20); + // Ни одна ветка не удалена — сворачивание прячет видность, не корень («потом не восстановим»). + expect(proto.hidden).toHaveLength(12); + const closed = proto.hidden.filter((h) => h.status === 'закрыт'); + expect(closed).toHaveLength(6); + for (const h of closed) { + expect(h.struck).toBe(true); + expect(Number.isFinite(h.born)).toBe(true); // источник на месте + } + }); +}); + +describe('mechanics: гравитация (кандидат → ствол)', () => { + // ПРОБЕЛ (follow-up): в A механика НЕ реализована. applyResults накапливает кандидатов (p.candidates, + // дедуп по branch), но НЕТ пути продвижения кандидата в ствол (decisions/will), когда владелец заговорил + // об идее. Это суждение РЕДАКТОРА (сопоставить кандидата с движением ствола), не детерминированное + // правило — строить спекулятивно нельзя (решение владельца; реализация = доработка редактор-промпта). + // Документируем как незакрытый хвост, не выдаём отсутствующее за готовое. + it.todo('кандидат поднимается в ствол, когда владелец заговорил об его идее'); + + it('сейчас: кандидат лишь накапливается в candidates, в ствол сам не уходит (фиксируем текущее поведение)', () => { + const proto = { hidden: [], candidates: [], nextSvId: 1, decisions: [] }; + const d13 = { forks: [{ branch: 'вынести очередь в SQLite', trigger: 'цитата', why: 'масштаб', опора: 'догадка', релевантность: 'medium' }] }; + const out = applyResults(proto, 5, null, null, d13, null); + expect(out.candidates).toHaveLength(1); + expect(out.candidates[0].branch).toBe('вынести очередь в SQLite'); + expect(out.decisions || []).toHaveLength(0); // в ствол не просочился — подтверждает, что гравитации нет + }); +});