fix(workspace-server#2489): derive ComputeMetadata from SSOT maps #2853
@@ -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(
|
||||
<ContainerConfigTab
|
||||
workspaceId="ws-fallback-pin"
|
||||
data={{
|
||||
runtime: "claude-code",
|
||||
status: "online",
|
||||
needsRestart: false,
|
||||
activeTasks: 0,
|
||||
maxConcurrentTasks: null,
|
||||
workspaceAccess: "none",
|
||||
deliveryMode: "push",
|
||||
compute: { instance_type: "t3.medium", provider: "aws", volume: { root_gb: 30 } },
|
||||
}}
|
||||
/>,
|
||||
);
|
||||
|
||||
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<typeof providers[number], string[]> = {
|
||||
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 <select> 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(
|
||||
|
||||
@@ -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})
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user