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

248 lines
12 KiB
JavaScript
Raw Permalink 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/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);
});
});