2772b197b3
secretary-audit.mjs (новый): 9 линз, buildAuditPrompt -> {system,user}, parseAuditResponse,
applyAudit (новые СВ с номером от хука, мутация+родословная, close/тихо/partial, горящие
блоки Л8/Л9), preserveRegistry (реестр СВ изолирован от reconcile).
protocol: поля hidden/acceptance/tails/nextSvId + рендер горящих блоков и раздела
«Скрытые вопросы (фон)». stop-hook: второй проход после reconcile + снимок реестра ДО
reconcile (reconcile не владеет СВ). + дизайн-спека и план.
97 юнит-тестов зелёные. Живьём подтверждены: наполнение, мутация под тем же номером,
routing Л9 в горящий блок. Известно: старый реестр в деле «линза» уже искажён до фикса.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
122 lines
6.7 KiB
JavaScript
122 lines
6.7 KiB
JavaScript
import { describe, it, expect } from 'vitest';
|
||
import { applyAudit, parseAuditResponse, buildAuditPrompt, LENSES, preserveRegistry } from './secretary-audit.mjs';
|
||
|
||
// Task 2: новые скрытые вопросы получают номер от хука
|
||
describe('applyAudit — новые СВ', () => {
|
||
it('новый СВ получает номер от хука и статус открыт', () => {
|
||
const p = { hidden: [], acceptance: [], tails: [], nextSvId: 1 };
|
||
applyAudit(p, { new: [{ text: 'хватит ли перезапуска?', lens: 'Л1' }], ops: [] }, 4);
|
||
expect(p.hidden).toHaveLength(1);
|
||
expect(p.hidden[0]).toMatchObject({ id: 'СВ-1', lens: 'Л1', status: 'открыт', text: 'хватит ли перезапуска?', born: 4, lastTouch: 4 });
|
||
expect(p.nextSvId).toBe(2);
|
||
});
|
||
});
|
||
|
||
// Task 3: мутация (зачёркивание + родословная)
|
||
describe('applyAudit — мутация', () => {
|
||
it('mutate сохраняет старый текст в родословную и меняет статус', () => {
|
||
const p = { hidden: [{ id: 'СВ-1', lens: 'Л1', status: 'открыт', text: 'старая', born: 1, lastTouch: 1, lineage: [] }],
|
||
acceptance: [], tails: [], nextSvId: 2 };
|
||
applyAudit(p, { new: [], ops: [{ id: 'СВ-1', action: 'mutate', newText: 'новая' }] }, 11);
|
||
expect(p.hidden[0].text).toBe('новая');
|
||
expect(p.hidden[0].status).toBe('мутировал');
|
||
expect(p.hidden[0].lineage).toEqual([{ turn: 1, text: 'старая' }]);
|
||
expect(p.hidden[0].lastTouch).toBe(11);
|
||
});
|
||
});
|
||
|
||
// Task 4: закрытие, тихое закрытие, partial
|
||
describe('applyAudit — close/partial', () => {
|
||
it('close/тихое/partial выставляют статус', () => {
|
||
const mk = (id) => ({ id, lens: 'Л1', status: 'открыт', text: 't', born: 1, lastTouch: 1, lineage: [] });
|
||
const p = { hidden: [mk('СВ-1'), mk('СВ-2'), mk('СВ-3')], acceptance: [], tails: [], nextSvId: 4 };
|
||
applyAudit(p, { new: [], ops: [
|
||
{ id: 'СВ-1', action: 'close', silent: false },
|
||
{ id: 'СВ-2', action: 'close', silent: true },
|
||
{ id: 'СВ-3', action: 'partial' },
|
||
] }, 18);
|
||
expect(p.hidden[0].status).toBe('закрыт');
|
||
expect(p.hidden[1].status).toBe('тихо-закрыт');
|
||
expect(p.hidden[2].status).toBe('открыт');
|
||
});
|
||
});
|
||
|
||
// Task 5: горящие блоки Л8/Л9
|
||
describe('applyAudit — Л8/Л9 горящие блоки', () => {
|
||
it('Л8→acceptance, Л9→tails с дедупом; resolved гасит', () => {
|
||
const p = { hidden: [], acceptance: [], tails: [], nextSvId: 1 };
|
||
applyAudit(p, { new: [
|
||
{ text: 'заявлено работает, проверки нет', lens: 'Л8' },
|
||
{ text: 'temp-скрипт не удалён', lens: 'Л9' },
|
||
{ text: 'заявлено работает, проверки нет', lens: 'Л8' }, // дубль
|
||
], ops: [] }, 5);
|
||
expect(p.acceptance).toHaveLength(1);
|
||
expect(p.tails).toHaveLength(1);
|
||
applyAudit(p, { new: [], ops: [], resolved: ['заявлено работает, проверки нет'] }, 6);
|
||
expect(p.acceptance[0].done).toBe(true);
|
||
});
|
||
});
|
||
|
||
// Task 6: parseAuditResponse
|
||
describe('parseAuditResponse', () => {
|
||
it('вытаскивает JSON из обёртки и переносит дефолты', () => {
|
||
const txt = 'Вот результат:\n```json\n{"new":[{"text":"q","lens":"Л1"}]}\n```\nготово';
|
||
const r = parseAuditResponse(txt);
|
||
expect(r.new).toEqual([{ text: 'q', lens: 'Л1' }]);
|
||
expect(r.ops).toEqual([]);
|
||
expect(r.resolved).toEqual([]);
|
||
});
|
||
it('возвращает пустой результат на мусоре', () => {
|
||
expect(parseAuditResponse('не json')).toEqual({ new: [], ops: [], resolved: [] });
|
||
});
|
||
});
|
||
|
||
// Task 7: buildAuditPrompt и LENSES
|
||
describe('buildAuditPrompt и LENSES', () => {
|
||
it('промпт содержит все 9 линз, текущие СВ и обмен', () => {
|
||
expect(LENSES).toHaveLength(9);
|
||
const proto = { hidden: [{ id: 'СВ-1', lens: 'Л1', status: 'открыт', text: 'старый вопрос' }] };
|
||
const msgs = buildAuditPrompt(proto, { user: 'реплика', assistant: 'ответ' });
|
||
const joined = JSON.stringify(msgs);
|
||
expect(joined).toContain('Л9');
|
||
expect(joined).toContain('СВ-1');
|
||
expect(joined).toContain('старый вопрос');
|
||
expect(joined).toContain('реплика');
|
||
});
|
||
// ФОРМА для callAnthropicAPI: { system, user } обе строки (НЕ массив сообщений — иначе API 400).
|
||
it('возвращает { system, user } строками (форма для callAnthropicAPI)', () => {
|
||
const msgs = buildAuditPrompt({ hidden: [] }, { user: 'у', assistant: 'а' });
|
||
expect(Array.isArray(msgs)).toBe(false);
|
||
expect(typeof msgs.system).toBe('string');
|
||
expect(typeof msgs.user).toBe('string');
|
||
expect(msgs.system.length).toBeGreaterThan(0);
|
||
});
|
||
// Контекст «по максимуму»: решения и явные вопросы шлём (для Л5/анти-дублей), Шаги — нет.
|
||
it('кидает контекст дела — решения и явные вопросы', () => {
|
||
const proto = { hidden: [], decisions: [{ text: 'решили B' }], open: [{ text: 'явный вопрос X' }] };
|
||
const { user } = buildAuditPrompt(proto, { user: 'у', assistant: 'а' });
|
||
expect(user).toContain('РЕШЕНИЯ');
|
||
expect(user).toContain('решили B');
|
||
expect(user).toContain('явный вопрос X');
|
||
});
|
||
});
|
||
|
||
// Изоляция реестра от reconcile: версию reconcile игнорируем, берём снимок ДО reconcile
|
||
describe('preserveRegistry — реестр СВ изолирован от reconcile', () => {
|
||
it('возвращает реестр из снимка, игнорируя перенумерованную версию reconcile', () => {
|
||
const reconciled = { hidden: [{ id: 'СВ-99', lens: '1' }], acceptance: [{ text: 'мусор' }], tails: [], nextSvId: 50 };
|
||
const snap = { hidden: [{ id: 'СВ-1', lens: 'Л4', text: 'вопрос' }], acceptance: [], tails: [{ text: 'хвост' }], nextSvId: 2 };
|
||
preserveRegistry(reconciled, snap);
|
||
expect(reconciled.hidden).toEqual([{ id: 'СВ-1', lens: 'Л4', text: 'вопрос' }]);
|
||
expect(reconciled.acceptance).toEqual([]);
|
||
expect(reconciled.tails).toEqual([{ text: 'хвост' }]);
|
||
expect(reconciled.nextSvId).toBe(2);
|
||
});
|
||
it('пустой снимок даёт чистый реестр', () => {
|
||
const p = { hidden: [{ id: 'x' }], nextSvId: 9 };
|
||
preserveRegistry(p, undefined);
|
||
expect(p.hidden).toEqual([]);
|
||
expect(p.nextSvId).toBe(1);
|
||
});
|
||
});
|