Files
brain/tools/mentor-seam.test.mjs
T

190 lines
12 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/mentor-seam.test.mjs
import { describe, it, expect } from 'vitest';
import { buildMentorPrompt, renderSkillContext } from './mentor-seam.mjs';
describe('renderSkillContext (мерж роутер↔наставник)', () => {
it('содержит объявленные скилы и рекомендацию роутера', () => {
const s = renderSkillContext({ declared: ['executing-plans'], recommendedChain: ['systematic-debugging', 'test-driven-development'] });
expect(s).toMatch(/executing-plans/);
expect(s).toMatch(/systematic-debugging/);
expect(s).toMatch(/скил/i);
});
it('classify недоступен (null) → маркер «рекомендация недоступна», не пусто', () => {
const s = renderSkillContext({ declared: ['x'], recommendedChain: null });
expect(s).toMatch(/недоступн/i);
});
});
describe('renderSkillContext — резолв ID→имя (фикс)', () => {
const registry = { indexById: new Map([['#4', { id: '#4', name: 'markdownlint-cli2', slug: 'markdownlint' }]]) };
it('с реестром резолвит #4 → имя', () => {
const s = renderSkillContext({ declared: ['executing-plans'], recommendedChain: ['#4'], registry });
expect(s).toMatch(/#4 — markdownlint-cli2/);
});
it('без реестра → голый ID (fail-safe)', () => {
const s = renderSkillContext({ declared: ['x'], recommendedChain: ['#4'] });
expect(s).toMatch(/#4/);
expect(s).not.toMatch(/markdownlint/);
});
it('неизвестный ID → голый ID (не падает)', () => {
const s = renderSkillContext({ declared: ['x'], recommendedChain: ['#999'], registry });
expect(s).toMatch(/#999/);
});
});
describe('buildMentorPrompt (§6.2)', () => {
const graphSection = { kind: 'project-graph', districtCount: 2, layer0: [{ district: 'tools', nodeCount: 3 }, { district: 'app-backend', nodeCount: 7 }], staleness: { stale: true, commits_behind: 12, uncommitted: 1 } };
const verifiedContext = [{ id: '1', claim: 'есть runRouter', ref: 'router-engine.mjs:140', kind: 'EXTRACTED' }];
const negotiationLog = [{ round: 1, side: 'mentor', utterance: 'расширь', justification: 'риск' }];
it('возвращает {system, user}', () => {
const p = buildMentorPrompt({ prompt: 'сделай X', graphSection, verifiedContext, negotiationLog, catalog: { nodes: [{ id: 'n1', slug: 's', capabilities: 'c' }] } });
expect(typeof p.system).toBe('string');
expect(typeof p.user).toBe('string');
});
it('user содержит проверенный контекст и журнал переговоров', () => {
const p = buildMentorPrompt({ prompt: 'X', graphSection, verifiedContext, negotiationLog });
expect(p.user).toMatch(/ПРОВЕРЕННЫЙ КОНТЕКСТ/);
expect(p.user).toMatch(/runRouter/);
expect(p.user).toMatch(/ПЕРЕГОВОРЫ/);
expect(p.user).toMatch(/расширь/);
});
// FR-2 (финревью 2026-06-11, канон W1): районы+staleness рендерит БАЗОВЫЙ
// buildRouterPrompt в system; seam НЕ дублирует в user (Д-1а-обход снят).
it('staleness виден наставнику (✅O16 сигнал) — в system (W1-канон)', () => {
const p = buildMentorPrompt({ prompt: 'X', graphSection, verifiedContext, negotiationLog });
expect(p.system).toMatch(/staleness|устарел|commits_behind/i);
});
it('карта районов (layer0) рендерится в system (W1-канон; A3 нах.F1/C-2)', () => {
const p = buildMentorPrompt({ prompt: 'X', graphSection, verifiedContext, negotiationLog });
expect(p.system).toMatch(/КАРТА РАЙОНОВ/);
expect(p.system).toMatch(/tools/);
expect(p.system).toMatch(/app-backend/);
});
it('FR-2: карта районов НЕ дублируется — ровно одно вхождение на system+user', () => {
const p = buildMentorPrompt({ prompt: 'X', graphSection, verifiedContext, negotiationLog });
const total = `${p.system}\n${p.user}`;
expect(total.match(/КАРТА РАЙОНОВ/g)).toHaveLength(1);
expect(total.match(/staleness/gi)).toHaveLength(1);
});
it('FR-2: пустой layer0 + staleness → маркер ОТСУТСТВИЯ (F-C6) и staleness-строка в user (O16 сигнал-всегда)', () => {
const p = buildMentorPrompt({ prompt: 'X', graphSection: { layer0: [], staleness: { stale: true, commits_behind: null, uncommitted: 3 } } });
expect(p.user).toMatch(/КАРТА РАЙОНОВ ОТСУТСТВУЕТ/);
expect(p.user).toMatch(/СВЕЖЕСТЬ ГРАФА|staleness/i);
});
// VA-2 (финревью 3/5, «сигнал-всегда»): пустой verifiedContext НЕ молчит — наставник
// видит, что контекста НЕТ (VA-9 ловит только на печати, круги идут до неё).
it('VA-2: пустой verifiedContext → явный маркер КОНТЕКСТ ПУСТ в user', () => {
const p = buildMentorPrompt({ prompt: 'X', graphSection, verifiedContext: [] });
expect(p.user).toMatch(/КОНТЕКСТ ПУСТ/);
});
it('system несёт ДР-1 гранулярность (A8): растяжка + detectHighRisk', () => {
const p = buildMentorPrompt({ prompt: 'X', graphSection });
expect(p.system).toMatch(/растяжка до следующей НЕИЗВЕСТНОСТИ/);
expect(p.system).toMatch(/detectHighRisk/);
});
});
// Task 5 — runMentorRound + петля + F7: импорты внизу перед describe (ESM hoisting)
import { runMentorRound, mentorLoop, MENTOR_ROUND_CAP, DISTRICT_PROBE_CAP } from './mentor-seam.mjs';
import { buildNodeGraph } from './node-graph.mjs';
describe('runMentorRound (§4/6.1)', () => {
// Д-3: фикстура через buildNodeGraph (resolveNode ждёт byId/bySlug/byName Map)
const groundingGraph = buildNodeGraph({ nodes: [
{ id: '#19', slug: 'writing-plans', name: 'writing-plans', status: 'active' },
], chains: {} });
const trace = { candidates: ['writing-plans'], chosen: 'writing-plans', why_chosen: 'подходит', twins_considered: 'не brainstorming', confidence: 0.9 };
it('MENTOR_ROUND_CAP = 3', () => { expect(MENTOR_ROUND_CAP).toBe(3); });
it('валидный трейс → ok, wired:true', async () => {
const r = await runMentorRound({ prompt: 'X', groundingGraph, graphSection: { layer0: [] }, llmCall: async () => trace });
expect(r.ok).toBe(true);
expect(r.wired).toBe(true);
expect(r.abstain).toBe(false);
});
it('низкая уверенность → abstain (5.2)', async () => {
const lo = { ...trace, confidence: 0.2 };
const r = await runMentorRound({ prompt: 'X', groundingGraph, graphSection: { layer0: [] }, llmCall: async () => lo });
expect(r.abstain).toBe(true);
});
it('сбой вызова → ok:false, wired:false (НЕ суд, SE-R6-6)', async () => {
const r = await runMentorRound({ prompt: 'X', groundingGraph, graphSection: { layer0: [] }, llmCall: async () => { throw new Error('сеть'); } });
expect(r.ok).toBe(false);
expect(r.wired).toBe(false);
});
it('выдуманный узел → ok:false (заземление)', async () => {
const bad = { candidates: ['ВЫДУМКА'], chosen: 'ВЫДУМКА', why_chosen: 'w', twins_considered: 't', confidence: 0.9 };
const r = await runMentorRound({ prompt: 'X', groundingGraph, graphSection: { layer0: [] }, llmCall: async () => bad });
expect(r.ok).toBe(false);
});
});
describe('F7 — дозапрос соседнего района (A6/Н-3, ДР-P1)', () => {
const groundingGraph = buildNodeGraph({ nodes: [
{ id: '#19', slug: 'writing-plans', name: 'writing-plans', status: 'active' },
], chains: {} });
it('DISTRICT_PROBE_CAP = 2', () => { expect(DISTRICT_PROBE_CAP).toBe(2); });
it('трасса с request_district в пределах cap → сигнал escalateDistrict (конструктивное воздержание)', async () => {
const r = await runMentorRound({ prompt: 'X', groundingGraph, graphSection: { layer0: [] }, districtProbesUsed: 0, llmCall: async () => ({ request_district: 'app-backend', justification: 'нужен сосед' }) });
expect(r.ok).toBe(true);
expect(r.abstain).toBe(true);
expect(r.escalateDistrict).toBe('app-backend');
});
it('request_district СВЕРХ cap → отказ (ok:false, без escalateDistrict)', async () => {
const r = await runMentorRound({ prompt: 'X', groundingGraph, graphSection: { layer0: [] }, districtProbesUsed: 2, llmCall: async () => ({ request_district: 'app-backend', justification: 'ещё' }) });
expect(r.ok).toBe(false);
expect(r.escalateDistrict).toBe(null);
expect(r.reason).toMatch(/DISTRICT_PROBE_CAP/);
});
// VA-3 (финревью 3/5): несуществующий район НЕ эскалируется молча — при непустой
// карте (layer0) имя проверяется; вне карты → отказ с reason (опечатка ≠ пустая деталь).
it('VA-3: request_district вне карты районов (layer0 непуст) → отказ, не эскалация', async () => {
const gs = { layer0: [{ district: 'tools', nodeCount: 3 }] };
const r = await runMentorRound({ prompt: 'X', groundingGraph, graphSection: gs, districtProbesUsed: 0, llmCall: async () => ({ request_district: 'нет-такого', justification: 'опечатка' }) });
expect(r.ok).toBe(false);
expect(r.escalateDistrict).toBe(null);
expect(r.reason).toMatch(/район|VA-3/i);
});
it('VA-3: район ИЗ карты → эскалация как раньше; пустая карта → старое поведение (валидировать нечем)', async () => {
const inMap = await runMentorRound({ prompt: 'X', groundingGraph, graphSection: { layer0: [{ district: 'tools', nodeCount: 3 }] }, districtProbesUsed: 0, llmCall: async () => ({ request_district: 'tools', justification: 'сосед' }) });
expect(inMap.abstain).toBe(true);
expect(inMap.escalateDistrict).toBe('tools');
const noMap = await runMentorRound({ prompt: 'X', groundingGraph, graphSection: { layer0: [] }, districtProbesUsed: 0, llmCall: async () => ({ request_district: 'app-backend', justification: 'сосед' }) });
expect(noMap.escalateDistrict).toBe('app-backend');
});
});
describe('mentorLoop (§4/6.4 — Н-1)', () => {
it('круг ok без abstain → resolved, без эскалации', async () => {
const out = await mentorLoop({ runRoundImpl: async () => ({ ok: true, abstain: false }) });
expect(out.resolved).toBe(true);
expect(out.escalate).toBe(false);
expect(out.round).toBe(1);
});
it('потолок кругов без согласия → escalate к владельцу (ДР-6)', async () => {
const out = await mentorLoop({ runRoundImpl: async () => ({ ok: true, abstain: true }) });
expect(out.resolved).toBe(false);
expect(out.escalate).toBe(true);
expect(out.round).toBe(MENTOR_ROUND_CAP);
});
});
describe('sharp-edges F-C5/F-C6', () => {
const gg = buildNodeGraph({ nodes: [
{ id: '#19', slug: 'writing-plans', name: 'writing-plans', status: 'active' },
], chains: {} });
it('F-C6: без graphSection в user явный маркер ОТСУТСТВИЯ карты (слой-0 ВСЕГДА, сигнал §5.4)', () => {
const p = buildMentorPrompt({ prompt: 'X' });
expect(p.user).toMatch(/КАРТА РАЙОНОВ ОТСУТСТВУЕТ/);
});
it('F-C6: пустой layer0 → тоже маркер отсутствия', () => {
const p = buildMentorPrompt({ prompt: 'X', graphSection: { layer0: [] } });
expect(p.user).toMatch(/КАРТА РАЙОНОВ ОТСУТСТВУЕТ/);
});
it('F-C5: districtProbesUsed НЕ передан → дозапрос района отклоняется (secure default — омиссия не даёт безлимит)', async () => {
const r = await runMentorRound({ prompt: 'X', groundingGraph: gg, graphSection: { layer0: [] }, llmCall: async () => ({ request_district: 'app-backend', justification: 'нужен' }) });
expect(r.ok).toBe(false);
expect(r.escalateDistrict).toBe(null);
expect(r.reason).toMatch(/DISTRICT_PROBE_CAP/);
});
});