go-wiring-registry-field-never-populated
Root cause (U-GAP-024): the target field ApplicationConfig.Workflows existed and the admin handler collected the workflow names — but the update DTO AppConfigUpdate had no Workflows field. So Service.Update(AppConfigUpdate{}) was a no-op and the collected names were silently dropped. The router (workflowForEndpoint) was already wired correctly; the gap was in the update path, not the router.
Fix — three targeted changes:
1. Add the missing field to the update struct (pointer = "unset" vs "set to empty", mirroring the existing APIKeys convention):
type AppConfigUpdate struct {
Workflows *[]string // FIX: was missing — the wiring gap
APIKeys *[]string
}
2. Clone-apply it in Service.Update (same append([]string(nil), ...) defensive-copy pattern as APIKeys):
func (s *Service) Update(u AppConfigUpdate) {
if u.APIKeys != nil {
s.cfg.APIKeys = append([]string(nil), (*u.APIKeys)...)
}
if u.Workflows != nil { // FIX: clone-apply, mirrors APIKeys
s.cfg.Workflows = append([]string(nil), (*u.Workflows)...)
}
}
3. Pass the collected names from the handlers:
// BEFORE (bug): names collected, then dropped
names, _ := collectWorkflowNames(r)
s.Update(AppConfigUpdate{})
// AFTER (fix): attach and pass a pointer
names, _ := collectWorkflowNames(r)
s.Update(AppConfigUpdate{Workflows: &names}) // create
// update handler: s.Update(AppConfigUpdate{Workflows: &names}) — explicit empty clears
Router, engine, and DAG code untouched. Pattern: when a config field is collected but never lands, check the UPDATE STRUCT for the missing field before blaming the router.
Built a self-contained reproduction (`~/gap024`, Go 1.26) with the registry, update struct, handlers, router, DAG engine, and an OpenAI-compatible LLM client. `gofmt`/`go vet` clean; **7/7 tests pass with `-race`**:
- **Bug repro pinned** — `brokenAdminCreate` (pre-fix handler passing `AppConfigUpdate{}`) leaves `Workflows` empty; `workflowForEndpoint` returns `ErrUnknownWorkflow`.
- **Fix proven** — `adminCreate`/`adminUpdate` with `&names` populate the registry; the already-wired router resolves `summarize`/`translate`; unregistered names still 404.
- **Live proof (mock provider via `llm.base_url`)** — `httptest` server records every request body; `NewOpenAIClient(mock.URL)` runs `summarize` end-to-end. Recorded body (asserted verbatim):
`{"model":"mock-gpt","messages":[{"role":"user","content":"Given these inputs {\"condense\":\"a 3-bullet summary\",\"extract\":\"key points of the document\"}, produce a final answer."}]}`
Asserted: `{{inputs}}` placeholder fully rendered, both DAG node outputs present, exactly 1 provider call. Counterfactual: broken handler → engine fails at router resolution with **zero** provider calls.
- **Counterfactual regression** — reverting the two-line fix makes the suite fail to compile against the old struct (`unknown field Workflows in struct literal of type AppConfigUpdate`): the tests are coupled to the fixed shape, so the gap can't silently regress.
- **Edge cases** — caller mutating its slice after `Update` cannot corrupt the registry (clone-apply, same as `APIKeys`); `Workflows: nil` = no change (nil-vs-empty distinction); explicit empty list clears the field; unknown workflow misses cleanly.{"model": "deepseek-v4-flash", "problem_class": "go-wiring-registry-field-never-populated", "result": "passed", "tests": 7}