package cluster import ( "encoding/json" "net/http" "net/http/httptest" "testing" ) func TestWriteServiceErrorLegacyRemovalBlockedIncludesBreakdownDetails(t *testing.T) { recorder := httptest.NewRecorder() handled := writeServiceError(recorder, &LegacyRemovalBlockedError{ BlockedOperation: "create_breaking_release", Report: StaleNodeRiskReport{ HeartbeatStaleAfterSeconds: 900, LegacyRemovalAllowed: false, BridgeHoldRequired: true, BridgeHoldNodeIDs: []string{"node-1"}, BridgeHoldReasons: []string{"legacy_contract_overlap"}, BlockedOperations: []string{"create_breaking_release", "target_breaking_update_policy", "remove_recovery_bridge_overlap"}, Nodes: []StaleNodeRiskNode{ {NodeID: "node-1", Blocked: true, RecoveryBridgeRequired: true}, {NodeID: "node-2", Blocked: false}, }, Summary: StaleNodeRiskSummary{ StaleNodes: 1, BlockedNodes: 1, ArtifactGapNodes: 0, UnknownProfileNodes: 0, WaitingUpdateStatusNodes: 0, UnknownVersionNodes: 0, LegacyRecoveryContractNodes: 0, WaitingRecoveryHeartbeatNodes: 1, }, }, }) if !handled { t.Fatalf("writeServiceError returned false") } if recorder.Code != http.StatusConflict { t.Fatalf("status = %d, want %d", recorder.Code, http.StatusConflict) } var payload struct { Error struct { Details map[string]any `json:"details"` } `json:"error"` } if err := json.Unmarshal(recorder.Body.Bytes(), &payload); err != nil { t.Fatalf("unmarshal response: %v", err) } if payload.Error.Details["blocked_operation"] != "create_breaking_release" { t.Fatalf("blocked_operation = %v", payload.Error.Details["blocked_operation"]) } if payload.Error.Details["waiting_recovery_heartbeat_nodes"] != float64(1) { t.Fatalf("waiting_recovery_heartbeat_nodes = %v", payload.Error.Details["waiting_recovery_heartbeat_nodes"]) } if payload.Error.Details["bridge_hold_required"] != true { t.Fatalf("bridge_hold_required = %v", payload.Error.Details["bridge_hold_required"]) } blockedNodeIDs, ok := payload.Error.Details["blocked_node_ids"].([]any) if !ok || len(blockedNodeIDs) != 1 || blockedNodeIDs[0] != "node-1" { t.Fatalf("blocked_node_ids = %#v", payload.Error.Details["blocked_node_ids"]) } bridgeHoldNodeIDs, ok := payload.Error.Details["bridge_hold_node_ids"].([]any) if !ok || len(bridgeHoldNodeIDs) != 1 || bridgeHoldNodeIDs[0] != "node-1" { t.Fatalf("bridge_hold_node_ids = %#v", payload.Error.Details["bridge_hold_node_ids"]) } }