From 598520b9a41ba3567c254cdb76413825fbb7b403 Mon Sep 17 00:00:00 2001 From: core-devops Date: Sun, 7 Jun 2026 14:06:19 -0700 Subject: [PATCH] fix(workspace): round-trip compute.provider (+ data_persistence) in GET MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit workspaceComputeJSON hand-builds the serialized compute map and only emitted instance_type/volume/display — so compute.provider and compute.data_persistence were FORWARDED to CP at provision time (2f5536fd / internal#734) but never returned by GET /workspaces. Consequences: - the canvas per-workspace provider badge (core#2404) always showed the default AWS, regardless of the workspace's real cloud; - the data-persistence selector always showed "auto". Both are read back by the canvas (data.compute.provider / .data_persistence), so this completes the round-trip. Still omit-when-empty, so existing AWS/default rows serialize byte-identically (the exact-JSON test is unchanged). workspaceComputeIsZero now also accounts for these two fields so a provider-only compute isn't collapsed to "{}". Surfaced by the multi-provider e2e (cp #611): the persisted compute came back without provider, which I initially mis-read as a staging-image lag — it was this serialization gap on main. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../internal/handlers/workspace_compute.go | 18 ++++++++++- .../handlers/workspace_compute_test.go | 32 +++++++++++++++++++ 2 files changed, 49 insertions(+), 1 deletion(-) diff --git a/workspace-server/internal/handlers/workspace_compute.go b/workspace-server/internal/handlers/workspace_compute.go index 0824a655b..3a105d988 100644 --- a/workspace-server/internal/handlers/workspace_compute.go +++ b/workspace-server/internal/handlers/workspace_compute.go @@ -112,7 +112,12 @@ func workspaceComputeIsZero(compute models.WorkspaceCompute) bool { compute.Display.Mode == "" && compute.Display.Width == 0 && compute.Display.Height == 0 && - compute.Display.Protocol == "" + compute.Display.Protocol == "" && + // A provider- or persistence-only compute is NOT zero — it must + // round-trip so GET returns those fields (canvas provider badge + + // data-persistence selector both read them back). + compute.Provider == "" && + compute.DataPersistence == "" } func workspaceComputeJSON(compute models.WorkspaceCompute) (string, error) { @@ -142,6 +147,17 @@ func workspaceComputeJSON(compute models.WorkspaceCompute) (string, error) { if len(display) > 0 { out["display"] = display } + // Cloud/compute provider + durable-data choice. These were FORWARDED to CP + // at provision time but never serialized back here, so GET /workspaces + // dropped them — the canvas provider badge always showed the default AWS and + // the data-persistence selector always showed "auto". Round-trip them (still + // omit-when-empty, so existing AWS/default rows serialize unchanged). + if compute.Provider != "" { + out["provider"] = compute.Provider + } + if compute.DataPersistence != "" { + out["data_persistence"] = compute.DataPersistence + } b, err := json.Marshal(out) if err != nil { return "", err diff --git a/workspace-server/internal/handlers/workspace_compute_test.go b/workspace-server/internal/handlers/workspace_compute_test.go index a85d55cc1..ba1c38ab1 100644 --- a/workspace-server/internal/handlers/workspace_compute_test.go +++ b/workspace-server/internal/handlers/workspace_compute_test.go @@ -93,6 +93,38 @@ func TestWorkspaceComputeJSON_OmitsEmptyNestedSections(t *testing.T) { } } +// Regression: provider + data_persistence were FORWARDED to CP but dropped from +// the serialized compute, so GET /workspaces never returned them (the canvas +// provider badge always showed AWS, the persistence selector always "auto"). +func TestWorkspaceComputeJSON_RoundTripsProviderAndDataPersistence(t *testing.T) { + got, err := workspaceComputeJSON(models.WorkspaceCompute{ + InstanceType: "t3.medium", + Provider: "gcp", + DataPersistence: "persist", + }) + if err != nil { + t.Fatalf("workspaceComputeJSON returned error: %v", err) + } + if !strings.Contains(got, `"provider":"gcp"`) { + t.Fatalf("workspaceComputeJSON dropped provider: %s", got) + } + if !strings.Contains(got, `"data_persistence":"persist"`) { + t.Fatalf("workspaceComputeJSON dropped data_persistence: %s", got) + } +} + +// A provider-only compute must NOT be treated as zero (else it serializes to +// "{}" and the cloud is lost). +func TestWorkspaceComputeJSON_ProviderOnlyIsNotZero(t *testing.T) { + got, err := workspaceComputeJSON(models.WorkspaceCompute{Provider: "hetzner"}) + if err != nil { + t.Fatalf("workspaceComputeJSON returned error: %v", err) + } + if got == "{}" || !strings.Contains(got, `"provider":"hetzner"`) { + t.Fatalf("provider-only compute serialized as zero: %s", got) + } +} + func TestWorkspaceCreate_WithCompute_PersistsComputeJSON(t *testing.T) { mock := setupTestDB(t) setupTestRedis(t) -- 2.52.0