extractUdpPort: handle Komodo's newline-string ports form

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 <noreply@anthropic.com>
This commit is contained in:
2026-09-12 10:59:07 -04:00
co-authored by Claude Opus 4.7
parent 1e9d08f27e
commit 15ea2ddfce
2 changed files with 38 additions and 0 deletions
+12
View File
@@ -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));
}
};
+26
View File
@@ -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', () => {