From fe6ada46c2bae7f8bc5cc22f9bf98de070ed0e23 Mon Sep 17 00:00:00 2001 From: Molecule AI Core-BE Date: Tue, 12 May 2026 17:47:12 +0000 Subject: [PATCH] fix(handlers/discovery): nil-guard role in filterPeersByQuery (mc#731) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit queryPeerMaps sets peer["role"] = nil when the DB role column is empty (discovery.go lines 337-341). filterPeersByQuery did a bare type assertion p["role"].(string) which panics on nil. Fix: use the comma-ok form so nil → "" (empty string) — both name and role fields now use x, _ := p["key"].(string) rather than x := p["key"].(string). Add TestFilterPeersByQuery_NilRoleRegression with three cases: - nil role matches on name substring - nil name/role with empty q (no-op, returns all) - all nil — no panic, returns empty Regression gate for mc#730/#731. Co-Authored-By: Claude Opus 4.7 --- .../internal/handlers/discovery.go | 4 +- .../internal/handlers/discovery_test.go | 74 +++++++++++++++++++ 2 files changed, 76 insertions(+), 2 deletions(-) diff --git a/workspace-server/internal/handlers/discovery.go b/workspace-server/internal/handlers/discovery.go index 79315016..5c798c81 100644 --- a/workspace-server/internal/handlers/discovery.go +++ b/workspace-server/internal/handlers/discovery.go @@ -292,8 +292,8 @@ func filterPeersByQuery(peers []map[string]interface{}, q string) []map[string]i needle := strings.ToLower(q) out := make([]map[string]interface{}, 0, len(peers)) for _, p := range peers { - name := p["name"].(string) - role := p["role"].(string) + name, _ := p["name"].(string) // nil → "" — safe on empty-role rows + role, _ := p["role"].(string) // nil → "" — queryPeerMaps sets nil when DB role is empty if strings.Contains(strings.ToLower(name), needle) || strings.Contains(strings.ToLower(role), needle) { out = append(out, p) diff --git a/workspace-server/internal/handlers/discovery_test.go b/workspace-server/internal/handlers/discovery_test.go index 892a1f0a..3070fcf4 100644 --- a/workspace-server/internal/handlers/discovery_test.go +++ b/workspace-server/internal/handlers/discovery_test.go @@ -394,6 +394,80 @@ func TestPeers_Q_NoMatches_RawBodyIsArrayNotNull(t *testing.T) { } } +// TestFilterPeersByQuery_NilRoleRegression is the regression gate for +// mc#730/#731: queryPeerMaps sets peer["role"] = nil when the DB role column +// is empty (discovery.go lines 337-341). filterPeersByQuery did a bare +// type assertion p["role"].(string) which panics on nil. The fix uses the +// comma-ok form so nil → "". The test passes a map with nil name and nil +// role and asserts no panic + correct filter behaviour. +func TestFilterPeersByQuery_NilRoleRegression(t *testing.T) { + cases := []struct { + name string + peers []map[string]interface{} + q string + wantLen int + wantIDs []string + }{ + { + name: "nil role matches on name", + peers: []map[string]interface{}{ + {"id": "ws-a", "name": nil, "role": nil}, + {"id": "ws-b", "name": "Alpha Builder", "role": nil}, + {"id": "ws-c", "name": "Beta Builder", "role": nil}, + }, + q: "alpha", + wantLen: 1, + wantIDs: []string{"ws-b"}, + }, + { + name: "nil name matches on nil role (empty string)", + peers: []map[string]interface{}{ + {"id": "ws-x", "name": nil, "role": nil}, + {"id": "ws-y", "name": "Dev Workspace", "role": nil}, + }, + q: "", + wantLen: 2, // empty q is a no-op + wantIDs: []string{"ws-x", "ws-y"}, + }, + { + name: "all nil — no panic, returns input", + peers: []map[string]interface{}{ + {"id": "ws-z", "name": nil, "role": nil}, + }, + q: "anything", + wantLen: 0, + wantIDs: nil, + }, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + got := filterPeersByQuery(tc.peers, tc.q) + if len(got) != tc.wantLen { + t.Fatalf("len: got %d, want %d", len(got), tc.wantLen) + } + gotIDs := make([]string, len(got)) + for i, p := range got { + gotIDs[i] = p["id"].(string) + } + if tc.wantIDs != nil { + for _, id := range tc.wantIDs { + found := false + for _, g := range gotIDs { + if g == id { + found = true + break + } + } + if !found { + t.Errorf("missing id %q; got IDs: %v", id, gotIDs) + } + } + } + }) + } +} + func keysOf(m map[string]struct{}) []string { out := make([]string, 0, len(m)) for k := range m { -- 2.45.2