import { describe, expect, it } from 'vitest'; import { buildTunnelsGraph, buildAliasesGraph, buildStackTopologyGraph, meshNodeStateEqual, miniMapColorFor, stacksKey, MINIMAP_BRAND, MINIMAP_WARNING, MINIMAP_MUTED, MINIMAP_DESTRUCTIVE, } from './mesh-topology-layout'; import type { MeshAlias, MeshNodeStatus } from '@/types/mesh'; function makeNode(over: Partial & Pick): MeshNodeStatus { return { nodeId: over.nodeId, nodeName: over.nodeName, enabled: over.enabled ?? false, localForwarderListening: over.localForwarderListening ?? null, pilotConnected: over.pilotConnected ?? false, reachableMode: over.reachableMode ?? 'unreachable', reachableReason: over.reachableReason ?? null, reverseCallbackStatus: over.reverseCallbackStatus ?? 'not_applicable', optedInStacks: over.optedInStacks ?? [], activeStreamCount: over.activeStreamCount ?? 0, }; } function makeAlias(over: Partial & Pick): MeshAlias { return { host: over.host, nodeId: over.nodeId, nodeName: over.nodeName ?? `node-${over.nodeId}`, stackName: over.stackName ?? 'stack-a', serviceName: over.serviceName ?? 'svc', port: over.port ?? 80, }; } function entries(...names: string[]): MeshNodeStatus['optedInStacks'] { return names.map((stackName) => ({ stackName, currentlyResolvable: true })); } describe('stacksKey', () => { it('returns the same key for the same membership regardless of order', () => { expect(stacksKey(entries('a', 'b', 'c'))).toBe(stacksKey(entries('c', 'a', 'b'))); }); it('differs when membership differs even with the same count', () => { expect(stacksKey(entries('a', 'b'))).not.toBe(stacksKey(entries('a', 'c'))); }); it('returns empty string for empty list', () => { expect(stacksKey([])).toBe(''); }); it('differs when a stack flips currentlyResolvable', () => { const a = [{ stackName: 'svc', currentlyResolvable: true }]; const b = [{ stackName: 'svc', currentlyResolvable: false }]; expect(stacksKey(a)).not.toBe(stacksKey(b)); }); }); describe('meshNodeStateEqual', () => { const base = makeNode({ nodeId: 1, nodeName: 'n', reachableMode: 'pilot', enabled: true, pilotConnected: true, optedInStacks: entries('a', 'b') }); it('returns true when scalar fields and stack membership match', () => { const a = { ...base }; const b = { ...base, optedInStacks: entries('b', 'a') }; expect(meshNodeStateEqual(a, b)).toBe(true); }); it('detects a stack swap that preserves the count', () => { const a = { ...base }; const b = { ...base, optedInStacks: entries('a', 'c') }; expect(meshNodeStateEqual(a, b)).toBe(false); }); it('detects a resolvability flip on a stack that otherwise stays identical', () => { const a = { ...base }; const b = { ...base, optedInStacks: [ { stackName: 'a', currentlyResolvable: false }, { stackName: 'b', currentlyResolvable: true }, ], }; expect(meshNodeStateEqual(a, b)).toBe(false); }); it('detects a reachable-mode flip', () => { const a = { ...base }; const b = { ...base, reachableMode: 'unreachable' as const }; expect(meshNodeStateEqual(a, b)).toBe(false); }); it('detects a pilot disconnect', () => { const a = { ...base }; const b = { ...base, pilotConnected: false }; expect(meshNodeStateEqual(a, b)).toBe(false); }); }); describe('miniMapColorFor', () => { it('returns destructive for unreachable', () => { const node = makeNode({ nodeId: 1, nodeName: 'n', reachableMode: 'unreachable' }); expect(miniMapColorFor(node)).toBe(MINIMAP_DESTRUCTIVE); }); it('returns warning for pilot that is not connected', () => { const node = makeNode({ nodeId: 1, nodeName: 'n', reachableMode: 'pilot', enabled: true, pilotConnected: false }); expect(miniMapColorFor(node)).toBe(MINIMAP_WARNING); }); it('returns muted for an enabled-false pilot that is connected', () => { const node = makeNode({ nodeId: 1, nodeName: 'n', reachableMode: 'pilot', enabled: false, pilotConnected: true }); expect(miniMapColorFor(node)).toBe(MINIMAP_MUTED); }); it('returns brand for a healthy meshed remote', () => { const node = makeNode({ nodeId: 1, nodeName: 'n', reachableMode: 'pilot', enabled: true, pilotConnected: true }); expect(miniMapColorFor(node)).toBe(MINIMAP_BRAND); }); it('returns muted when node is undefined', () => { expect(miniMapColorFor(undefined)).toBe(MINIMAP_MUTED); }); }); describe('buildTunnelsGraph', () => { it('returns empty result when no nodes', () => { const result = buildTunnelsGraph([]); expect(result.nodes).toHaveLength(0); expect(result.edges).toHaveLength(0); }); it('returns nodes but no edges when only the local node is present', () => { const result = buildTunnelsGraph([ makeNode({ nodeId: 1, nodeName: 'local', reachableMode: 'local', enabled: true }), ]); expect(result.nodes).toHaveLength(1); expect(result.edges).toHaveLength(0); }); it('returns one edge from local to every other node', () => { const result = buildTunnelsGraph([ makeNode({ nodeId: 1, nodeName: 'local', reachableMode: 'local', enabled: true }), makeNode({ nodeId: 2, nodeName: 'peer-a', reachableMode: 'pilot', enabled: true, pilotConnected: true }), makeNode({ nodeId: 3, nodeName: 'peer-b', reachableMode: 'proxy', enabled: true }), ]); expect(result.nodes).toHaveLength(3); expect(result.edges).toHaveLength(2); expect(result.edges.every((e) => e.source === '1')).toBe(true); }); it('labels pilot edges with state suffixes', () => { const result = buildTunnelsGraph([ makeNode({ nodeId: 1, nodeName: 'local', reachableMode: 'local', enabled: true }), makeNode({ nodeId: 2, nodeName: 'on', reachableMode: 'pilot', enabled: true, pilotConnected: true }), makeNode({ nodeId: 3, nodeName: 'off', reachableMode: 'pilot', enabled: true, pilotConnected: false }), ]); const onEdge = result.edges.find((e) => e.target === '2'); const offEdge = result.edges.find((e) => e.target === '3'); expect(onEdge?.label).toBe('pilot · ok'); expect(offEdge?.label).toBe('pilot · idle'); }); it('dashes unreachable edges', () => { const result = buildTunnelsGraph([ makeNode({ nodeId: 1, nodeName: 'local', reachableMode: 'local', enabled: true }), makeNode({ nodeId: 2, nodeName: 'down', reachableMode: 'unreachable', enabled: false, reachableReason: 'auth failed' }), ]); const edge = result.edges[0]; expect(edge).toBeDefined(); expect((edge?.style as { strokeDasharray?: string } | undefined)?.strokeDasharray).toBe('4 4'); expect(edge?.label).toBe('unreachable'); }); }); describe('buildAliasesGraph', () => { const status: MeshNodeStatus[] = [ makeNode({ nodeId: 1, nodeName: 'local', reachableMode: 'local', enabled: true }), makeNode({ nodeId: 2, nodeName: 'peer-a', reachableMode: 'pilot', enabled: true, pilotConnected: true }), makeNode({ nodeId: 3, nodeName: 'peer-b', reachableMode: 'proxy', enabled: true }), ]; it('produces one edge per remote node like the tunnels graph', () => { const aliases = [ makeAlias({ host: 'a.mesh', nodeId: 2 }), makeAlias({ host: 'b.mesh', nodeId: 2 }), makeAlias({ host: 'c.mesh', nodeId: 3 }), ]; const result = buildAliasesGraph(status, aliases); expect(result.edges).toHaveLength(2); const labels = result.edges.map((e) => e.label); expect(labels).toContain('2 aliases'); expect(labels).toContain('1 alias'); }); it('labels edges with "no aliases" when remote owns none', () => { const result = buildAliasesGraph(status, [makeAlias({ host: 'a.mesh', nodeId: 2 })]); const noAliasEdge = result.edges.find((e) => e.target === '3'); expect(noAliasEdge?.label).toBe('no aliases'); }); it('encodes aliasCount on node data when in aliases mode', () => { const aliases = [ makeAlias({ host: 'a.mesh', nodeId: 2 }), makeAlias({ host: 'b.mesh', nodeId: 2 }), ]; const result = buildAliasesGraph(status, aliases); const peerA = result.nodes.find((n) => n.id === '2'); const data = peerA?.data as { aliasCount: number } | undefined; expect(data?.aliasCount).toBe(2); }); }); describe('buildStackTopologyGraph', () => { const status: MeshNodeStatus[] = [ makeNode({ nodeId: 1, nodeName: 'local', reachableMode: 'local', enabled: true }), makeNode({ nodeId: 2, nodeName: 'peer-a', reachableMode: 'pilot', enabled: true, pilotConnected: true }), makeNode({ nodeId: 3, nodeName: 'peer-b', reachableMode: 'proxy', enabled: true }), ]; it('returns empty when stack publishes nothing and no consumers', () => { const result = buildStackTopologyGraph({ nodeId: 1, stackName: 'empty', status: [makeNode({ nodeId: 1, nodeName: 'local', reachableMode: 'local', enabled: true })], aliases: [], }); expect(result.nodes).toHaveLength(0); expect(result.edges).toHaveLength(0); }); it('builds a center + alias + consumer fanout for a stack with 2 aliases and 2 consumers', () => { const aliases = [ makeAlias({ host: 'a.mesh', nodeId: 1, stackName: 'api' }), makeAlias({ host: 'b.mesh', nodeId: 1, stackName: 'api' }), makeAlias({ host: 'other.mesh', nodeId: 1, stackName: 'other' }), ]; const result = buildStackTopologyGraph({ nodeId: 1, stackName: 'api', status, aliases, }); const centerNodes = result.nodes.filter((n) => n.type === 'stackCenter'); const aliasNodes = result.nodes.filter((n) => n.type === 'stackAlias'); const consumerNodes = result.nodes.filter((n) => n.type === 'stackConsumer'); expect(centerNodes).toHaveLength(1); expect(aliasNodes).toHaveLength(2); expect(consumerNodes).toHaveLength(2); // 2 center→alias edges + 2 aliases × 2 consumers = 4 alias→consumer edges = 6 total. expect(result.edges).toHaveLength(6); }); it('filters aliases by both nodeId and stackName', () => { const aliases = [ makeAlias({ host: 'a.mesh', nodeId: 1, stackName: 'api' }), makeAlias({ host: 'wrong-stack.mesh', nodeId: 1, stackName: 'other' }), makeAlias({ host: 'wrong-node.mesh', nodeId: 2, stackName: 'api' }), ]; const result = buildStackTopologyGraph({ nodeId: 1, stackName: 'api', status, aliases, }); const aliasNodes = result.nodes.filter((n) => n.type === 'stackAlias'); expect(aliasNodes).toHaveLength(1); expect(aliasNodes[0]?.id).toBe('alias-1-api-a.mesh'); }); it('only treats meshed (enabled) remote nodes as consumers', () => { const aliases = [makeAlias({ host: 'a.mesh', nodeId: 1, stackName: 'api' })]; const mixedStatus = [ makeNode({ nodeId: 1, nodeName: 'local', reachableMode: 'local', enabled: true }), makeNode({ nodeId: 2, nodeName: 'meshed', reachableMode: 'pilot', enabled: true, pilotConnected: true }), makeNode({ nodeId: 3, nodeName: 'unmeshed', reachableMode: 'pilot', enabled: false }), ]; const result = buildStackTopologyGraph({ nodeId: 1, stackName: 'api', status: mixedStatus, aliases, }); const consumerNodes = result.nodes.filter((n) => n.type === 'stackConsumer'); expect(consumerNodes).toHaveLength(1); expect(consumerNodes[0]?.id).toBe('consumer-1-api-2'); }); });