From f30a963329eb0e4627ed2a8bce9c979dd76232b2 Mon Sep 17 00:00:00 2001 From: vikingowl Date: Tue, 28 Apr 2026 13:09:26 +0200 Subject: [PATCH] fix(market): marshal empty merge-plan buckets as [] not null Nil slices in MergePlan.AutoApply/ReviewRequired/Rejected serialized to JSON null, causing the admin research panel to crash with "can't access property 'map', plan.review_required is null". Initialize the buckets as empty slices so the wire contract is always an array. Tightened the empty-buckets test to assert the JSON shape. --- backend/internal/domain/market/merge.go | 7 +++++-- backend/internal/domain/market/merge_test.go | 16 ++++++++++++++++ 2 files changed, 21 insertions(+), 2 deletions(-) diff --git a/backend/internal/domain/market/merge.go b/backend/internal/domain/market/merge.go index cd158e0..fec6aaa 100644 --- a/backend/internal/domain/market/merge.go +++ b/backend/internal/domain/market/merge.go @@ -145,8 +145,11 @@ func PlanMerge(m Market, r ResearchResult) MergePlan { } } - // Assign decisions. - var autoApply, reviewRequired, rejected []FieldMerge + // Assign decisions. Initialize as empty (not nil) so empty buckets + // marshal to [] instead of null — the web UI calls .map() on them. + autoApply := []FieldMerge{} + reviewRequired := []FieldMerge{} + rejected := []FieldMerge{} for _, vs := range validated { fm := FieldMerge{ diff --git a/backend/internal/domain/market/merge_test.go b/backend/internal/domain/market/merge_test.go index 294a5a6..d6b7804 100644 --- a/backend/internal/domain/market/merge_test.go +++ b/backend/internal/domain/market/merge_test.go @@ -1,6 +1,8 @@ package market import ( + "encoding/json" + "strings" "testing" "time" ) @@ -177,6 +179,20 @@ func TestPlanMerge(t *testing.T) { if len(plan.AutoApply) != 0 || len(plan.ReviewRequired) != 0 || len(plan.Rejected) != 0 { t.Error("expected all buckets empty for empty suggestions") } + + // Buckets must marshal to JSON arrays, never null — the web UI calls + // .map() on them and crashes on null. Go's encoding/json renders nil + // slices as null, so PlanMerge must return non-nil empty slices. + raw, err := json.Marshal(plan) + if err != nil { + t.Fatalf("marshal plan: %v", err) + } + got := string(raw) + for _, key := range []string{`"auto_apply":null`, `"review_required":null`, `"rejected":null`} { + if strings.Contains(got, key) { + t.Errorf("plan JSON contains %s; expected []. Got: %s", key, got) + } + } }) }