Files
brain/tools/router-mentor-integration.test.mjs
T
Дмитрий e91aa021f0 feat: A - чтение под опечатанным планом свободно (ДР-1 снят в impl)
Под планом авторское чтение больше не блок: свой вывод, лог упавшего шага,
новый файл доступны. Чтение не двигает очередь шагов; impl-чтения логируются
с пометкой impl:true для ретро и не считаются во фронт-лоад порог. Секреты
держит отдельный read-path-deny. Свод зелёный: 4221 passed, 2 skipped.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-18 10:40:23 +03:00

87 lines
6.6 KiB
JavaScript
Raw 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.
// tools/router-mentor-integration.test.mjs
// W6 (C2, V-2) [НЕ-TDD: интеграционный] — РЕАЛЬНЫЕ A+B+D проведены в C/W1/W2 без стабов:
// ловит ошибки полярности/формы/состояния, которые per-модульные тесты на стабах прячут
// (SE2/SE3/SE5). llmCall остаётся инъекцией (живой транспорт — активация владельца).
import { describe, it, expect } from 'vitest';
import { artifactHasUnresolvedExtracted } from './context-verity.mjs'; // A (реальный)
import { buildDistrictMap, buildGraphSection } from './project-graph.mjs'; // B (реальный)
import { decideReadEvent } from './reading-discipline.mjs'; // D (реальный, через W2)
import { buildMentorPrompt, runMentorRound } from './mentor-seam.mjs'; // C
import { runMentorVerdict } from './mentor-verdict.mjs'; // C
import { freezeGate } from './freeze-gate.mjs'; // C
import { decide } from './enforce-supreme-gate.mjs'; // М2 + W2
import { buildNodeGraph } from './node-graph.mjs';
import { planId } from './plan-lock.mjs';
// — B: реальная карта районов из реальных DISTRICT_RULES —
const NODES = [
{ id: 'n1', label: 'router-engine', source_file: 'tools/router-engine.mjs', community: 1 },
{ id: 'n2', label: 'LeadRouter', source_file: 'app/Services/LeadRouter.php', community: 2 },
];
const districtMap = buildDistrictMap(NODES, []);
const graphSection = buildGraphSection({ districtMap, staleness: { stale: false, commits_behind: 0, uncommitted: 0 } });
// — A: реальный verity-резолв по anchor-подстроке —
const FILES = { 'tools/x.mjs': 'export function runRouter() { /* живой код */ }' };
const readFileImpl = (f) => { if (!(f in FILES)) { throw new Error('ENOENT'); } return FILES[f]; };
const cleanArtifact = [{ id: '1', kind: 'EXTRACTED', claim: 'есть runRouter', ref: 'tools/x.mjs:1', anchor: 'runRouter' }];
const dirtyArtifact = [{ id: '1', kind: 'EXTRACTED', claim: 'выдумка', ref: 'tools/x.mjs:1', anchor: 'НЕСУЩЕСТВУЮЩИЙ_ЯКОРЬ' }];
const STEPS = [{ n: 1, op: 'Edit', object: 'tools/x.mjs' }];
const goodVerdict = { plan_points_addressed: ['шаг 1 ок'], reasoning: 'разбор', recommendation: 'ок', confidence: 0.8, decision: 'GO' };
describe('W6 — интеграция B→C/W1: районы видны наставнику', () => {
// FR-2 (финревью 2026-06-11): канон W1 — районы в system (база), seam НЕ дублирует
// в user (Д-1а-обход снят); ровно одно вхождение карты на весь промпт.
it('реальная карта районов B рендерится в system (W1-канон), без дубля в user', () => {
const p = buildMentorPrompt({ prompt: 'x', graphSection, verifiedContext: cleanArtifact });
expect(p.system).toMatch(/КАРТА РАЙОНОВ/);
expect(p.system).toMatch(/app-backend/);
expect(`${p.system}\n${p.user}`.match(/КАРТА РАЙОНОВ/g)).toHaveLength(1);
});
});
describe('W6 — интеграция A→C: verity-полярность на реальном guard', () => {
const PH = planId(STEPS);
const impl = (art) => artifactHasUnresolvedExtracted(art, readFileImpl);
it('чистый артефакт (anchor резолвится) + содержательный вердикт → freeze pass', async () => {
const r = await runMentorVerdict({ plan: { steps: STEPS }, planHash: PH, llmCall: async () => goodVerdict });
const g = freezeGate({ mentorVerdict: r.verdict, mentorWired: r.wired, planHash: PH, verifiedContextArtifact: cleanArtifact, hasUnresolvedExtractedImpl: impl });
expect(g.pass).toBe(true);
});
it('грязный артефакт (anchor НЕ резолвится → downgrade) → freeze блок (✅O2)', async () => {
const r = await runMentorVerdict({ plan: { steps: STEPS }, planHash: PH, llmCall: async () => goodVerdict });
const g = freezeGate({ mentorVerdict: r.verdict, mentorWired: r.wired, planHash: PH, verifiedContextArtifact: dirtyArtifact, hasUnresolvedExtractedImpl: impl });
expect(g.pass).toBe(false);
expect(g.reason).toMatch(/EXTRACTED/);
});
});
describe('W6 — SE2: graph vs graphSection не взаимозаменяемы', () => {
const trace = { candidates: ['writing-plans'], chosen: 'writing-plans', why_chosen: 'w', twins_considered: 't', confidence: 0.9 };
it('верная проводка: groundingGraph=нод-граф, graphSection=карта B → ok', async () => {
const groundingGraph = buildNodeGraph({ nodes: [{ id: '#19', slug: 'writing-plans', name: 'writing-plans' }], chains: {} });
const r = await runMentorRound({ prompt: 'x', groundingGraph, graphSection, llmCall: async () => trace });
expect(r.ok).toBe(true);
});
it('своп (graphSection на месте groundingGraph) ломает заземление с грохотом (SE2)', async () => {
await expect(runMentorRound({ prompt: 'x', groundingGraph: graphSection, graphSection, llmCall: async () => trace }))
.rejects.toThrow();
});
});
describe('W6 — интеграция D→W2: дисциплина чтения на реальном decideReadEvent', () => {
const base = (toolUse) => ({ toolUse, frozenPlan: { artifact_id: null, steps: STEPS }, frozenArtifact: null, stepPtr: 0, key: 'k', verifyImpl: () => true, verifyArtifactImpl: () => true, normalize: (p) => String(p).toLowerCase() });
it('A: сырьё под планом в impl → allow (ДР-1 снят); файл шага → allow; граф-карта → allow', () => {
expect(decide(base({ name: 'Read', input: { file_path: 'app/Services/LeadRouter.php' } })).decision).toBe('allow');
expect(decide(base({ name: 'Read', input: { file_path: 'tools/x.mjs' } })).decision).toBe('allow');
expect(decide(base({ name: 'Read', input: { file_path: '.claude/worktrees/graphify-spike/graphify-out/graph.json' } })).decision).toBe('allow');
});
it('A: decideReadEvent (реальный D) — и граф-карта, и сырьё в impl свободны (ДР-1 снят)', () => {
const raw = decideReadEvent({ ext: '.php', path: 'app/Services/LeadRouter.php', frozenPlan: true });
expect(raw.gate.block).toBe(false);
const map = decideReadEvent({ ext: '.json', path: '.claude/worktrees/graphify-spike/graphify-out/graph.json', frozenPlan: true });
expect(map.gate.block).toBe(false);
});
});