Files
portal/tools/on-plan-write.test.mjs
T
Дмитрий b739d5adad feat(mentor): мерж роутера в наставника — единый рецензент (спека+план+скилы) + decision GO/NO-GO
Болезни B (роутер в пустоту) + A (наставник не заворачивал) — лечение Р7/Р8 (Подход 1):
наставник — единый мозг-рецензент, зовёт classify() как функцию (3 слоя + граф nodes.yaml +
карточки — код не тронут, новый вызыватель), судит спеку+план+выбор скилов, заворачивает NO-GO.

- validateMentorVerdict + промпты (план/спека): явное decision GO|NO-GO (поглощённый Р7)
- plan-skills.mjs: parsePlanSkills (skills-json) + extractPlanGoal (зеркало extractGoal судьи)
- mentor-seam: renderSkillContext; onPlanWrite зовёт classifyImpl (fail-safe: сбой → без скил-сверки)
- decideMentorObjection: заворот на decision=NO-GO ИЛИ сломанный вердикт; mentor-GO только на чистом GO
- formatMentorObjection доносит суть (recommendation + reasoning + plan_points), GO -> пусто
- enforce-mentor main: loadRegistry + classify; счётчик L1 decision-aware (Р7/§3.4)
- скил-сверка — только план (gate2); спека (gate1) — по сути + decision
- включает redesign согласования L1->L2 (Фазы 0-6, способ B: наставник->судья->печать)
- регрессия tools-only 3901 passed + 2 skip (база 3877, +24 теста, 0 регрессий)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-13 11:38:09 +03:00

88 lines
4.8 KiB
JavaScript

// tools/on-plan-write.test.mjs
import { describe, it, expect } from 'vitest';
import { onPlanWrite } from './on-plan-write.mjs';
import { planId } from './plan-lock.mjs';
const STEPS = [{ n: 1, op: 'Write', object: 'tools/a.mjs' }];
const goodVerdict = { plan_points_addressed: ['шаг 1 ок'], reasoning: 'разбор', recommendation: 'ок', confidence: 0.8, decision: 'GO' };
const llmOk = async () => goodVerdict;
describe('onPlanWrite (W3, A0 — нах.F5/C-1)', () => {
it('первый план → task-id присвоен из planId(steps) и персистнут', async () => {
const persisted = [];
const r = await onPlanWrite({ planSteps: STEPS, existingTaskId: null, persistTaskIdImpl: (id) => persisted.push(id), llmCall: llmOk, journalKey: 'k', nowMs: 1 });
expect(r.taskId).toBe(`task:${planId(STEPS)}`);
expect(persisted).toEqual([r.taskId]);
});
it('re-issue плана с новым хешем НЕ сбрасывает task-id (✅O17)', async () => {
const NEW_STEPS = [{ n: 1, op: 'Write', object: 'tools/b.mjs' }];
const r = await onPlanWrite({ planSteps: NEW_STEPS, existingTaskId: 'task:OLD', llmCall: llmOk, journalKey: 'k', nowMs: 1 });
expect(r.taskId).toBe('task:OLD');
});
it('вердикт произведён и привязан: verdict.plan_hash === planId(steps) (нах.F4)', async () => {
const r = await onPlanWrite({ planSteps: STEPS, llmCall: llmOk, journalKey: 'k', nowMs: 1 });
expect(r.ok).toBe(true);
expect(r.wired).toBe(true);
expect(r.verdict.plan_hash).toBe(planId(STEPS));
});
it('журнал переговоров пишется (side=mentor, круг 1, task-id-bound)', async () => {
const r = await onPlanWrite({ planSteps: STEPS, llmCall: llmOk, journalKey: 'k', nowMs: 1 });
expect(r.journalOk).toBe(true);
expect(r.journal.entries.length).toBe(1);
const p = r.journal.entries[0].payload;
expect(p.side).toBe('mentor');
expect(p.round).toBe(1);
expect(p.task_id).toBe(r.taskId);
});
it('SE10: сбой журнала НЕ крашит круг — journalOk:false, вердикт/task-id целы', async () => {
const boom = () => { throw new Error('журнал упал'); };
const r = await onPlanWrite({ planSteps: STEPS, llmCall: llmOk, journalKey: 'k', nowMs: 1, appendNegotiationImpl: boom });
expect(r.journalOk).toBe(false);
expect(r.taskId).toBe(`task:${planId(STEPS)}`);
expect(r.verdict.plan_hash).toBe(planId(STEPS));
});
it('сбой персиста task-id НЕ крашит — taskIdPersisted:false', async () => {
const r = await onPlanWrite({ planSteps: STEPS, persistTaskIdImpl: () => { throw new Error('disk'); }, llmCall: llmOk, journalKey: 'k', nowMs: 1 });
expect(r.taskIdPersisted).toBe(false);
expect(r.taskId).toBe(`task:${planId(STEPS)}`);
});
it('сбой вызова наставника → ok:false, wired:false (SE-R6-6), без краша', async () => {
const r = await onPlanWrite({ planSteps: STEPS, llmCall: async () => { throw new Error('сеть'); }, journalKey: 'k', nowMs: 1 });
expect(r.ok).toBe(false);
expect(r.wired).toBe(false);
expect(r.verdict).toBe(null);
});
});
describe('onPlanWrite — скил-сверка через classify (мерж)', () => {
const GO = { plan_points_addressed: ['ок'], reasoning: 'r', recommendation: 'rec', confidence: 0.9, decision: 'GO' };
it('зовёт classifyImpl и кладёт рекомендацию в промпт вердикта', async () => {
let classified = null; let capturedUser = null;
const r = await onPlanWrite({
planSteps: [{ n: 1, op: 'Edit', object: 'x', ref: 'D1' }],
declaredSkills: ['executing-plans'],
classifyImpl: async (goal) => { classified = goal; return { recommended_chain: ['systematic-debugging'] }; },
registry: {},
llmCall: async ({ buildPrompt }) => { capturedUser = buildPrompt().user; return GO; },
planGoal: 'починить парсер',
});
expect(classified).toBe('починить парсер');
expect(capturedUser).toMatch(/systematic-debugging/);
expect(capturedUser).toMatch(/executing-plans/);
expect(r.ok).toBe(true);
});
it('classifyImpl бросил → вердикт без скил-сверки (маркер недоступности), не падает', async () => {
let capturedUser = null;
const r = await onPlanWrite({
planSteps: [{ n: 1, op: 'Edit', object: 'x', ref: 'D1' }],
declaredSkills: ['x'],
classifyImpl: async () => { throw new Error('классификатор недоступен'); },
registry: {},
llmCall: async ({ buildPrompt }) => { capturedUser = buildPrompt().user; return GO; },
planGoal: 'g',
});
expect(r.ok).toBe(true);
expect(capturedUser).toMatch(/недоступн/i);
});
});