/** * Regression guard for the M-12 fix: `MeshService.getRouteDiagnostic` and * `MeshService.getStatus` must report `pilotConnected: true` for local nodes * instead of asking `PilotTunnelManager.hasActiveTunnel`, which always returns * false for local nodes (they do not have pilot tunnels because they do not * need them — local mesh traffic uses the same-node fast path). Without this * conditional, the route detail sheet renders every local alias as a * destructive `tunnel down` pill on a working route. */ import { afterAll, afterEach, beforeAll, describe, expect, it, vi } from 'vitest'; import { setupTestDb, cleanupTestDb } from './helpers/setupTestDb'; let tmpDir: string; let MeshService: typeof import('../services/MeshService').MeshService; let DatabaseService: typeof import('../services/DatabaseService').DatabaseService; let PilotTunnelManager: typeof import('../services/PilotTunnelManager').PilotTunnelManager; beforeAll(async () => { tmpDir = await setupTestDb(); ({ MeshService } = await import('../services/MeshService')); ({ DatabaseService } = await import('../services/DatabaseService')); ({ PilotTunnelManager } = await import('../services/PilotTunnelManager')); }); afterAll(() => { vi.restoreAllMocks(); cleanupTestDb(tmpDir); }); afterEach(() => { vi.restoreAllMocks(); const svc = MeshService.getInstance() as unknown as { aliasCache: Map; aliasByPort: Map; }; svc.aliasCache = new Map(); svc.aliasByPort = new Map(); }); describe('MeshService diagnostic — local-node pilot state (M-12)', () => { it('reports pilotConnected: true and state: healthy for a local alias even though no pilot tunnel exists', async () => { const svc = MeshService.getInstance(); const db = DatabaseService.getInstance(); const localNodeId = db.getNodes()[0].id; db.insertMeshStack(localNodeId, 'audit-mesh-prod', 'tester'); // hasActiveTunnel always returns false for local nodes; the helper // must short-circuit that, otherwise the diagnostic flips to // 'tunnel down'. vi.spyOn(PilotTunnelManager.getInstance(), 'hasActiveTunnel').mockReturnValue(false); // F-11: getRouteDiagnostic now probes synchronously. Stub testUpstream // so the unit test does not depend on a live TCP socket on port 9000. // The stub writes routeLatencyMap in lockstep with the real probe // success path so the state computation below resolves to 'healthy'. vi.spyOn(svc, 'testUpstream').mockImplementation(async (alias: string) => { (svc as unknown as { routeLatencyMap: Map }).routeLatencyMap.set(alias, 5); (svc as unknown as { routeProbeAtMap: Map }).routeProbeAtMap.set(alias, Date.now()); return { ok: true, latencyMs: 5 }; }); (svc as unknown as { aliasCache: Map }).aliasCache = new Map([ ['echo.audit-mesh-prod.local.sencho', { host: 'echo.audit-mesh-prod.local.sencho', nodeId: localNodeId, nodeName: 'local', stackName: 'audit-mesh-prod', serviceName: 'echo', port: 9000, }], ]); const diag = await svc.getRouteDiagnostic('echo.audit-mesh-prod.local.sencho'); expect(diag.pilot.connected).toBe(true); expect(diag.state).toBe('healthy'); db.deleteMeshStack(localNodeId, 'audit-mesh-prod'); }); it('reports pilotConnected: false when the alias points to a node that no longer exists', async () => { const svc = MeshService.getInstance(); const db = DatabaseService.getInstance(); // Create a remote node, register an alias pointing at it, opt the // stack in (so the alias is not 'not authorized'), then delete the // node row. The diagnostic should fall back to false rather than // throw or treat a missing node as reachable. const remoteNodeId = db.addNode({ name: 'm12-orphan', type: 'remote', mode: 'pilot_agent', compose_dir: '/tmp', is_default: false, api_url: '', api_token: '', }); db.insertMeshStack(remoteNodeId, 'audit-mesh-orphan', 'tester'); (svc as unknown as { aliasCache: Map }).aliasCache = new Map([ ['echo.audit-mesh-orphan.m12-orphan.sencho', { host: 'echo.audit-mesh-orphan.m12-orphan.sencho', nodeId: remoteNodeId, nodeName: 'm12-orphan', stackName: 'audit-mesh-orphan', serviceName: 'echo', port: 9002, }], ]); // Drop the node row but leave the alias cache + mesh_stacks intact // (mirrors a race where listMeshStacks ran before the node delete // cascade completed). The orphan path is already covered by the // initial !node check; this asserts the fallback is false rather // than the test-default true. db.deleteMeshStack(remoteNodeId, 'audit-mesh-orphan'); db.deleteNode(remoteNodeId); const diag = await svc.getRouteDiagnostic('echo.audit-mesh-orphan.m12-orphan.sencho'); expect(diag.pilot.connected).toBe(false); }); it('still reports pilotConnected: false and state: tunnel down for a remote alias when the tunnel is actually down', async () => { const svc = MeshService.getInstance(); const db = DatabaseService.getInstance(); const remoteNodeId = db.addNode({ name: 'm12-remote', type: 'remote', mode: 'pilot_agent', compose_dir: '/tmp', is_default: false, api_url: '', api_token: '', }); db.insertMeshStack(remoteNodeId, 'audit-mesh-pilot', 'tester'); vi.spyOn(PilotTunnelManager.getInstance(), 'hasActiveTunnel').mockReturnValue(false); (svc as unknown as { aliasCache: Map }).aliasCache = new Map([ ['echo.audit-mesh-pilot.m12-remote.sencho', { host: 'echo.audit-mesh-pilot.m12-remote.sencho', nodeId: remoteNodeId, nodeName: 'm12-remote', stackName: 'audit-mesh-pilot', serviceName: 'echo', port: 9001, }], ]); const diag = await svc.getRouteDiagnostic('echo.audit-mesh-pilot.m12-remote.sencho'); expect(diag.pilot.connected).toBe(false); expect(diag.state).toBe('tunnel down'); db.deleteMeshStack(remoteNodeId, 'audit-mesh-pilot'); db.deleteNode(remoteNodeId); }); });