Pulse/internal/servicediscovery/service_identity_test.go
rcourtman ac1628f9b4 Add distinctive-port fast-path for un-named workloads
The name fast-path identifies workloads named after their service, but a
workload with a generic name (ct101, db1) still fell through to the model —
which, with a slow reasoning model configured, times out rather than just
being slow. Many such workloads are still identifiable instantly by a
DISTINCTIVE listening port.

Add a Ports field to the identity table (only single-service ports: 8123
HA, 32400 Plex, 5432 postgres, 6379 redis, 1883/8883 mosquitto, 3306
mariadb, 8086 influxdb, 8096 jellyfin, 9090 prometheus, 6052 esphome) and
a second fast-path inferSurfaceIdentityFromPorts that runs after the name
path and before the model. Ambiguous ports (80, 443, 8080, 3000, 5000) are
deliberately excluded — a bad port guess is worse than asking the model.

Parser validated against real ss -tlnp output from LXC 101 (extracts 8123,
ignores ephemeral ports / PIDs / docker-proxy noise → home-assistant).
Unit tests cover distinctive-port matches and ambiguous-only non-matches.
2026-06-07 21:52:41 +01:00

124 lines
4.6 KiB
Go

package servicediscovery
import (
"strings"
"testing"
)
func TestInferSurfaceIdentity(t *testing.T) {
cases := []struct {
name string
hostname string
wantType string
wantOK bool
}{
{"home assistant by hostname", "home-assistant", "home-assistant", true},
{"homeassistant one word", "homeassistant", "home-assistant", true},
{"esphome by hostname", "esphome", "esphome", true},
{"frigate by hostname", "frigate", "frigate", true},
{"mqtt maps to mosquitto", "mqtt", "mosquitto", true},
{"plex within a longer name", "plex-media", "plex", true},
{"postgres by hostname", "postgres-primary", "postgresql", true},
{"generic name does not match", "ct-200", "", false},
{"numeric id does not match", "101", "", false},
}
for _, tc := range cases {
tc := tc
t.Run(tc.name, func(t *testing.T) {
identity, _, ok := inferSurfaceIdentity(DiscoveryRequest{Hostname: tc.hostname}, nil)
if ok != tc.wantOK {
t.Fatalf("inferSurfaceIdentity(%q) ok=%v, want %v", tc.hostname, ok, tc.wantOK)
}
if ok && identity.ServiceType != tc.wantType {
t.Fatalf("inferSurfaceIdentity(%q) type=%q, want %q", tc.hostname, identity.ServiceType, tc.wantType)
}
})
}
}
func TestSurfaceIdentityResponseIsIdentityOnly(t *testing.T) {
identity, evidence, ok := inferSurfaceIdentity(DiscoveryRequest{Hostname: "home-assistant"}, nil)
if !ok {
t.Fatal("expected home-assistant to match by name")
}
resp := surfaceIdentityResponse(identity, evidence)
if resp.ServiceType != "home-assistant" || resp.ServiceName != "Home Assistant" {
t.Fatalf("unexpected identity: type=%q name=%q", resp.ServiceType, resp.ServiceName)
}
if resp.Confidence < 0.85 {
t.Fatalf("expected a confident identity, got %v", resp.Confidence)
}
// Surface = identity only. Paths/facts are intentionally empty — the
// Assistant knows standard layouts and fetches specifics on demand.
if len(resp.ConfigPaths) != 0 || len(resp.DataPaths) != 0 || len(resp.LogPaths) != 0 || len(resp.Facts) != 0 {
t.Fatalf("surface response must carry no deep paths/facts, got %+v", resp)
}
}
func TestNestedContainerForService(t *testing.T) {
// Mirrors the real HA-in-LXC topology on delly: homeassistant + watchtower.
haProbe := "homeassistant|ghcr.io/home-assistant/home-assistant:stable\nwatchtower|containrrr/watchtower"
cases := []struct {
name string
probe string
serviceType string
serviceName string
want string
}{
{"HA matches by container name", haProbe, "home-assistant", "Home Assistant", "homeassistant"},
{"postgres matches by image", "db|postgres:16-alpine", "postgresql", "PostgreSQL", "db"},
{"unrelated service does not match", haProbe, "redis", "Redis", ""},
{"no nested containers", "", "home-assistant", "Home Assistant", ""},
{"watchtower alone is not the service", "watchtower|containrrr/watchtower", "home-assistant", "Home Assistant", ""},
}
for _, tc := range cases {
tc := tc
t.Run(tc.name, func(t *testing.T) {
got := nestedContainerForService(tc.probe, tc.serviceType, tc.serviceName)
if got != tc.want {
t.Fatalf("nestedContainerForService = %q, want %q", got, tc.want)
}
})
}
}
func TestWithNestedDockerAccess(t *testing.T) {
base := "Use pulse_control with target_host matching this container's hostname."
out := withNestedDockerAccess(base, "homeassistant")
if !strings.Contains(out, base) {
t.Fatalf("expected base guidance preserved, got %q", out)
}
if !strings.Contains(out, "docker exec homeassistant") {
t.Fatalf("expected layered docker exec path, got %q", out)
}
}
func TestInferSurfaceIdentityFromPorts(t *testing.T) {
haSS := "State Recv-Q Send-Q Local-Address:Port Peer:Port Process\n" +
"LISTEN 0 4096 0.0.0.0:8123 0.0.0.0:* users:((\"python3\",pid=1234,fd=10))\n" +
"LISTEN 0 128 127.0.0.1:22 0.0.0.0:*"
cases := []struct {
name string
output string
wantType string
wantOK bool
}{
{"HA by distinctive port 8123", haSS, "home-assistant", true},
{"postgres by 5432", "LISTEN 0 244 0.0.0.0:5432 0.0.0.0:*", "postgresql", true},
{"plex by 32400", "LISTEN 0 128 *:32400 *:*", "plex", true},
{"ambiguous ports only do not match", "LISTEN 0 128 0.0.0.0:80\nLISTEN 0 128 0.0.0.0:443\nLISTEN 0 128 0.0.0.0:22", "", false},
{"empty output", "", "", false},
}
for _, tc := range cases {
tc := tc
t.Run(tc.name, func(t *testing.T) {
id, _, ok := inferSurfaceIdentityFromPorts(tc.output)
if ok != tc.wantOK {
t.Fatalf("inferSurfaceIdentityFromPorts ok=%v, want %v", ok, tc.wantOK)
}
if ok && id.ServiceType != tc.wantType {
t.Fatalf("inferSurfaceIdentityFromPorts type=%q, want %q", id.ServiceType, tc.wantType)
}
})
}
}