397777089e
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
103 lines
4.8 KiB
JavaScript
103 lines
4.8 KiB
JavaScript
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);
|
|
});
|
|
});
|