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 drives the instance-type options.
+ const providerSelect = screen.getByLabelText("Cloud provider") as HTMLSelectElement;
+ fireEvent.change(providerSelect, { target: { value: p } });
+ const instanceSelect = screen.getByLabelText("Instance type");
+ const values = Array.from(instanceSelect.querySelectorAll("option")).map(
+ (o) => o.getAttribute("value"),
+ );
+ for (const want of wantInstances[p]) {
+ expect(values, `provider ${p} fallback missing ${want}`).toContain(want);
+ }
+ }
+ });
+
it("does not treat a non-provider edit as a recreate (no confirm; aws default omitted)", async () => {
const confirmSpy = vi.spyOn(window, "confirm").mockReturnValue(true);
render(
diff --git a/workspace-server/internal/handlers/workspace_compute.go b/workspace-server/internal/handlers/workspace_compute.go
index c1ec8a236..f5e7ecfd5 100644
--- a/workspace-server/internal/handlers/workspace_compute.go
+++ b/workspace-server/internal/handlers/workspace_compute.go
@@ -138,10 +138,149 @@ 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"}
+
+// checkComputeSSOTConsistency is the bidirectional SSOT consistency
+// check (core#2489, Researcher's RC #11736 + CR2's RC #11738). It
+// enforces the full invariant between the SSOT data shapes:
+// - labels map keys == providers slice entries
+// - render-order slice is a permutation of providers slice (same
+// set, no duplicates)
+// - every rendered provider has a default + non-empty instance-types
+//
+// A mismatch in ANY direction panics. The check is extracted as a
+// pure function (no side effects, no init() dependency) so the test
+// suite can invoke it against MUTATED SSOT data (negative cases:
+// missing label, missing render entry, duplicate render entry,
+// missing default, empty instance-types) and assert the panic
+// behavior. A future regression in the production init() — e.g.
+// someone removing the panic for "weird but tolerable" cases —
+// would be caught by the negative tests calling this function.
+//
+// Every direction is enforced:
+// - label without a provider: dead data (a future
+// workspaceComputeProvidersOrdered growth would miss the label)
+// - provider without a label: silent empty label in the
+// response (UX dead-end)
+// - render-order entry without a provider: dead data
+// - provider missing from render order: silent omission from
+// the dropdown (the user couldn't switch to that provider)
+// - render-order entry without a default: silent empty
+// default (the canvas would have to fall back to a hardcoded
+// "t3.medium" or fail)
+// - render-order entry with empty instance-types: silent
+// empty dropdown
+// - duplicate render-order entry: render would silently drop
+// one (the second occurrence overwrites the map)
+//
+// Pinned in lockstep with TestComputeMetadata_SSOTInternalConsistency
+// + the negative TestComputeMetadata_InitPanics* family.
+func checkComputeSSOTConsistency(
+ providers []string,
+ labels map[string]string,
+ renderOrder []string,
+ defaults map[string]string,
+ instanceTypes map[string][]string,
+) {
+ ssotSet := make(map[string]struct{}, len(providers))
+ for _, p := range providers {
+ ssotSet[p] = struct{}{}
+ }
+ // 1. labels keys ⊆ providers keys AND providers keys ⊆ labels keys
+ // (bidirectional — every provider has a label, every label
+ // has a provider).
+ labelsSet := make(map[string]struct{}, len(labels))
+ for p := range labels {
+ if _, ok := ssotSet[p]; !ok {
+ panic(fmt.Sprintf("workspaceComputeProviderLabels has key %q not in workspaceComputeProvidersOrdered", p))
+ }
+ labelsSet[p] = struct{}{}
+ }
+ for _, p := range providers {
+ if _, ok := labelsSet[p]; !ok {
+ panic(fmt.Sprintf("workspaceComputeProvidersOrdered has entry %q with no label in workspaceComputeProviderLabels", p))
+ }
+ }
+ // 2. render-order is a permutation of providers: every entry
+ // has a provider, every provider has an entry, no duplicates.
+ renderSet := make(map[string]struct{}, len(renderOrder))
+ for _, p := range renderOrder {
+ if _, ok := ssotSet[p]; !ok {
+ panic(fmt.Sprintf("workspaceComputeMetadataRenderOrder has entry %q not in workspaceComputeProvidersOrdered", p))
+ }
+ if _, dup := renderSet[p]; dup {
+ panic(fmt.Sprintf("workspaceComputeMetadataRenderOrder has duplicate entry %q", p))
+ }
+ renderSet[p] = struct{}{}
+ }
+ for _, p := range providers {
+ if _, ok := renderSet[p]; !ok {
+ panic(fmt.Sprintf("workspaceComputeProvidersOrdered has entry %q missing from workspaceComputeMetadataRenderOrder", p))
+ }
+ }
+ // 3. every rendered provider has a default + non-empty
+ // instance-types (the canvas relies on both; an empty
+ // default falls back to "t3.medium" via the consumer
+ // helper, but a missing default is a UX dead-end we want
+ // to catch at boot, not in the field).
+ for _, p := range renderOrder {
+ if _, ok := defaults[p]; !ok {
+ panic(fmt.Sprintf("workspaceComputeMetadataRenderOrder has entry %q with no default in workspaceComputeDefaultInstanceByProvider", p))
+ }
+ if len(instanceTypes[p]) == 0 {
+ panic(fmt.Sprintf("workspaceComputeMetadataRenderOrder has entry %q with empty instance-types list", p))
+ }
+ }
+}
+
func init() {
for _, p := range workspaceComputeProvidersOrdered {
workspaceComputeProviderAllowlist[p] = struct{}{}
}
+ // SSOT consistency check (core#2489, Researcher's RC #11736
+ // bidirectional-init fix): the production init guard delegates
+ // to the pure checkComputeSSOTConsistency function above so
+ // the test suite can exercise the same logic against MUTATED
+ // SSOT data (negative cases) and prove the panic behavior.
+ // See checkComputeSSOTConsistency's doc-comment for the full
+ // rationale + the list of drift bugs each direction prevents.
+ checkComputeSSOTConsistency(
+ workspaceComputeProvidersOrdered,
+ workspaceComputeProviderLabels,
+ workspaceComputeMetadataRenderOrder,
+ workspaceComputeDefaultInstanceByProvider,
+ workspaceComputeInstanceTypesOrdered,
+ )
}
func validateWorkspaceCompute(compute models.WorkspaceCompute) error {
@@ -237,18 +376,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..1bcbdc98b 100644
--- a/workspace-server/internal/handlers/workspace_compute_test.go
+++ b/workspace-server/internal/handlers/workspace_compute_test.go
@@ -840,3 +840,316 @@ 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)
+ }
+ }
+}
+
+// TestComputeMetadata_InitPanicsOnLabelMissingFromProviders is
+// the negative case for direction 1.a (label without a
+// provider). The production init() guard must panic on this
+// mutation. The check is the PRODUCTION checkComputeSSOTConsistency
+// function (not a local mirror) so a future regression that
+// weakens the production check is caught here.
+func TestComputeMetadata_InitPanicsOnLabelMissingFromProviders(t *testing.T) {
+ // Mutate: add a "future-cloud" label that has no provider entry.
+ mutatedLabels := make(map[string]string, len(workspaceComputeProviderLabels)+1)
+ for k, v := range workspaceComputeProviderLabels {
+ mutatedLabels[k] = v
+ }
+ mutatedLabels["future-cloud"] = "Future Cloud"
+
+ defer func() {
+ r := recover()
+ if r == nil {
+ t.Fatal("expected panic on label-without-provider, got nil (production checkComputeSSOTConsistency is too lenient)")
+ }
+ // Sanity check: the panic message must mention the bad key.
+ msg, _ := r.(string)
+ if !strings.Contains(msg, "future-cloud") {
+ t.Errorf("panic message should mention the offending key 'future-cloud', got: %q", msg)
+ }
+ }()
+
+ checkComputeSSOTConsistency(
+ workspaceComputeProvidersOrdered,
+ mutatedLabels,
+ workspaceComputeMetadataRenderOrder,
+ workspaceComputeDefaultInstanceByProvider,
+ workspaceComputeInstanceTypesOrdered,
+ )
+}
+
+// TestComputeMetadata_InitPanicsOnProviderMissingLabel is the
+// negative case for direction 1.b (provider without a label).
+// A new provider added to the ordered slice without a matching
+// label entry would silently render an empty string in the
+// /compute/metadata response — the production check must panic.
+func TestComputeMetadata_InitPanicsOnProviderMissingLabel(t *testing.T) {
+ // Mutate: add "new-cloud" to providers but no label for it.
+ mutatedProviders := append([]string{}, workspaceComputeProvidersOrdered...)
+ mutatedProviders = append(mutatedProviders, "new-cloud")
+
+ defer func() {
+ r := recover()
+ if r == nil {
+ t.Fatal("expected panic on provider-without-label, got nil (production checkComputeSSOTConsistency is too lenient)")
+ }
+ msg, _ := r.(string)
+ if !strings.Contains(msg, "new-cloud") {
+ t.Errorf("panic message should mention the offending provider 'new-cloud', got: %q", msg)
+ }
+ }()
+
+ checkComputeSSOTConsistency(
+ mutatedProviders,
+ workspaceComputeProviderLabels,
+ workspaceComputeMetadataRenderOrder,
+ workspaceComputeDefaultInstanceByProvider,
+ workspaceComputeInstanceTypesOrdered,
+ )
+}
+
+// TestComputeMetadata_InitPanicsOnRenderOrderEntryMissingProvider
+// is the negative case for direction 2.a (render-order entry
+// without a provider). A render entry for an unknown provider
+// would silently render a dropdown row with no instances — the
+// production check must panic.
+func TestComputeMetadata_InitPanicsOnRenderOrderEntryMissingProvider(t *testing.T) {
+ // Mutate: add a "ghost-provider" entry to render order
+ // (no matching provider).
+ mutatedRender := append([]string{}, workspaceComputeMetadataRenderOrder...)
+ mutatedRender = append(mutatedRender, "ghost-provider")
+
+ defer func() {
+ r := recover()
+ if r == nil {
+ t.Fatal("expected panic on render-order-entry-without-provider, got nil (production checkComputeSSOTConsistency is too lenient)")
+ }
+ msg, _ := r.(string)
+ if !strings.Contains(msg, "ghost-provider") {
+ t.Errorf("panic message should mention the offending entry 'ghost-provider', got: %q", msg)
+ }
+ }()
+
+ checkComputeSSOTConsistency(
+ workspaceComputeProvidersOrdered,
+ workspaceComputeProviderLabels,
+ mutatedRender,
+ workspaceComputeDefaultInstanceByProvider,
+ workspaceComputeInstanceTypesOrdered,
+ )
+}
+
+// TestComputeMetadata_InitPanicsOnProviderMissingFromRenderOrder
+// is the negative case for direction 2.b (provider missing from
+// render order). A new provider added without a render-order
+// entry would silently be absent from the canvas dropdown —
+// the production check must panic.
+func TestComputeMetadata_InitPanicsOnProviderMissingFromRenderOrder(t *testing.T) {
+ // Mutate: add "new-cloud" to providers + labels, but NOT
+ // to render order.
+ mutatedProviders := append([]string{}, workspaceComputeProvidersOrdered...)
+ mutatedProviders = append(mutatedProviders, "new-cloud")
+ mutatedLabels := make(map[string]string, len(workspaceComputeProviderLabels)+1)
+ for k, v := range workspaceComputeProviderLabels {
+ mutatedLabels[k] = v
+ }
+ mutatedLabels["new-cloud"] = "New Cloud"
+
+ defer func() {
+ r := recover()
+ if r == nil {
+ t.Fatal("expected panic on provider-missing-from-render-order, got nil (production checkComputeSSOTConsistency is too lenient)")
+ }
+ msg, _ := r.(string)
+ if !strings.Contains(msg, "new-cloud") {
+ t.Errorf("panic message should mention the offending provider 'new-cloud', got: %q", msg)
+ }
+ }()
+
+ checkComputeSSOTConsistency(
+ mutatedProviders,
+ mutatedLabels,
+ workspaceComputeMetadataRenderOrder,
+ workspaceComputeDefaultInstanceByProvider,
+ workspaceComputeInstanceTypesOrdered,
+ )
+}
+
+// TestComputeMetadata_InitPanicsOnDuplicateRenderOrderEntry is
+// the negative case for direction 2.c (duplicate render-order
+// entry). A duplicate in the slice would silently overwrite the
+// first occurrence in the map — the production check must panic.
+func TestComputeMetadata_InitPanicsOnDuplicateRenderOrderEntry(t *testing.T) {
+ // Mutate: add a second "aws" entry to render order.
+ mutatedRender := append([]string{}, workspaceComputeMetadataRenderOrder...)
+ mutatedRender = append(mutatedRender, "aws") // duplicate
+
+ defer func() {
+ r := recover()
+ if r == nil {
+ t.Fatal("expected panic on duplicate-render-order-entry, got nil (production checkComputeSSOTConsistency is too lenient)")
+ }
+ msg, _ := r.(string)
+ if !strings.Contains(msg, "duplicate") {
+ t.Errorf("panic message should mention the duplicate, got: %q", msg)
+ }
+ }()
+
+ checkComputeSSOTConsistency(
+ workspaceComputeProvidersOrdered,
+ workspaceComputeProviderLabels,
+ mutatedRender,
+ workspaceComputeDefaultInstanceByProvider,
+ workspaceComputeInstanceTypesOrdered,
+ )
+}
+
+// TestComputeMetadata_InitPanicsOnRenderOrderEntryMissingDefault
+// is the negative case for direction 3.a (render-order entry
+// without a default). A render entry whose provider has no
+// default would silently fall back to "t3.medium" in the
+// consumer helper — the production check must panic.
+func TestComputeMetadata_InitPanicsOnRenderOrderEntryMissingDefault(t *testing.T) {
+ // Mutate: remove the "gcp" default.
+ mutatedDefaults := make(map[string]string, len(workspaceComputeDefaultInstanceByProvider))
+ for k, v := range workspaceComputeDefaultInstanceByProvider {
+ if k == "gcp" {
+ continue
+ }
+ mutatedDefaults[k] = v
+ }
+
+ defer func() {
+ r := recover()
+ if r == nil {
+ t.Fatal("expected panic on render-order-entry-without-default, got nil (production checkComputeSSOTConsistency is too lenient)")
+ }
+ msg, _ := r.(string)
+ if !strings.Contains(msg, "gcp") {
+ t.Errorf("panic message should mention the offending provider 'gcp', got: %q", msg)
+ }
+ }()
+
+ checkComputeSSOTConsistency(
+ workspaceComputeProvidersOrdered,
+ workspaceComputeProviderLabels,
+ workspaceComputeMetadataRenderOrder,
+ mutatedDefaults,
+ workspaceComputeInstanceTypesOrdered,
+ )
+}
+
+// TestComputeMetadata_InitPanicsOnRenderOrderEntryEmptyInstanceTypes
+// is the negative case for direction 3.b (render-order entry with
+// empty instance-types). A render entry whose provider has an
+// empty instance-types list would silently render an empty
+// dropdown — the production check must panic.
+func TestComputeMetadata_InitPanicsOnRenderOrderEntryEmptyInstanceTypes(t *testing.T) {
+ // Mutate: empty the "hetzner" instance-types list.
+ mutatedInstances := make(map[string][]string, len(workspaceComputeInstanceTypesOrdered))
+ for k, v := range workspaceComputeInstanceTypesOrdered {
+ if k == "hetzner" {
+ mutatedInstances[k] = []string{} // empty
+ } else {
+ mutatedInstances[k] = v
+ }
+ }
+
+ defer func() {
+ r := recover()
+ if r == nil {
+ t.Fatal("expected panic on render-order-entry-with-empty-instance-types, got nil (production checkComputeSSOTConsistency is too lenient)")
+ }
+ msg, _ := r.(string)
+ if !strings.Contains(msg, "hetzner") {
+ t.Errorf("panic message should mention the offending provider 'hetzner', got: %q", msg)
+ }
+ }()
+
+ checkComputeSSOTConsistency(
+ workspaceComputeProvidersOrdered,
+ workspaceComputeProviderLabels,
+ workspaceComputeMetadataRenderOrder,
+ workspaceComputeDefaultInstanceByProvider,
+ mutatedInstances,
+ )
+}
+
+// TestComputeMetadata_InitAcceptsLiveSSOT pins the positive case
+// against the PRODUCTION checkComputeSSOTConsistency function
+// (not a local mirror) so a future regression in the production
+// check that would weaken the LIVE-SSOT case is caught here. The
+// package init has already run at process boot; this test is the
+// "readable contract pin" while the package init is the
+// "boot-time fail-closed." Pairs with the negative
+// TestComputeMetadata_InitPanics* family above.
+func TestComputeMetadata_InitAcceptsLiveSSOT(t *testing.T) {
+ // MUST not panic. (If it did, the package init would have
+ // panicked at process boot, and we wouldn't have reached
+ // this test.)
+ checkComputeSSOTConsistency(
+ workspaceComputeProvidersOrdered,
+ workspaceComputeProviderLabels,
+ workspaceComputeMetadataRenderOrder,
+ workspaceComputeDefaultInstanceByProvider,
+ workspaceComputeInstanceTypesOrdered,
+ )
+}