Files
brain/tools/project-graph.test.mjs
T

248 lines
12 KiB
JavaScript
Raw Normal View History

// tools/project-graph.test.mjs
import { describe, it, expect } from 'vitest';
import { parseGraph, nodeDistrict, DISTRICT_RULES } from './project-graph.mjs';
describe('parseGraph', () => {
it('парсит node-link JSON в {nodes, links}', () => {
const text = JSON.stringify({
directed: false, multigraph: false, graph: {},
nodes: [{ id: 'a', label: 'A', file_type: 'code', source_file: 'tools/x.mjs', community: 3 }],
links: [{ source: 'a', target: 'b', relation: 'references' }],
});
const g = parseGraph(text);
expect(g.nodes).toHaveLength(1);
expect(g.links).toHaveLength(1);
expect(g.nodes[0].id).toBe('a');
});
it('битый JSON → {nodes:[], links:[]} (не крашит)', () => {
expect(parseGraph('{не json')).toEqual({ nodes: [], links: [] });
});
it('отсутствующие массивы → пустые', () => {
expect(parseGraph('{}')).toEqual({ nodes: [], links: [] });
});
it('не-строка → {nodes:[], links:[]}', () => {
expect(parseGraph(null)).toEqual({ nodes: [], links: [] });
});
});
describe('nodeDistrict', () => {
it('docs/superpowers → docs-superpowers', () => {
expect(nodeDistrict('docs/superpowers/specs/x.md')).toBe('docs-superpowers');
});
it('docs/observer → docs-observer', () => {
expect(nodeDistrict('docs/observer/STATUS.md')).toBe('docs-observer');
});
it('app/resources/js → app-frontend', () => {
expect(nodeDistrict('app/resources/js/views/DealsView.vue')).toBe('app-frontend');
});
it('app/app (php backend) → app-backend', () => {
expect(nodeDistrict('app/app/Services/LeadRouter.php')).toBe('app-backend');
});
it('tools/ → tools', () => {
expect(nodeDistrict('tools/router-engine.mjs')).toBe('tools');
});
it('неизвестный/пустой источник → other', () => {
expect(nodeDistrict('some/random/thing.txt')).toBe('other');
expect(nodeDistrict('')).toBe('other');
expect(nodeDistrict(null)).toBe('other');
});
it('DISTRICT_RULES заморожен и упорядочен (specific перед generic)', () => {
expect(Object.isFrozen(DISTRICT_RULES)).toBe(true);
const docsSuperIdx = DISTRICT_RULES.findIndex((r) => r.prefix === 'docs/superpowers/');
const docsIdx = DISTRICT_RULES.findIndex((r) => r.prefix === 'docs/');
expect(docsSuperIdx).toBeGreaterThanOrEqual(0);
expect(docsSuperIdx).toBeLessThan(docsIdx); // specific раньше generic
});
});
import { pruneJunk, PRUNE_PREFIXES } from './project-graph.mjs';
describe('pruneJunk', () => {
const nodes = [
{ id: '1', source_file: 'docs/x.md' },
{ id: '2', source_file: 'app/public/build/assets/app-x.css' },
{ id: '3', source_file: 'tools/router-engine.mjs' },
{ id: '4', source_file: null },
];
it('узлы с junk-префиксом отсечены, остальные сохранены', () => {
const r = pruneJunk(nodes, ['app/public/build/']);
expect(r.kept.map((n) => n.id)).toEqual(['1', '3', '4']);
expect(r.prunedCount).toBe(1);
});
it('PRUNE_PREFIXES заморожен и включает подтверждённый build', () => {
expect(Object.isFrozen(PRUNE_PREFIXES)).toBe(true);
expect(PRUNE_PREFIXES).toContain('app/public/build/');
});
it('не-массив → {kept:[], prunedCount:0}', () => {
expect(pruneJunk(null, PRUNE_PREFIXES)).toEqual({ kept: [], prunedCount: 0 });
});
});
import { nodeDegrees, buildDistrictMap } from './project-graph.mjs';
describe('nodeDegrees', () => {
it('считает степень по вхождениям id в source/target', () => {
const links = [
{ source: 'a', target: 'b' },
{ source: 'a', target: 'c' },
{ source: 'b', target: 'c' },
];
const deg = nodeDegrees(links);
expect(deg.get('a')).toBe(2);
expect(deg.get('b')).toBe(2);
expect(deg.get('c')).toBe(2);
});
it('пустые/битые links → пустая карта', () => {
expect(nodeDegrees(null).size).toBe(0);
});
});
describe('buildDistrictMap (layer-0)', () => {
const nodes = [
{ id: 'a', label: 'A', source_file: 'tools/router-engine.mjs', community: 5 },
{ id: 'b', label: 'B', source_file: 'tools/judge-engine.mjs', community: 5 },
{ id: 'c', label: 'C', source_file: 'tools/escape-grant.mjs', community: 7 },
{ id: 'd', label: 'D', source_file: 'docs/observer/STATUS.md', community: null },
{ id: 'j', label: 'J', source_file: 'app/public/build/assets/x.css', community: 1 },
];
const links = [
{ source: 'a', target: 'b' }, { source: 'a', target: 'c' }, { source: 'b', target: 'c' },
];
it('район tools имеет 3 узла, топ по степени, communities вторично', () => {
const map = buildDistrictMap(nodes, links, { topNodesPerDistrict: 2 });
const tools = map.find((d) => d.district === 'tools');
expect(tools.nodeCount).toBe(3);
expect(tools.topNodes[0].id).toBe('a'); // степень 2 (наибольшая, lexically первый при равенстве)
expect(tools.topNodes).toHaveLength(2); // cap
expect(tools.communities).toEqual([5, 7]); // вторичный сигнал, non-null, отсортирован
});
it('junk-узлы (build) НЕ попадают в карту', () => {
const map = buildDistrictMap(nodes, links, {});
expect(map.find((d) => d.district === 'app-frontend' || d.district === 'app-backend')).toBeUndefined();
expect(map.every((d) => d.district !== 'other' || d.nodeCount > 0)).toBe(true);
});
it('район docs-observer: 1 узел, communities пуст (все null)', () => {
const map = buildDistrictMap(nodes, links, {});
const obs = map.find((d) => d.district === 'docs-observer');
expect(obs.nodeCount).toBe(1);
expect(obs.communities).toEqual([]);
});
});
import { districtDetail } from './project-graph.mjs';
describe('districtDetail (layer-1)', () => {
const nodes = [
{ id: 'a', label: 'A', file_type: 'code', source_file: 'tools/router-engine.mjs', community: 5 },
{ id: 'b', label: 'B', file_type: 'code', source_file: 'tools/judge-engine.mjs', community: 5 },
{ id: 'c', label: 'C', file_type: 'code', source_file: 'tools/escape-grant.mjs', community: 7 },
{ id: 'd', label: 'D', file_type: 'document', source_file: 'docs/observer/STATUS.md', community: null },
];
it('возвращает узлы района + группировку по community', () => {
const r = districtDetail(nodes, 'tools', {});
expect(r.district).toBe('tools');
expect(r.nodes.map((n) => n.id).sort()).toEqual(['a', 'b', 'c']);
expect(r.byCommunity['5'].sort()).toEqual(['a', 'b']);
expect(r.byCommunity['7']).toEqual(['c']);
});
it('пустой район → пустые', () => {
const r = districtDetail(nodes, 'nope', {});
expect(r.nodes).toEqual([]);
expect(r.byCommunity).toEqual({});
});
it('узлы с community:null группируются под "none"', () => {
const r = districtDetail(nodes, 'docs-observer', {});
expect(r.byCommunity.none).toEqual(['d']);
});
});
import { readStaleness, gatherStalenessInputs } from './project-graph.mjs';
describe('readStaleness (✅O16 pure)', () => {
it('needs_update присутствует → stale true', () => {
const r = readStaleness({ needsUpdatePresent: true, graphCommit: 'aaa', headCommit: 'aaa', commitsBehind: 0, uncommittedCount: 0 });
expect(r).toEqual({ stale: true, commits_behind: 0, uncommitted: 0 });
});
it('commit рассинхрон → stale true даже без needs_update', () => {
const r = readStaleness({ needsUpdatePresent: false, graphCommit: 'aaa', headCommit: 'bbb', commitsBehind: 12, uncommittedCount: 3 });
expect(r).toEqual({ stale: true, commits_behind: 12, uncommitted: 3 });
});
it('всё чисто → stale false', () => {
const r = readStaleness({ needsUpdatePresent: false, graphCommit: 'aaa', headCommit: 'aaa', commitsBehind: 0, uncommittedCount: 0 });
expect(r).toEqual({ stale: false, commits_behind: 0, uncommitted: 0 });
});
it('кривые числа → 0 (не NaN)', () => {
const r = readStaleness({ needsUpdatePresent: true, commitsBehind: 'x', uncommittedCount: undefined });
expect(r.commits_behind).toBe(0);
expect(r.uncommitted).toBe(0);
});
it('commits_behind=null сохраняется (SE11: устарел, величина неизвестна)', () => {
const r = readStaleness({ needsUpdatePresent: true, commitsBehind: null, uncommittedCount: 2 });
expect(r.stale).toBe(true);
expect(r.commits_behind).toBe(null);
expect(r.uncommitted).toBe(2);
});
});
describe('gatherStalenessInputs (обёртка через моки)', () => {
it('собирает входы из инъектированных fs/git', () => {
const readFileImpl = (p) => p.endsWith('needs_update') ? 'Run /graphify --update (4 changed)' : 'Built from commit: `81cbd8c1`';
const existsImpl = () => true;
const gitImpl = (args) => args.includes('rev-list') ? '7' : ' M a.mjs\n?? b.mjs';
const inp = gatherStalenessInputs({ readFileImpl, existsImpl, gitImpl, headCommit: 'deadbeef' });
expect(inp.needsUpdatePresent).toBe(true);
expect(inp.graphCommit).toBe('81cbd8c1');
expect(inp.commitsBehind).toBe(7);
expect(inp.uncommittedCount).toBe(2);
});
it('git rev-list бросил (граф-commit вне истории) → commitsBehind null (SE11)', () => {
const readFileImpl = () => 'Built from commit: `81cbd8c1`';
const existsImpl = () => false;
const gitImpl = (args) => { if (args.includes('rev-list')) throw new Error('bad revision'); return ''; };
const inp = gatherStalenessInputs({ readFileImpl, existsImpl, gitImpl, headCommit: 'deadbeef' });
expect(inp.commitsBehind).toBe(null);
});
});
import { buildGraphSection } from './project-graph.mjs';
describe('buildGraphSection (CD-R6-G)', () => {
const districtMap = [
{ district: 'tools', nodeCount: 3, topNodes: [{ id: 'a', label: 'A', degree: 2 }], communities: [5, 7] },
{ district: 'docs-observer', nodeCount: 1, topNodes: [], communities: [] },
];
const staleness = { stale: true, commits_behind: 12, uncommitted: 3 };
it('секция содержит kind, районы (layer-0) и inline staleness', () => {
const s = buildGraphSection({ districtMap, staleness });
expect(s.kind).toBe('project-graph');
expect(s.layer0).toBe(districtMap);
expect(s.staleness).toEqual(staleness);
expect(s.districtCount).toBe(2);
});
it('пустая карта → districtCount 0, staleness сохранён', () => {
const s = buildGraphSection({ districtMap: [], staleness });
expect(s.districtCount).toBe(0);
expect(s.staleness.stale).toBe(true);
});
it('отсутствующий staleness → безопасный дефолт stale:true (fail-safe сигнал)', () => {
const s = buildGraphSection({ districtMap: [] });
expect(s.staleness.stale).toBe(true); // нет сигнала → считаем сталым (§5.4 сигнал ВСЕГДА)
});
it('commits_behind=null сохраняется в секции (SE-B1: SE11 не схлопывается в 0)', () => {
const s = buildGraphSection({ districtMap: [], staleness: { stale: true, commits_behind: null, uncommitted: 2 } });
expect(s.staleness.commits_behind).toBe(null);
expect(s.staleness.uncommitted).toBe(2);
});
});
describe('readStaleness — SE-B3 (нечитаемый граф → консервативно stale)', () => {
it('graphCommit пуст (не прочитан) при наличии headCommit → stale:true', () => {
const r = readStaleness({ needsUpdatePresent: false, graphCommit: '', headCommit: 'deadbeef', commitsBehind: null, uncommittedCount: 0 });
expect(r.stale).toBe(true);
});
it('headCommit пуст → stale:true (свежесть не подтверждена)', () => {
const r = readStaleness({ needsUpdatePresent: false, graphCommit: 'aaa', headCommit: '', commitsBehind: 0, uncommittedCount: 0 });
expect(r.stale).toBe(true);
});
});