From 15ea2ddfce5d3f986395f1b61c65f3b5dad0980d Mon Sep 17 00:00:00 2001 From: josh Date: Sat, 12 Sep 2026 10:59:07 -0400 Subject: [PATCH] extractUdpPort: handle Komodo's newline-string ports form MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Real Komodo GetDeployment responses render config.ports as a single newline-delimited string ("28963:28963/udp\n28963:28963/tcp\n"), not as the array shapes we already handled. The walker only descended into arrays and objects, so the string was skipped entirely and every Komodo probe on a real deployment fell through to no-udp-in-response. When the walker hits a value under a ports-keyed field that's a string, split it on \r\n , ; and treat the resulting parts as a preferred string array — the existing "host:container/udp" and "container/udp" regexes handle each part. Fixture pulled verbatim from the t5-nacht-1 response the fleet UI surfaced. Also covers single-line, host-network (no host prefix), and TCP-only-shouldn't-match cases. Co-Authored-By: Claude Opus 4.7 --- src/lib/server/integrations/komodo.ts | 12 ++++++++++++ tests/unit/komodo.test.ts | 26 ++++++++++++++++++++++++++ 2 files changed, 38 insertions(+) diff --git a/src/lib/server/integrations/komodo.ts b/src/lib/server/integrations/komodo.ts index dce4670..400ad1a 100644 --- a/src/lib/server/integrations/komodo.ts +++ b/src/lib/server/integrations/komodo.ts @@ -301,6 +301,18 @@ export function extractUdpPort(payload: unknown): number | null { } } for (const [key, val] of Object.entries(obj)) { + // Komodo's GetDeployment renders `config.ports` as a single + // newline-delimited string ("28963:28963/udp\n28963:28963/tcp"). + // Synthesize an array from it and treat as a preferred candidate. + if (PORT_ARRAY_KEYS.has(key) && typeof val === 'string' && val.trim()) { + const parts = val + .split(/[\r\n,;]+/) + .map((s) => s.trim()) + .filter(Boolean); + if (parts.length > 0) { + candidates.push({ arr: parts, preferred: true }); + } + } walk(val, PORT_ARRAY_KEYS.has(key)); } }; diff --git a/tests/unit/komodo.test.ts b/tests/unit/komodo.test.ts index e7aa63e..9118352 100644 --- a/tests/unit/komodo.test.ts +++ b/tests/unit/komodo.test.ts @@ -81,6 +81,32 @@ describe('extractUdpPort', () => { }; expect(extractUdpPort(payload)).toBe(28968); }); + + it('splits Komodo`s newline-delimited string form of `config.ports`', () => { + // Verbatim shape from a real Komodo GetDeployment response — the + // container publishes UDP 28963 and TCP 28963 as a single string. + const payload = { + name: 't5-nacht-1', + config: { + image: { type: 'Image', params: { image: 'ghcr.io/ayymoss/plutainer:latest' } }, + network: 'komodo_default', + ports: '28963:28963/udp\n28963:28963/tcp\n' + } + }; + expect(extractUdpPort(payload)).toBe(28963); + }); + + it('handles a single-line string-form ports value', () => { + expect(extractUdpPort({ config: { ports: '28969:28960/udp' } })).toBe(28969); + }); + + it('handles a host-network string-form ports value (no host prefix)', () => { + expect(extractUdpPort({ config: { ports: '28970/udp\n' } })).toBe(28970); + }); + + it('ignores TCP-only string-form ports entries', () => { + expect(extractUdpPort({ config: { ports: '80:80/tcp\n443:443/tcp' } })).toBeNull(); + }); }); describe('komodoResolvePortDetailed', () => {