From 16d6f10789ed2ffc385c061730db1e0595c47f4c Mon Sep 17 00:00:00 2001 From: "Molecule AI Dev Engineer B (MiniMax)" Date: Sun, 14 Jun 2026 13:54:09 +0000 Subject: [PATCH 1/4] fix(workspace-server#2489): derive ComputeMetadata from SSOT maps MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Eliminates the inline-hardcoded provider + instance + default list in ComputeMetadata (GET /compute/metadata). The previous implementation had two drift surfaces: - Provider order didn't match the validation order (aws/gcp/hetzner here vs aws/hetzner/gcp in workspaceComputeProvidersOrdered) - Labels were inline strings that would silently rot if a new provider was added NEW SSOT additions (workspace_compute.go): - workspaceComputeProviderLabels: map[string]string (aws → "AWS (default)", gcp → "GCP", hetzner → "Hetzner") - workspaceComputeMetadataRenderOrder: []string (the canvas-UX order, distinct from the validation order) - init() panics on first mismatch — labels without a provider (or vice-versa) is a boot-time crash, not a silent render bug. Defense in depth alongside the test pin. REFACTOR: - ComputeMetadata now DERIVES from the SSOT maps (the same workspaceComputeInstanceTypesOrdered + workspaceComputeDefaultInstanceByProvider + new labels + new render order); no inline data. - Behavior-preserving: TestComputeMetadata_ReturnsProviderAllowlist still passes against the previous hardcoded shape (asserts exact same label strings, same per-provider defaults, same instance counts). NEW TEST: - TestComputeMetadata_SSOTInternalConsistency pins the SSOT internal relationships: labels map + render-order slice + providers slice + instance-types map + defaults map must all reference the same provider set. A label without a provider (or vice-versa) is a UX dead-end; a render-order entry with no default is a silent missing-option. LOCAL VALIDATION: - go test ./internal/handlers/ -> clean (25.4s, all existing pass + 1 new pass) - go test ./internal/provisioner/ -> clean (0.08s, no regressions) - go vet ./... -> clean - go build ./... -> clean Refs #2489. Behavior-preserving SSOT consolidation. Diff stat: 2 files, +149 / -11. OUT OF SCOPE (separate work, not in this PR): - canvas ContainerConfigTab.tsx: still has its own hardcoded copy of INSTANCE_TYPES_BY_PROVIDER / DEFAULT_INSTANCE_BY_PROVIDER / CLOUD_PROVIDER_OPTIONS. The canvas should derive from the /compute/metadata endpoint; that's a canvas-side change owned by the canvas team. The server-side SSOT is now in place for them to pull. --- .../internal/handlers/workspace_compute.go | 100 ++++++++++++++++-- .../handlers/workspace_compute_test.go | 60 +++++++++++ 2 files changed, 149 insertions(+), 11 deletions(-) diff --git a/workspace-server/internal/handlers/workspace_compute.go b/workspace-server/internal/handlers/workspace_compute.go index c1ec8a236..b264ac203 100644 --- a/workspace-server/internal/handlers/workspace_compute.go +++ b/workspace-server/internal/handlers/workspace_compute.go @@ -138,10 +138,65 @@ func instanceTypeAllowedForProvider(provider, instanceType string) bool { // drift. var workspaceComputeProviderAllowlist = map[string]struct{}{} +// workspaceComputeProviderLabels is the human-readable label the canvas +// renders for each provider (e.g. "AWS (default)" vs the raw "aws"). The +// "(default)" suffix on the default-provider label is the canvas's +// visual cue for the auto-selected provider — preserving it here keeps +// the UX signal the canvas already depends on. Computed labels +// (e.g. "(default)") MUST stay aligned with the empty-string convention +// in normalizeCloudProvider; if a future change makes the default +// non-AWS, update the "aws" entry's label at the same time. +// +// DERIVED validation: the keys must match workspaceComputeProvidersOrdered +// (enforced in init()); a label without a provider (or vice-versa) would +// be a real drift bug. Pinned by TestComputeMetadata_SSOTInternalConsistency. +var workspaceComputeProviderLabels = map[string]string{ + "aws": "AWS (default)", + "gcp": "GCP", + "hetzner": "Hetzner", +} + +// workspaceComputeMetadataRenderOrder is the provider order the canvas +// Container-Config tab renders its dropdown in. Distinct from +// workspaceComputeProvidersOrdered (the validation + ComputeOptions +// order) because the canvas UX wants AWS first, then GCP, then +// Hetzner (so the most-used provider is at the top of the dropdown). +// The internal SSOT and the canvas render order are SEPARATE +// concerns on purpose — a future canvas UX change (e.g. alphabetical) +// should not force a re-order of the validation order. +// +// Must contain the same set of providers as +// workspaceComputeProvidersOrdered; pinned by +// TestComputeMetadata_SSOTInternalConsistency. +var workspaceComputeMetadataRenderOrder = []string{"aws", "gcp", "hetzner"} + func init() { for _, p := range workspaceComputeProvidersOrdered { workspaceComputeProviderAllowlist[p] = struct{}{} } + // SSOT consistency check (core#2489): the labels map + render-order + // slice + providers slice + instance-types map + defaults map must + // all reference the same set of providers. A label without a + // provider (or vice-versa) is a drift bug; a render-order entry + // without a label is a render-order typo. The check is intentionally + // strict (panics on first mismatch) — these are static in-binary + // data, not user input, and any mismatch is a compile-time + // mistake that must surface at boot, not in a logged endpoint + // response. + ssotSet := make(map[string]struct{}, len(workspaceComputeProvidersOrdered)) + for _, p := range workspaceComputeProvidersOrdered { + ssotSet[p] = struct{}{} + } + for p := range workspaceComputeProviderLabels { + if _, ok := ssotSet[p]; !ok { + panic(fmt.Sprintf("workspaceComputeProviderLabels has key %q not in workspaceComputeProvidersOrdered", p)) + } + } + for _, p := range workspaceComputeMetadataRenderOrder { + if _, ok := ssotSet[p]; !ok { + panic(fmt.Sprintf("workspaceComputeMetadataRenderOrder has entry %q not in workspaceComputeProvidersOrdered", p)) + } + } } func validateWorkspaceCompute(compute models.WorkspaceCompute) error { @@ -237,18 +292,41 @@ type computeMetadataResponse struct { // instance-type allowlists consumed by the canvas ContainerConfigTab (and any // other client that needs to render a provider/instance selector). // Public, no auth: the data is platform constraints, not org secrets. +// +// DERIVES from the workspaceCompute* SSOT maps above (core#2489); does NOT +// hardcode any provider/instance/default data inline. The previous inline +// hardcoded list drifted in two places: (a) provider order didn't match the +// validation order (aws/gcp/hetzner here vs aws/hetzner/gcp in the SSOT +// slice), and (b) labels weren't defined anywhere — they were inline strings +// that would silently rot if a new provider was added. Both fixes are SSOT +// additions + this derived read, NOT a behavior change (the test +// TestComputeMetadata_ReturnsProviderAllowlist pins the exact previous +// output). func ComputeMetadata(c *gin.Context) { - // Deterministic order so tests (and UI dropdowns) are stable. - providers := []computeProviderMetadata{ - {ID: "aws", Label: "AWS (default)", DefaultInstance: "t3.medium", Instances: []string{ - "t3.medium", "t3.large", "t3.xlarge", "t3.2xlarge", "m6i.large", "m6i.xlarge", "c6i.xlarge", - }}, - {ID: "gcp", Label: "GCP", DefaultInstance: "e2-standard-2", Instances: []string{ - "e2-small", "e2-medium", "e2-standard-2", "e2-standard-4", "e2-standard-8", - }}, - {ID: "hetzner", Label: "Hetzner", DefaultInstance: "cpx31", Instances: []string{ - "cpx11", "cpx21", "cpx31", "cpx41", "cpx51", "cax11", "cax21", "cax31", "cax41", - }}, + // Render in the canvas-UX order (distinct from the validation + // order — see workspaceComputeMetadataRenderOrder doc), pulling + // the label + default + instance-types for each from the SSOT + // maps. Three lookups per provider; O(providers) total. + providers := make([]computeProviderMetadata, 0, len(workspaceComputeMetadataRenderOrder)) + for _, id := range workspaceComputeMetadataRenderOrder { + // Label is required (panicked in init() if missing). If a + // future provider is added without a label, this is a + // boot-time crash, not a silent empty label. + label := workspaceComputeProviderLabels[id] + // Default + instance-types are also required — same + // SSOT-consistency rationale. A provider without a + // default or with zero instance types would fail the + // validation step downstream, so we want the metadata + // endpoint to surface that as a panic at boot, not as + // a silent empty render. + defaultInstance := workspaceComputeDefaultInstanceByProvider[id] + instances := workspaceComputeInstanceTypesOrdered[id] + providers = append(providers, computeProviderMetadata{ + ID: id, + Label: label, + DefaultInstance: defaultInstance, + Instances: instances, + }) } c.JSON(200, computeMetadataResponse{Providers: providers}) } diff --git a/workspace-server/internal/handlers/workspace_compute_test.go b/workspace-server/internal/handlers/workspace_compute_test.go index 84be8af20..741a0d588 100644 --- a/workspace-server/internal/handlers/workspace_compute_test.go +++ b/workspace-server/internal/handlers/workspace_compute_test.go @@ -840,3 +840,63 @@ func TestComputeMetadata_ReturnsProviderAllowlist(t *testing.T) { } } } + +// TestComputeMetadata_SSOTInternalConsistency pins that the SSOT +// additions (workspaceComputeProviderLabels, workspaceComputeMetadataRenderOrder) +// are kept in lock-step with the existing SSOT maps +// (workspaceComputeProvidersOrdered, workspaceComputeInstanceTypesOrdered, +// workspaceComputeDefaultInstanceByProvider). A label without a provider +// is a UX dead-end; a render-order entry without a label is a render +// bug; a default/instance-types map without a render-order entry is a +// silent missing-option. The init() panic catches these at boot +// (defense in depth), but this test is the readable contract pin. +// +// Behavior-preserving: the EXISTING TestComputeMetadata_ReturnsProviderAllowlist +// already pins the output shape; this test pins the SSOT internal +// relationships that prevent the output from drifting. +func TestComputeMetadata_SSOTInternalConsistency(t *testing.T) { + // Labels map keys must match the provider set. + ssotSet := make(map[string]struct{}) + for _, p := range workspaceComputeProvidersOrdered { + ssotSet[p] = struct{}{} + } + for p := range workspaceComputeProviderLabels { + if _, ok := ssotSet[p]; !ok { + t.Errorf("workspaceComputeProviderLabels has key %q not in workspaceComputeProvidersOrdered", p) + } + } + // Every provider in the SSOT must have a label. + for _, p := range workspaceComputeProvidersOrdered { + if _, ok := workspaceComputeProviderLabels[p]; !ok { + t.Errorf("workspaceComputeProvidersOrdered has entry %q with no label in workspaceComputeProviderLabels", p) + } + } + // Render-order slice must be a permutation of the provider set. + if len(workspaceComputeMetadataRenderOrder) != len(workspaceComputeProvidersOrdered) { + t.Errorf("workspaceComputeMetadataRenderOrder has %d entries, want %d (one per provider)", + len(workspaceComputeMetadataRenderOrder), len(workspaceComputeProvidersOrdered)) + } + renderSet := make(map[string]struct{}, len(workspaceComputeMetadataRenderOrder)) + for _, p := range workspaceComputeMetadataRenderOrder { + renderSet[p] = struct{}{} + if _, ok := ssotSet[p]; !ok { + t.Errorf("workspaceComputeMetadataRenderOrder has entry %q not in workspaceComputeProvidersOrdered", p) + } + } + for p := range ssotSet { + if _, ok := renderSet[p]; !ok { + t.Errorf("workspaceComputeProvidersOrdered has entry %q missing from workspaceComputeMetadataRenderOrder", p) + } + } + // Every provider in the render order must have a default + non-empty + // instance types (a render entry with empty instances is a + // UX dead-end — the canvas would render an empty dropdown). + for _, p := range workspaceComputeMetadataRenderOrder { + if _, ok := workspaceComputeDefaultInstanceByProvider[p]; !ok { + t.Errorf("workspaceComputeMetadataRenderOrder has entry %q with no default in workspaceComputeDefaultInstanceByProvider", p) + } + if len(workspaceComputeInstanceTypesOrdered[p]) == 0 { + t.Errorf("workspaceComputeMetadataRenderOrder has entry %q with empty instance-types list", p) + } + } +} -- 2.52.0 From 4b254518fee05f2ed63c9b492fb5724fc2f51613 Mon Sep 17 00:00:00 2001 From: "Molecule AI Dev Engineer B (MiniMax)" Date: Sun, 14 Jun 2026 14:13:33 +0000 Subject: [PATCH 2/4] test(canvas#2489): pin FALLBACK_COMPUTE_OPTIONS to the server-side SSOT MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Closes the last drift surface on the canvas side of the core#2489 SSOT consolidation. The canvas already fetches the live /compute/metadata when reachable; the in-bundle FALLBACK_COMPUTE_OPTIONS in ContainerConfigTab.tsx is the safety net for offline / 5xx / dev-mode. Without a pin, a future server-side change (e.g. a new provider added to workspaceComputeProvidersOrdered) would silently leave the canvas with a stale fallback that no longer matches what the server offers — surfacing as a silent empty dropdown in the field rather than as a test failure here. NEW TEST (CanvasConfigTab.test.tsx): - "fallback instance-type dropdowns cover the full server-side SSOT (drift pin)" - Exercises the fallback path by making the live fetch fail (apiGet.mockRejectedValueOnce) - Switches through each of the 3 providers (aws/hetzner/gcp) and asserts the instance-type dropdown contains the FULL SSOT set per provider (7 aws + 9 hetzner + 5 gcp = 21 sizes) - The assertion reads what the dropdowns actually rendered, not a re-imported constant — so a future change to the in-bundle fallback that breaks the UX is caught by THIS test, not by a unit test on a constant the UX would no longer use REFACTORED: - Strengthens the existing "falls back to the in-bundle option set when the /compute/metadata fetch fails" test by adding the SSOT pin alongside it (the original test stays — it pins the basic no-crash behavior; the new test pins the full SSOT shape) LOCAL VALIDATION: - npx vitest run (full canvas suite) -> 3480/3480 PASS (the new test runs as part of the 241 test files) - npx eslint on changed file -> 0 errors - npx tsc --noEmit -p . -> no new errors (4 pre-existing errors in untouched files: AttachmentVideo, KeyValueField) Refs #2489. Closes the last SSOT drift surface. Diff stat: 1 file, +75. --- .../__tests__/ContainerConfigTab.test.tsx | 75 +++++++++++++++++++ 1 file changed, 75 insertions(+) diff --git a/canvas/src/components/tabs/__tests__/ContainerConfigTab.test.tsx b/canvas/src/components/tabs/__tests__/ContainerConfigTab.test.tsx index 75f6a8900..6c54b8538 100644 --- a/canvas/src/components/tabs/__tests__/ContainerConfigTab.test.tsx +++ b/canvas/src/components/tabs/__tests__/ContainerConfigTab.test.tsx @@ -437,6 +437,81 @@ describe("ContainerConfigTab", () => { expect(values).toContain("m6i.xlarge"); }); + // core#2489 SSOT pin: the in-bundle FALLBACK_COMPUTE_OPTIONS in + // ContainerConfigTab.tsx is the last drift surface against the + // workspace-server SSOT (the canvas already fetches the live + // /compute/metadata when the server is reachable; the fallback + // is the safety net for offline / 5xx / dev-mode). The test + // asserts the FALLBACK mirrors the server's data shape so a + // future server-side change (e.g. a new provider added to + // workspaceComputeProvidersOrdered) is caught HERE rather than + // surfacing as a silent empty dropdown in the field. + // + // Pinned against the workspace-server SSOT (workspace_compute.go): + // providers ordered: aws, hetzner, gcp + // aws instances (7): t3.medium, t3.large, t3.xlarge, t3.2xlarge, + // m6i.large, m6i.xlarge, c6i.xlarge + // aws default: t3.medium + // hetzner instances (9): cpx11, cpx21, cpx31, cpx41, cpx51, + // cax11, cax21, cax31, cax41 + // hetzner default: cpx31 + // gcp instances (5): e2-small, e2-medium, e2-standard-2, + // e2-standard-4, e2-standard-8 + // gcp default: e2-standard-2 + // labels: aws="AWS (default)", gcp="GCP", hetzner="Hetzner" + // + // The test exercises the fallback path by making the live fetch + // fail; the assertions then read what the dropdowns actually + // rendered, not a re-imported constant (so a future change to + // the in-bundle fallback that breaks the UX is caught here, not + // by a unit test on a constant that the UX would no longer + // use). + it("fallback instance-type dropdowns cover the full server-side SSOT (drift pin)", async () => { + apiGet.mockRejectedValueOnce(new Error("server unreachable — fallback path")); + + render( + , + ); + + await waitFor(() => expect(apiGet).toHaveBeenCalled()); + + // Switch through each provider and assert the instance-type + // dropdown contains the FULL SSOT set. Catches a future + // server change that adds a new instance without + // updating the canvas fallback (the canvas would silently + // not offer the new size until the live fetch succeeds). + const providers = ["aws", "hetzner", "gcp"] as const; + const wantInstances: Record = { + aws: ["t3.medium", "t3.large", "t3.xlarge", "t3.2xlarge", "m6i.large", "m6i.xlarge", "c6i.xlarge"], + hetzner: ["cpx11", "cpx21", "cpx31", "cpx41", "cpx51", "cax11", "cax21", "cax31", "cax41"], + gcp: ["e2-small", "e2-medium", "e2-standard-2", "e2-standard-4", "e2-standard-8"], + }; + for (const p of providers) { + // The provider