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.
This commit is contained in:
2026-04-28 13:09:26 +02:00
parent 3d62ba9526
commit f30a963329
2 changed files with 21 additions and 2 deletions

View File

@@ -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{

View File

@@ -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)
}
}
})
}