fix(mesh): reject opt-in when every service has empty ports (#1056)

optInStack rejected empty service lists but not the case where services
exist with every ports: []. The newPorts Set ended up empty, the
function sailed past collision checks, wrote a mesh_stacks row, and
produced an empty-aliases override. The stack then appeared online in
the Routing tab but no traffic actually routed.

Add a check after building newPorts: throw no_target with a clear
message before the SENCHO_LISTEN_PORT collision check. Reuses the
existing MeshError code (semantically: no routable target) so the
frontend's error toast path is unchanged.
This commit is contained in:
Anso
2026-05-15 09:00:24 -04:00
committed by GitHub
parent 6185206323
commit 3d5f0ffacd
2 changed files with 26 additions and 0 deletions
@@ -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<unknown> }, 'inspectStackServices')
.mockResolvedValue([
{ service: 'web', ports: [] },
{ service: 'db', ports: [] },
]);
vi.spyOn(svc as unknown as { regenerateOverridesForNode: (n: number) => Promise<void> }, '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<unknown> }, 'inspectStackServices')
+6
View File
@@ -565,6 +565,12 @@ export class MeshService extends EventEmitter implements MeshForwarderHost {
const newPorts = new Set<number>();
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',