From 52dbb0da07b471b3c52ad6425a58d017873e9b36 Mon Sep 17 00:00:00 2001 From: Jon Langevin Date: Mon, 1 Jun 2026 17:57:17 -0400 Subject: [PATCH] fix(module/infra_admin): populate provider types in Start, not Init engine.BuildFromConfig registers the "workflow" config section AFTER app.Init() (app.Init() then RegisterConfigSection("workflow")), so infra.admin.Init -> populateProviderTypes -> GetConfigSection("workflow") returned "config section not found" and silently degraded provider_type, supported_regions, AND the mutation desiredSpecs/wfCfg to empty. Moved the call to Start() (runs after BuildFromConfig completes). Surfaced by the scenario-92 live boot (region dropdown empty + curl showed empty provider_type); unit tests pre-registered the section via withConfigSectionApp so they missed it. Co-Authored-By: Claude Opus 4.8 (1M context) --- module/infra_admin.go | 24 +++++++++++++++++------- module/infra_admin_test.go | 7 +++++++ 2 files changed, 24 insertions(+), 7 deletions(-) diff --git a/module/infra_admin.go b/module/infra_admin.go index 5240a595..3187696e 100644 --- a/module/infra_admin.go +++ b/module/infra_admin.go @@ -397,13 +397,15 @@ func (m *InfraAdmin) Init(app modular.Application) error { m.providerMu[pm] = &sync.Mutex{} } - // Populate providerTypeByModule from the loaded WorkflowConfig - // per spec-reviewer T6 F1 + design cycle-5/6: handler. - // ListProviders needs the YAML-config `provider:` string, NOT - // the plugin's display name from provider.Name(). - if err := m.populateProviderTypes(app); err != nil { - return fmt.Errorf("infra.admin: populate provider types: %w", err) - } + // NOTE: providerTypeByModule / wfCfg / desiredSpecs are populated in + // Start(), NOT here. engine.BuildFromConfig registers the "workflow" + // config section AFTER app.Init() (engine.go: app.Init() then + // RegisterConfigSection("workflow")), so app.GetConfigSection("workflow") + // returns "not found" during Init and would silently degrade + // provider_type, supported_regions, and the mutation desiredSpecs to + // empty. Start() runs after BuildFromConfig completes, when the section + // is present. (Surfaced by scenario-92 live boot; unit tests pre-register + // the section via withConfigSectionApp so they did not catch it.) // In-process catalogs. m.fieldCatalog = catalog.New() @@ -563,6 +565,14 @@ func (m *InfraAdmin) Start(ctx context.Context) error { return fmt.Errorf("infra.admin: workflowEngine: %w", err) } + // Populate providerTypeByModule + wfCfg + desiredSpecs from the loaded + // WorkflowConfig. MUST run here (Start), not Init: the engine registers + // the "workflow" config section after app.Init(), so this would silently + // degrade to empty during Init. Runs before routes serve any request. + if err := m.populateProviderTypes(m.app); err != nil { + return fmt.Errorf("infra.admin: populate provider types: %w", err) + } + if m.router == nil { return fmt.Errorf("infra.admin: router unresolved — Init failed silently?") } diff --git a/module/infra_admin_test.go b/module/infra_admin_test.go index 16bb3a9a..551301cc 100644 --- a/module/infra_admin_test.go +++ b/module/infra_admin_test.go @@ -200,6 +200,13 @@ func TestInfraAdmin_Init_ResolvesAllServices(t *testing.T) { if len(m.providers) != 1 || m.providers["do-provider"] == nil { t.Errorf("providers = %v, want one do-provider entry", m.providers) } + // providerTypeByModule is populated in Start() now, not Init() — the + // engine registers the "workflow" config section after app.Init(), so + // Init-time GetConfigSection would fail. Drive populateProviderTypes + // explicitly to assert the F1 contract (the function is unchanged). + if err := m.populateProviderTypes(app); err != nil { + t.Fatalf("populateProviderTypes: %v", err) + } if m.providerTypeByModule["do-provider"] != "digitalocean" { t.Errorf("providerTypeByModule[do-provider] = %q, want digitalocean (F1 contract)", m.providerTypeByModule["do-provider"]) }