e91aa021f0
Под планом авторское чтение больше не блок: свой вывод, лог упавшего шага, новый файл доступны. Чтение не двигает очередь шагов; impl-чтения логируются с пометкой impl:true для ретро и не считаются во фронт-лоад порог. Секреты держит отдельный read-path-deny. Свод зелёный: 4221 passed, 2 skipped. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
87 lines
6.6 KiB
JavaScript
87 lines
6.6 KiB
JavaScript
// 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);
|
||
});
|
||
});
|