// 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); }); });