diff --git a/backend/src/__tests__/mesh-service.test.ts b/backend/src/__tests__/mesh-service.test.ts index c887c017..fe120c26 100644 --- a/backend/src/__tests__/mesh-service.test.ts +++ b/backend/src/__tests__/mesh-service.test.ts @@ -70,6 +70,26 @@ describe('MeshService.optInStack', () => { .rejects.toThrow(/port 5432 is already claimed/); }); + it('rejects opt-in when every service declared empty ports (no routable target)', async () => { + const svc = MeshService.getInstance(); + const db = DatabaseService.getInstance(); + const localNodeId = db.getNodes()[0].id; + + vi.spyOn(svc as unknown as { inspectStackServices: (n: number, s: string) => Promise }, 'inspectStackServices') + .mockResolvedValue([ + { service: 'web', ports: [] }, + { service: 'db', ports: [] }, + ]); + vi.spyOn(svc as unknown as { regenerateOverridesForNode: (n: number) => Promise }, 'regenerateOverridesForNode') + .mockResolvedValue(undefined); + + await expect(svc.optInStack(localNodeId, 'silent', 'tester')) + .rejects.toThrow(/no service ports to mesh/); + // Pre-fix this wrote a row with no aliases and the stack appeared + // "online but doesn't route". Verify the row was not written. + expect(db.isMeshStackEnabled(localNodeId, 'silent')).toBe(false); + }); + it('opt-out removes the row and the override', async () => { const svc = MeshService.getInstance(); vi.spyOn(svc as unknown as { inspectStackServices: (n: number, s: string) => Promise }, 'inspectStackServices') diff --git a/backend/src/services/MeshService.ts b/backend/src/services/MeshService.ts index 4fb77ba8..52cf814c 100644 --- a/backend/src/services/MeshService.ts +++ b/backend/src/services/MeshService.ts @@ -565,6 +565,12 @@ export class MeshService extends EventEmitter implements MeshForwarderHost { const newPorts = new Set(); for (const svc of services) for (const p of svc.ports) newPorts.add(p); + if (newPorts.size === 0) { + throw new MeshError( + 'no_target', + `stack ${stackName} has no service ports to mesh (every service declared ports: [])`, + ); + } if (newPorts.has(SENCHO_LISTEN_PORT)) { throw new MeshError( 'port_collision',