diff --git a/internal/api/api_test.go b/internal/api/api_test.go
index 1002bd2..3c55cd1 100644
--- a/internal/api/api_test.go
+++ b/internal/api/api_test.go
@@ -136,3 +136,20 @@ func TestApplyBadUUID(t *testing.T) {
t.Fatalf("expected 400 for bad uuid, got %d", w.Code)
}
}
+
+// TestChangesetResponseEmptyMarshalsToArrays guards the белый-экран bug: an
+// empty changeset (zone matches its template exactly, e.g. right after a
+// snapshot) must marshal updates/prunes/readOnly as [] not null — a nil slice
+// becomes JSON null and crashes the client's .length/.map calls.
+func TestChangesetResponseEmptyMarshalsToArrays(t *testing.T) {
+ b, err := json.Marshal(toChangesetResponse(diff.Changeset{}))
+ if err != nil {
+ t.Fatal(err)
+ }
+ s := string(b)
+ for _, want := range []string{`"updates":[]`, `"prunes":[]`, `"readOnly":[]`} {
+ if !strings.Contains(s, want) {
+ t.Fatalf("expected %s in %s", want, s)
+ }
+ }
+}
diff --git a/internal/api/dto.go b/internal/api/dto.go
index b127976..ddef2dd 100644
--- a/internal/api/dto.go
+++ b/internal/api/dto.go
@@ -72,7 +72,15 @@ func toRecordView(d diff.RecordDiff) recordView {
}
func toChangesetResponse(cs diff.Changeset) changesetResponse {
- resp := changesetResponse{}
+ // Initialise the slices so an empty changeset (e.g. a zone that exactly
+ // matches its template right after a snapshot) marshals to [] rather than
+ // null — a nil slice becomes JSON null, which crashes clients that call
+ // .length/.map on the field.
+ resp := changesetResponse{
+ Updates: []recordView{},
+ Prunes: []recordView{},
+ ReadOnly: []recordView{},
+ }
for _, d := range cs.Updates() {
resp.Updates = append(resp.Updates, toRecordView(d))
}
diff --git a/web/src/components/DiffView.test.tsx b/web/src/components/DiffView.test.tsx
index 50bba97..6d58953 100644
--- a/web/src/components/DiffView.test.tsx
+++ b/web/src/components/DiffView.test.tsx
@@ -26,3 +26,17 @@ test("marks read-only records", () => {
render(