Files
brain/tools/router-mentor-integration.test.mjs
T

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('сырьё вне плана в impl → блок; файл шага → allow; граф-карта → allow', () => {
expect(decide(base({ name: 'Read', input: { file_path: 'app/Services/LeadRouter.php' } })).decision).toBe('block');
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('decideReadEvent (реальный D) различает виды: граф-карта свободна, сырьё в impl блокируется', () => {
const raw = decideReadEvent({ ext: '.php', path: 'app/Services/LeadRouter.php', frozenPlan: true });
expect(raw.gate.block).toBe(true);
const map = decideReadEvent({ ext: '.json', path: '.claude/worktrees/graphify-spike/graphify-out/graph.json', frozenPlan: true });
expect(map.gate.block).toBe(false);
});
});