import { describe, it, expect } from 'vitest'; import { buildNodeGraph, resolveNode, twinsOf, hintLinksOf, conflictsOf, checkGraphFreshness, } from './node-graph.mjs'; const REG = { nodes: [ { id: '#19', name: 'Superpowers', slug: 'superpowers', subcategory: null, status: 'active' }, { id: '#36', name: 'adr-kit', slug: 'adr-kit', subcategory: 'architecture-tooling', status: 'active' }, { id: '#37', name: 'mermaid-skill', slug: 'mermaid', subcategory: 'architecture-tooling', status: 'active' }, { id: '#38', name: 'architecture-patterns', slug: 'architecture-patterns', subcategory: 'architecture-tooling', status: 'active' }, { id: '#17', name: 'pg_partman', slug: 'pg-partman', subcategory: null, status: 'dormant' }, ], chains: { L4: { name: 'diagram', sequence: ['#36', '#37'] }, }, }; describe('buildNodeGraph', () => { it('builds id/slug/name indexes + node count', () => { const g = buildNodeGraph(REG); expect(g.nodes).toHaveLength(5); expect(g.byId.get('#36').name).toBe('adr-kit'); expect(g.bySlug.get('mermaid').id).toBe('#37'); }); it('accepts a plain registry literal (not loadRegistry-bound)', () => { const g = buildNodeGraph({ nodes: [{ id: '#1', slug: 'x', name: 'X' }], chains: {} }); expect(g.bySlug.get('x').id).toBe('#1'); }); }); describe('resolveNode (ОВ-Д2 — механическое заземление)', () => { const g = buildNodeGraph(REG); it('resolves by exact id', () => { expect(resolveNode(g, '#36').slug).toBe('adr-kit'); }); it('resolves by exact slug', () => { expect(resolveNode(g, 'mermaid').id).toBe('#37'); }); it('resolves by exact name', () => { expect(resolveNode(g, 'adr-kit').id).toBe('#36'); }); it('resolves by suffix after colon (skill-ref)', () => { expect(resolveNode(g, 'superpowers:architecture-patterns').id).toBe('#38'); }); it('фикс-1: резолв по ПРЕФИКСУ до ":" когда суффикс не узел (superpowers:writing-plans → #19)', () => { expect(resolveNode(g, 'superpowers:writing-plans').id).toBe('#19'); expect(resolveNode(g, 'superpowers:brainstorming').id).toBe('#19'); expect(resolveNode(g, 'superpowers:subagent-driven-development').id).toBe('#19'); }); it('invented skill → null (выдумка отклоняется)', () => { expect(resolveNode(g, 'elasticsearch-mcp')).toBe(null); expect(resolveNode(g, '#999')).toBe(null); }); it('empty/garbage → null', () => { expect(resolveNode(g, '')).toBe(null); expect(resolveNode(g, null)).toBe(null); }); }); describe('twinsOf (близнецы = общий subcategory, активные)', () => { const g = buildNodeGraph(REG); it('возвращает соседей по subcategory без себя', () => { const t = twinsOf(g, '#36').map((n) => n.id).sort(); expect(t).toEqual(['#37', '#38']); }); it('узел без subcategory → нет близнецов', () => { expect(twinsOf(g, '#19')).toEqual([]); }); it('несуществующий ref → []', () => { expect(twinsOf(g, 'nope')).toEqual([]); }); }); describe('hintLinksOf (связи = со-членство в цепочке)', () => { const g = buildNodeGraph(REG); it('соседи по chains, без себя', () => { expect(hintLinksOf(g, '#36').map((n) => n.id)).toEqual(['#37']); expect(hintLinksOf(g, '#37').map((n) => n.id)).toEqual(['#36']); }); it('узел вне цепочек → []', () => { expect(hintLinksOf(g, '#19')).toEqual([]); }); }); describe('conflictsOf (явные attributes.conflicts_with)', () => { it('резолвит явные конфликт-рёбра', () => { const reg = { nodes: [ { id: '#a', slug: 'a', status: 'active', attributes: { conflicts_with: ['#b'] } }, { id: '#b', slug: 'b', status: 'active' }, ], chains: {} }; const g = buildNodeGraph(reg); expect(conflictsOf(g, '#a').map((n) => n.id)).toEqual(['#b']); }); it('нет поля → []', () => { const g = buildNodeGraph(REG); expect(conflictsOf(g, '#36')).toEqual([]); }); }); describe('checkGraphFreshness (3.6)', () => { it('реестр не новее сборки → fresh', () => { expect(checkGraphFreshness({ registryMtimeMs: 100, builtAtMs: 200 })).toMatchObject({ fresh: true, stale: false }); }); it('реестр новее сборки → stale + причина', () => { const r = checkGraphFreshness({ registryMtimeMs: 300, builtAtMs: 200 }); expect(r.stale).toBe(true); expect(r.fresh).toBe(false); expect(r.reason).toMatch(/устар|реестр новее|stale/i); }); it('нет данных о времени сборки → stale (не доверяем уверенно)', () => { expect(checkGraphFreshness({ registryMtimeMs: 100, builtAtMs: null }).stale).toBe(true); }); });