@@ -77,7 +77,7 @@ func TestConfig_configureProviders(t *testing.T) {
7777 env := env.NewFromMap(map[string]string{
7878 "OPENAI_API_KEY": "test-key",
7979 })
80− resolver := NewEnvironmentVariableResolver(env)
80+ resolver := NewShellVariableResolver(env)
8181 err := cfg.configureProviders(testStore(cfg), env, resolver, knownProviders)
8282 require.NoError(t, err)
8383 require.Equal(t, 1, cfg.Providers.Len())
@@ -120,7 +120,7 @@ func TestConfig_configureProvidersWithOverride(t *testing.T) {
120120 env := env.NewFromMap(map[string]string{
121121 "OPENAI_API_KEY": "test-key",
122122 })
123− resolver := NewEnvironmentVariableResolver(env)
123+ resolver := NewShellVariableResolver(env)
124124 err := cfg.configureProviders(testStore(cfg), env, resolver, knownProviders)
125125 require.NoError(t, err)
126126 require.Equal(t, 1, cfg.Providers.Len())
@@ -162,7 +162,7 @@ func TestConfig_configureProvidersWithNewProvider(t *testing.T) {
162162 env := env.NewFromMap(map[string]string{
163163 "OPENAI_API_KEY": "test-key",
164164 })
165− resolver := NewEnvironmentVariableResolver(env)
165+ resolver := NewShellVariableResolver(env)
166166 err := cfg.configureProviders(testStore(cfg), env, resolver, knownProviders)
167167 require.NoError(t, err)
168168 // Should be to because of the env variable
@@ -198,7 +198,7 @@ func TestConfig_configureProvidersBedrockWithCredentials(t *testing.T) {
198198 "AWS_ACCESS_KEY_ID": "test-key-id",
199199 "AWS_SECRET_ACCESS_KEY": "test-secret-key",
200200 })
201− resolver := NewEnvironmentVariableResolver(env)
201+ resolver := NewShellVariableResolver(env)
202202 err := cfg.configureProviders(testStore(cfg), env, resolver, knownProviders)
203203 require.NoError(t, err)
204204 require.Equal(t, cfg.Providers.Len(), 1)
@@ -224,7 +224,7 @@ func TestConfig_configureProvidersBedrockWithoutCredentials(t *testing.T) {
224224 cfg := &Config{}
225225 cfg.setDefaults("/tmp", "")
226226 env := env.NewFromMap(map[string]string{})
227− resolver := NewEnvironmentVariableResolver(env)
227+ resolver := NewShellVariableResolver(env)
228228 err := cfg.configureProviders(testStore(cfg), env, resolver, knownProviders)
229229 require.NoError(t, err)
230230 // Provider should not be configured without credentials
@@ -249,7 +249,7 @@ func TestConfig_configureProvidersBedrockWithoutUnsupportedModel(t *testing.T) {
249249 "AWS_ACCESS_KEY_ID": "test-key-id",
250250 "AWS_SECRET_ACCESS_KEY": "test-secret-key",
251251 })
252− resolver := NewEnvironmentVariableResolver(env)
252+ resolver := NewShellVariableResolver(env)
253253 err := cfg.configureProviders(testStore(cfg), env, resolver, knownProviders)
254254 require.Error(t, err)
255255}
@@ -272,7 +272,7 @@ func TestConfig_configureProvidersVertexAIWithCredentials(t *testing.T) {
272272 "VERTEXAI_PROJECT": "test-project",
273273 "VERTEXAI_LOCATION": "us-central1",
274274 })
275− resolver := NewEnvironmentVariableResolver(env)
275+ resolver := NewShellVariableResolver(env)
276276 err := cfg.configureProviders(testStore(cfg), env, resolver, knownProviders)
277277 require.NoError(t, err)
278278 require.Equal(t, cfg.Providers.Len(), 1)
@@ -304,7 +304,7 @@ func TestConfig_configureProvidersVertexAIWithoutCredentials(t *testing.T) {
304304 "GOOGLE_CLOUD_PROJECT": "test-project",
305305 "GOOGLE_CLOUD_LOCATION": "us-central1",
306306 })
307− resolver := NewEnvironmentVariableResolver(env)
307+ resolver := NewShellVariableResolver(env)
308308 err := cfg.configureProviders(testStore(cfg), env, resolver, knownProviders)
309309 require.NoError(t, err)
310310 // Provider should not be configured without proper credentials
@@ -329,7 +329,7 @@ func TestConfig_configureProvidersVertexAIMissingProject(t *testing.T) {
329329 "GOOGLE_GENAI_USE_VERTEXAI": "true",
330330 "GOOGLE_CLOUD_LOCATION": "us-central1",
331331 })
332− resolver := NewEnvironmentVariableResolver(env)
332+ resolver := NewShellVariableResolver(env)
333333 err := cfg.configureProviders(testStore(cfg), env, resolver, knownProviders)
334334 require.NoError(t, err)
335335 // Provider should not be configured without project
@@ -353,7 +353,7 @@ func TestConfig_configureProvidersSetProviderID(t *testing.T) {
353353 env := env.NewFromMap(map[string]string{
354354 "OPENAI_API_KEY": "test-key",
355355 })
356− resolver := NewEnvironmentVariableResolver(env)
356+ resolver := NewShellVariableResolver(env)
357357 err := cfg.configureProviders(testStore(cfg), env, resolver, knownProviders)
358358 require.NoError(t, err)
359359 require.Equal(t, cfg.Providers.Len(), 1)
@@ -544,7 +544,7 @@ func TestConfig_configureProvidersWithDisabledProvider(t *testing.T) {
544544 env := env.NewFromMap(map[string]string{
545545 "OPENAI_API_KEY": "test-key",
546546 })
547− resolver := NewEnvironmentVariableResolver(env)
547+ resolver := NewShellVariableResolver(env)
548548 err := cfg.configureProviders(testStore(cfg), env, resolver, knownProviders)
549549 require.NoError(t, err)
550550
@@ -572,7 +572,7 @@ func TestConfig_configureProvidersCustomProviderValidation(t *testing.T) {
572572 cfg.setDefaults("/tmp", "")
573573
574574 env := env.NewFromMap(map[string]string{})
575− resolver := NewEnvironmentVariableResolver(env)
575+ resolver := NewShellVariableResolver(env)
576576 err := cfg.configureProviders(testStore(cfg), env, resolver, []catwalk.Provider{})
577577 require.NoError(t, err)
578578
@@ -595,7 +595,7 @@ func TestConfig_configureProvidersCustomProviderValidation(t *testing.T) {
595595 cfg.setDefaults("/tmp", "")
596596
597597 env := env.NewFromMap(map[string]string{})
598− resolver := NewEnvironmentVariableResolver(env)
598+ resolver := NewShellVariableResolver(env)
599599 err := cfg.configureProviders(testStore(cfg), env, resolver, []catwalk.Provider{})
600600 require.NoError(t, err)
601601
@@ -617,7 +617,7 @@ func TestConfig_configureProvidersCustomProviderValidation(t *testing.T) {
617617 cfg.setDefaults("/tmp", "")
618618
619619 env := env.NewFromMap(map[string]string{})
620− resolver := NewEnvironmentVariableResolver(env)
620+ resolver := NewShellVariableResolver(env)
621621 err := cfg.configureProviders(testStore(cfg), env, resolver, []catwalk.Provider{})
622622 require.NoError(t, err)
623623
@@ -642,7 +642,7 @@ func TestConfig_configureProvidersCustomProviderValidation(t *testing.T) {
642642 cfg.setDefaults("/tmp", "")
643643
644644 env := env.NewFromMap(map[string]string{})
645− resolver := NewEnvironmentVariableResolver(env)
645+ resolver := NewShellVariableResolver(env)
646646 err := cfg.configureProviders(testStore(cfg), env, resolver, []catwalk.Provider{})
647647 require.NoError(t, err)
648648
@@ -667,7 +667,7 @@ func TestConfig_configureProvidersCustomProviderValidation(t *testing.T) {
667667 cfg.setDefaults("/tmp", "")
668668
669669 env := env.NewFromMap(map[string]string{})
670− resolver := NewEnvironmentVariableResolver(env)
670+ resolver := NewShellVariableResolver(env)
671671 err := cfg.configureProviders(testStore(cfg), env, resolver, []catwalk.Provider{})
672672 require.NoError(t, err)
673673
@@ -695,7 +695,7 @@ func TestConfig_configureProvidersCustomProviderValidation(t *testing.T) {
695695 cfg.setDefaults("/tmp", "")
696696
697697 env := env.NewFromMap(map[string]string{})
698− resolver := NewEnvironmentVariableResolver(env)
698+ resolver := NewShellVariableResolver(env)
699699 err := cfg.configureProviders(testStore(cfg), env, resolver, []catwalk.Provider{})
700700 require.NoError(t, err)
701701
@@ -725,7 +725,7 @@ func TestConfig_configureProvidersCustomProviderValidation(t *testing.T) {
725725 cfg.setDefaults("/tmp", "")
726726
727727 env := env.NewFromMap(map[string]string{})
728− resolver := NewEnvironmentVariableResolver(env)
728+ resolver := NewShellVariableResolver(env)
729729 err := cfg.configureProviders(testStore(cfg), env, resolver, []catwalk.Provider{})
730730 require.NoError(t, err)
731731
@@ -760,7 +760,7 @@ func TestConfig_configureProvidersEnhancedCredentialValidation(t *testing.T) {
760760 env := env.NewFromMap(map[string]string{
761761 "GOOGLE_GENAI_USE_VERTEXAI": "false",
762762 })
763− resolver := NewEnvironmentVariableResolver(env)
763+ resolver := NewShellVariableResolver(env)
764764 err := cfg.configureProviders(testStore(cfg), env, resolver, knownProviders)
765765 require.NoError(t, err)
766766
@@ -791,7 +791,7 @@ func TestConfig_configureProvidersEnhancedCredentialValidation(t *testing.T) {
791791 cfg.setDefaults("/tmp", "")
792792
793793 env := env.NewFromMap(map[string]string{})
794− resolver := NewEnvironmentVariableResolver(env)
794+ resolver := NewShellVariableResolver(env)
795795 err := cfg.configureProviders(testStore(cfg), env, resolver, knownProviders)
796796 require.NoError(t, err)
797797
@@ -822,7 +822,7 @@ func TestConfig_configureProvidersEnhancedCredentialValidation(t *testing.T) {
822822 cfg.setDefaults("/tmp", "")
823823
824824 env := env.NewFromMap(map[string]string{})
825− resolver := NewEnvironmentVariableResolver(env)
825+ resolver := NewShellVariableResolver(env)
826826 err := cfg.configureProviders(testStore(cfg), env, resolver, knownProviders)
827827 require.NoError(t, err)
828828
@@ -855,7 +855,7 @@ func TestConfig_configureProvidersEnhancedCredentialValidation(t *testing.T) {
855855 env := env.NewFromMap(map[string]string{
856856 "OPENAI_API_KEY": "test-key",
857857 })
858− resolver := NewEnvironmentVariableResolver(env)
858+ resolver := NewShellVariableResolver(env)
859859 err := cfg.configureProviders(testStore(cfg), env, resolver, knownProviders)
860860 require.NoError(t, err)
861861
@@ -889,7 +889,7 @@ func TestConfig_defaultModelSelection(t *testing.T) {
889889 cfg := &Config{}
890890 cfg.setDefaults("/tmp", "")
891891 env := env.NewFromMap(map[string]string{})
892− resolver := NewEnvironmentVariableResolver(env)
892+ resolver := NewShellVariableResolver(env)
893893 err := cfg.configureProviders(testStore(cfg), env, resolver, knownProviders)
894894 require.NoError(t, err)
895895
@@ -925,7 +925,7 @@ func TestConfig_defaultModelSelection(t *testing.T) {
925925 cfg := &Config{}
926926 cfg.setDefaults("/tmp", "")
927927 env := env.NewFromMap(map[string]string{})
928− resolver := NewEnvironmentVariableResolver(env)
928+ resolver := NewShellVariableResolver(env)
929929 err := cfg.configureProviders(testStore(cfg), env, resolver, knownProviders)
930930 require.NoError(t, err)
931931
@@ -955,7 +955,7 @@ func TestConfig_defaultModelSelection(t *testing.T) {
955955 cfg := &Config{}
956956 cfg.setDefaults("/tmp", "")
957957 env := env.NewFromMap(map[string]string{})
958− resolver := NewEnvironmentVariableResolver(env)
958+ resolver := NewShellVariableResolver(env)
959959 err := cfg.configureProviders(testStore(cfg), env, resolver, knownProviders)
960960 require.NoError(t, err)
961961 _, _, err = cfg.defaultModelSelection(knownProviders)
@@ -998,7 +998,7 @@ func TestConfig_defaultModelSelection(t *testing.T) {
998998 }
999999 cfg.setDefaults("/tmp", "")
10001000 env := env.NewFromMap(map[string]string{})
1001− resolver := NewEnvironmentVariableResolver(env)
1001+ resolver := NewShellVariableResolver(env)
10021002 err := cfg.configureProviders(testStore(cfg), env, resolver, knownProviders)
10031003 require.NoError(t, err)
10041004 large, small, err := cfg.defaultModelSelection(knownProviders)
@@ -1042,7 +1042,7 @@ func TestConfig_defaultModelSelection(t *testing.T) {
10421042 }
10431043 cfg.setDefaults("/tmp", "")
10441044 env := env.NewFromMap(map[string]string{})
1045− resolver := NewEnvironmentVariableResolver(env)
1045+ resolver := NewShellVariableResolver(env)
10461046 err := cfg.configureProviders(testStore(cfg), env, resolver, knownProviders)
10471047 require.NoError(t, err)
10481048 _, _, err = cfg.defaultModelSelection(knownProviders)
@@ -1084,7 +1084,7 @@ func TestConfig_defaultModelSelection(t *testing.T) {
10841084 }
10851085 cfg.setDefaults("/tmp", "")
10861086 env := env.NewFromMap(map[string]string{})
1087− resolver := NewEnvironmentVariableResolver(env)
1087+ resolver := NewShellVariableResolver(env)
10881088 err := cfg.configureProviders(testStore(cfg), env, resolver, knownProviders)
10891089 require.NoError(t, err)
10901090 large, small, err := cfg.defaultModelSelection(knownProviders)
@@ -1129,7 +1129,7 @@ func TestConfig_configureProvidersDisableDefaultProviders(t *testing.T) {
11291129 env := env.NewFromMap(map[string]string{
11301130 "OPENAI_API_KEY": "test-key",
11311131 })
1132− resolver := NewEnvironmentVariableResolver(env)
1132+ resolver := NewShellVariableResolver(env)
11331133 err := cfg.configureProviders(testStore(cfg), env, resolver, knownProviders)
11341134 require.ErrorContains(t, err, "no custom providers")
11351135
@@ -1172,7 +1172,7 @@ func TestConfig_configureProvidersDisableDefaultProviders(t *testing.T) {
11721172 "MY_API_KEY": "test-key",
11731173 "OPENAI_API_KEY": "test-key",
11741174 })
1175− resolver := NewEnvironmentVariableResolver(env)
1175+ resolver := NewShellVariableResolver(env)
11761176 err := cfg.configureProviders(testStore(cfg), env, resolver, knownProviders)
11771177 require.NoError(t, err)
11781178
@@ -1226,7 +1226,7 @@ func TestConfig_configureProvidersDisableDefaultProviders(t *testing.T) {
12261226 "OPENAI_API_KEY": "test-key",
12271227 "ANTHROPIC_API_KEY": "test-key",
12281228 })
1229− resolver := NewEnvironmentVariableResolver(env)
1229+ resolver := NewShellVariableResolver(env)
12301230 err := cfg.configureProviders(testStore(cfg), env, resolver, knownProviders)
12311231 require.NoError(t, err)
12321232
@@ -1254,7 +1254,7 @@ func TestConfig_configureProvidersDisableDefaultProviders(t *testing.T) {
12541254 cfg.setDefaults("/tmp", "")
12551255
12561256 env := env.NewFromMap(map[string]string{})
1257− resolver := NewEnvironmentVariableResolver(env)
1257+ resolver := NewShellVariableResolver(env)
12581258 err := cfg.configureProviders(testStore(cfg), env, resolver, []catwalk.Provider{})
12591259 require.ErrorContains(t, err, "no custom providers")
12601260
@@ -1278,7 +1278,7 @@ func TestConfig_configureProvidersDisableDefaultProviders(t *testing.T) {
12781278 cfg.setDefaults("/tmp", "")
12791279
12801280 env := env.NewFromMap(map[string]string{})
1281− resolver := NewEnvironmentVariableResolver(env)
1281+ resolver := NewShellVariableResolver(env)
12821282 err := cfg.configureProviders(testStore(cfg), env, resolver, []catwalk.Provider{})
12831283 require.ErrorContains(t, err, "no custom providers")
12841284
@@ -1336,7 +1336,7 @@ func TestConfig_configureSelectedModels(t *testing.T) {
13361336 cfg.setDefaults(dir, "")
13371337 store := &ConfigStore{config: cfg, globalDataPath: globalPath}
13381338 env := env.NewFromMap(map[string]string{})
1339− resolver := NewEnvironmentVariableResolver(env)
1339+ resolver := NewShellVariableResolver(env)
13401340 err := cfg.configureProviders(store, env, resolver, knownProviders)
13411341 require.NoError(t, err)
13421342
@@ -1386,7 +1386,7 @@ func TestConfig_configureSelectedModels(t *testing.T) {
13861386 }
13871387 cfg.setDefaults("/tmp", "")
13881388 env := env.NewFromMap(map[string]string{})
1389− resolver := NewEnvironmentVariableResolver(env)
1389+ resolver := NewShellVariableResolver(env)
13901390 err := cfg.configureProviders(testStore(cfg), env, resolver, knownProviders)
13911391 require.NoError(t, err)
13921392
@@ -1448,7 +1448,7 @@ func TestConfig_configureSelectedModels(t *testing.T) {
14481448 }
14491449 cfg.setDefaults("/tmp", "")
14501450 env := env.NewFromMap(map[string]string{})
1451− resolver := NewEnvironmentVariableResolver(env)
1451+ resolver := NewShellVariableResolver(env)
14521452 err := cfg.configureProviders(testStore(cfg), env, resolver, knownProviders)
14531453 require.NoError(t, err)
14541454
@@ -1493,7 +1493,7 @@ func TestConfig_configureSelectedModels(t *testing.T) {
14931493 }
14941494 cfg.setDefaults("/tmp", "")
14951495 env := env.NewFromMap(map[string]string{})
1496− resolver := NewEnvironmentVariableResolver(env)
1496+ resolver := NewShellVariableResolver(env)
14971497 err := cfg.configureProviders(testStore(cfg), env, resolver, knownProviders)
14981498 require.NoError(t, err)
14991499
@@ -1532,7 +1532,7 @@ func TestConfig_configureProviders_HyperAPIKeyFromEnv(t *testing.T) {
15321532 env := env.NewFromMap(map[string]string{
15331533 "HYPER_API_KEY": "env-api-key",
15341534 })
1535− resolver := NewEnvironmentVariableResolver(env)
1535+ resolver := NewShellVariableResolver(env)
15361536 err := cfg.configureProviders(testStore(cfg), env, resolver, knownProviders)
15371537 require.NoError(t, err)
15381538 require.Equal(t, 1, cfg.Providers.Len())
@@ -1579,7 +1579,7 @@ func TestConfig_configureProviders_HyperAPIKeyFromConfigOverrides(t *testing.T)
15791579 env := env.NewFromMap(map[string]string{
15801580 "HYPER_API_KEY": "env-api-key",
15811581 })
1582− resolver := NewEnvironmentVariableResolver(env)
1582+ resolver := NewShellVariableResolver(env)
15831583 err := cfg.configureProviders(testStore(cfg), env, resolver, knownProviders)
15841584 require.NoError(t, err)
15851585 require.Equal(t, 1, cfg.Providers.Len())
@@ -1590,18 +1590,13 @@ func TestConfig_configureProviders_HyperAPIKeyFromConfigOverrides(t *testing.T)
15901590 require.Equal(t, "env-api-key", pc.APIKey)
15911591}
15921592
1593−// TestConfig_configureProviders_ProviderHeaderResolveFailure pins the
1594−// intentional divergence at load.go:225: a provider header whose
1595−// expansion fails must NOT fail the whole config load. It logs an
1596−// error, keeps the literal template in the resolved header map, and
1597−// moves on. The MCP contract (hard error on failed expansion) does not
1598−// apply here because many providers ship DefaultHeaders that reference
1599−// env vars which are legitimately unset on most hosts.
1600−//
1601−// If this test ever flips to requiring an error, read PLAN.md "Design
1602−// decisions" item 4 before changing the production code — the
1603−// divergence is deliberate.
1604−func TestConfig_configureProviders_ProviderHeaderResolveFailure(t *testing.T) {
1593+// TestConfig_configureProviders_ProviderHeaderResolveError pins
1594+// Phase 2 design decision #14: a failing $(cmd) in a provider header
1595+// must fail the provider load with a clear message that names the
1596+// offending header. The Phase 1 log-and-continue divergence at
1597+// load.go:225 is gone; provider headers now share the MCP error
1598+// contract.
1599+func TestConfig_configureProviders_ProviderHeaderResolveError(t *testing.T) {
16051600 knownProviders := []catwalk.Provider{
16061601 {
16071602 ID: "openai",
@@ -1615,13 +1610,9 @@ func TestConfig_configureProviders_ProviderHeaderResolveFailure(t *testing.T) {
16151610 Providers: csync.NewMapFrom(map[string]ProviderConfig{
16161611 "openai": {
16171612 ExtraHeaders: map[string]string{
1618− // Failing $(...) — inner command exits 1, no stdout.
1613+ // Failing $(...) — inner command exits 1. Must
1614+ // propagate as an error, not a silent truncation.
16191615 "X-Broken": "$(false)",
1620− // Unset var — nounset makes this an error too.
1621− "X-Missing": "$PROVIDER_HEADER_NEVER_SET",
1622− // Happy path: must still be resolved, proving the
1623− // failure in the other two did not abort the loop.
1624− "X-Static": "kept-literal",
16251616 },
16261617 },
16271618 }),
@@ -1635,15 +1626,238 @@ func TestConfig_configureProviders_ProviderHeaderResolveFailure(t *testing.T) {
16351626 resolver := NewShellVariableResolver(testEnv)
16361627
16371628 err := cfg.configureProviders(testStore(cfg), testEnv, resolver, knownProviders)
1638− require.NoError(t, err, "provider load must tolerate failing header expansion")
1629+ require.Error(t, err, "failing $(cmd) in a header must fail the provider load")
1630+ require.Contains(t, err.Error(), "X-Broken", "error must name the offending header")
1631+}
1632+
1633+// TestConfig_configureProviders_CatwalkDefaultWithUnsetVarLoads pins
1634+// Phase 2 design decisions #11 and #18 from the provider angle: a
1635+// Catwalk-style default header like
1636+// "OpenAI-Organization": "$OPENAI_ORG_ID" must load cleanly under
1637+// lenient nounset (unset → "" → header dropped), not fail the load
1638+// and not leave the literal template on the wire.
1639+func TestConfig_configureProviders_CatwalkDefaultWithUnsetVarLoads(t *testing.T) {
1640+ knownProviders := []catwalk.Provider{
1641+ {
1642+ ID: "openai",
1643+ APIKey: "$OPENAI_API_KEY",
1644+ APIEndpoint: "https://api.openai.com/v1",
1645+ Models: []catwalk.Model{{ID: "test-model"}},
1646+ DefaultHeaders: map[string]string{
1647+ "OpenAI-Organization": "$OPENAI_ORG_ID",
1648+ },
1649+ },
1650+ }
1651+
1652+ cfg := &Config{}
1653+ cfg.setDefaults("/tmp", "")
1654+
1655+ testEnv := env.NewFromMap(map[string]string{
1656+ "OPENAI_API_KEY": "test-key",
1657+ "PATH": os.Getenv("PATH"),
1658+ })
1659+ resolver := NewShellVariableResolver(testEnv)
1660+
1661+ err := cfg.configureProviders(testStore(cfg), testEnv, resolver, knownProviders)
1662+ require.NoError(t, err, "optional env-gated header must not fail the load")
16391663
16401664 pc, ok := cfg.Providers.Get("openai")
16411665 require.True(t, ok, "openai provider must still be configured")
1666+ _, present := pc.ExtraHeaders["OpenAI-Organization"]
1667+ require.False(t, present, "header whose value resolves to empty must be absent")
1668+}
1669+
1670+// TestConfig_configureProviders_LiteralEmptyHeaderDropped pins design
1671+// decision #18 for the literal case: a user-authored
1672+// "X-Custom": "" in extra_headers is absent from the resolved map.
1673+// Applies to both known- and custom-provider paths; this test
1674+// exercises the custom-provider loop.
1675+func TestConfig_configureProviders_LiteralEmptyHeaderDropped(t *testing.T) {
1676+ cfg := &Config{
1677+ Providers: csync.NewMapFrom(map[string]ProviderConfig{
1678+ "my-llm": {
1679+ APIKey: "test-key",
1680+ BaseURL: "https://my-llm.example.com/v1",
1681+ Type: catwalk.TypeOpenAI,
1682+ Models: []catwalk.Model{{ID: "m"}},
1683+ ExtraHeaders: map[string]string{
1684+ "X-Custom": "",
1685+ "X-Kept": "present",
1686+ },
1687+ },
1688+ }),
1689+ }
1690+ cfg.setDefaults("/tmp", "")
1691+
1692+ testEnv := env.NewFromMap(map[string]string{
1693+ "PATH": os.Getenv("PATH"),
1694+ })
1695+ resolver := NewShellVariableResolver(testEnv)
1696+
1697+ err := cfg.configureProviders(testStore(cfg), testEnv, resolver, []catwalk.Provider{})
1698+ require.NoError(t, err)
1699+
1700+ pc, ok := cfg.Providers.Get("my-llm")
1701+ require.True(t, ok)
1702+ _, present := pc.ExtraHeaders["X-Custom"]
1703+ require.False(t, present, "literal empty-string header must be dropped")
1704+ require.Equal(t, "present", pc.ExtraHeaders["X-Kept"])
1705+}
1706+
1707+// TestConfig_configureProviders_EchoEmptyHeaderDropped pins design
1708+// decision #18 for the non-failing empty case: $(echo) exits 0 with
1709+// empty output, resolves cleanly to "", and must be dropped the same
1710+// way an unset bare $VAR is. Exercises the known-provider loop.
1711+func TestConfig_configureProviders_EchoEmptyHeaderDropped(t *testing.T) {
1712+ knownProviders := []catwalk.Provider{
1713+ {
1714+ ID: "openai",
1715+ APIKey: "$OPENAI_API_KEY",
1716+ APIEndpoint: "https://api.openai.com/v1",
1717+ Models: []catwalk.Model{{ID: "test-model"}},
1718+ DefaultHeaders: map[string]string{
1719+ "X-Empty": "$(echo)",
1720+ "X-Kept": "present",
1721+ },
1722+ },
1723+ }
1724+
1725+ cfg := &Config{}
1726+ cfg.setDefaults("/tmp", "")
1727+
1728+ testEnv := env.NewFromMap(map[string]string{
1729+ "OPENAI_API_KEY": "test-key",
1730+ "PATH": os.Getenv("PATH"),
1731+ })
1732+ resolver := NewShellVariableResolver(testEnv)
1733+
1734+ err := cfg.configureProviders(testStore(cfg), testEnv, resolver, knownProviders)
1735+ require.NoError(t, err)
1736+
1737+ pc, ok := cfg.Providers.Get("openai")
1738+ require.True(t, ok)
1739+ _, present := pc.ExtraHeaders["X-Empty"]
1740+ require.False(t, present, "$(echo) → empty → header must be dropped")
1741+ require.Equal(t, "present", pc.ExtraHeaders["X-Kept"])
1742+}
1743+
1744+// TestConfig_configureProviders_UnsetAPIKeySkipsProvider pins Phase 2
1745+// Step 12 / design decision #15: under the lenient-nounset shell
1746+// resolver, $UNSET_API_KEY expands to ("", nil) rather than ("", err),
1747+// and the existing `v == "" || err != nil` skip path at load.go:331
1748+// still drops the provider. The slog.Warn line is emitted on the same
1749+// path but is not asserted here — internal/config/load_test.go's
1750+// TestMain replaces the default slog handler with an io.Discard
1751+// writer, so capturing that log line would require mid-test handler
1752+// swapping and a sync.Mutex dance that adds more flake surface than
1753+// signal. The observable outcome (provider absent from the map) is
1754+// what downstream code — model picker, agent wiring — actually reads,
1755+// so that's what we pin.
1756+func TestConfig_configureProviders_UnsetAPIKeySkipsProvider(t *testing.T) {
1757+ knownProviders := []catwalk.Provider{
1758+ {
1759+ ID: "openai",
1760+ APIKey: "$SOMETHING_UNSET",
1761+ APIEndpoint: "https://api.openai.com/v1",
1762+ Models: []catwalk.Model{{ID: "test-model"}},
1763+ },
1764+ }
1765+
1766+ // Existing user config for this known provider so the load.go:332
1767+ // `if configExists` branch fires and actually calls Providers.Del.
1768+ // Without it the provider was never in the map to begin with and
1769+ // the test would pass trivially.
1770+ cfg := &Config{
1771+ Providers: csync.NewMapFrom(map[string]ProviderConfig{
1772+ "openai": {BaseURL: "custom-url"},
1773+ }),
1774+ }
1775+ cfg.setDefaults("/tmp", "")
1776+
1777+ testEnv := env.NewFromMap(map[string]string{
1778+ "PATH": os.Getenv("PATH"),
1779+ })
1780+ resolver := NewShellVariableResolver(testEnv)
1781+
1782+ err := cfg.configureProviders(testStore(cfg), testEnv, resolver, knownProviders)
1783+ require.NoError(t, err, "skip path must not surface as a load error")
1784+
1785+ require.Equal(t, 0, cfg.Providers.Len(), "provider with unset API key must be skipped")
1786+ _, exists := cfg.Providers.Get("openai")
1787+ require.False(t, exists)
1788+}
1789+
1790+// TestConfig_configureProviders_FailingAPIKeyCmdSkipsProvider pins
1791+// that the two failure modes for APIKey — ("", nil) from an unset var
1792+// under lenient nounset and ("", err) from a failing $(cmd) — are
1793+// equivalent for the skip outcome at load.go:331. The `v == "" ||
1794+// err != nil` check fires on either branch; this test locks in that
1795+// equivalence so a future refactor that splits the check into two
1796+// paths doesn't accidentally start propagating $(false) as a load
1797+// error while keeping unset-var as a silent skip (or vice versa).
1798+func TestConfig_configureProviders_FailingAPIKeyCmdSkipsProvider(t *testing.T) {
1799+ knownProviders := []catwalk.Provider{
1800+ {
1801+ ID: "openai",
1802+ APIKey: "$(false)",
1803+ APIEndpoint: "https://api.openai.com/v1",
1804+ Models: []catwalk.Model{{ID: "test-model"}},
1805+ },
1806+ }
1807+
1808+ cfg := &Config{
1809+ Providers: csync.NewMapFrom(map[string]ProviderConfig{
1810+ "openai": {BaseURL: "custom-url"},
1811+ }),
1812+ }
1813+ cfg.setDefaults("/tmp", "")
1814+
1815+ testEnv := env.NewFromMap(map[string]string{
1816+ "PATH": os.Getenv("PATH"),
1817+ })
1818+ resolver := NewShellVariableResolver(testEnv)
1819+
1820+ err := cfg.configureProviders(testStore(cfg), testEnv, resolver, knownProviders)
1821+ require.NoError(t, err, "failing $(cmd) in API key must skip provider, not fail load")
1822+
1823+ require.Equal(t, 0, cfg.Providers.Len(), "provider with failing $(cmd) API key must be skipped")
1824+ _, exists := cfg.Providers.Get("openai")
1825+ require.False(t, exists)
1826+}
1827+
1828+// TestConfig_configureProviders_UnsetAzureEndpointSkipsProvider pins
1829+// the same contract on the Azure path at load.go:287 — APIEndpoint is
1830+// the field that gates Azure and goes through the same
1831+// `v == "" || err != nil` skip check. Covered here so both branches
1832+// of the shared skip pattern (APIKey default path and APIEndpoint
1833+// Azure path) are tested; a future refactor that unifies them can
1834+// rely on these two tests to catch drift.
1835+func TestConfig_configureProviders_UnsetAzureEndpointSkipsProvider(t *testing.T) {
1836+ knownProviders := []catwalk.Provider{
1837+ {
1838+ ID: catwalk.InferenceProviderAzure,
1839+ APIKey: "test-key",
1840+ APIEndpoint: "$UNSET_AZURE_ENDPOINT",
1841+ Models: []catwalk.Model{{ID: "test-model"}},
1842+ },
1843+ }
1844+
1845+ cfg := &Config{
1846+ Providers: csync.NewMapFrom(map[string]ProviderConfig{
1847+ "azure": {BaseURL: ""},
1848+ }),
1849+ }
1850+ cfg.setDefaults("/tmp", "")
1851+
1852+ testEnv := env.NewFromMap(map[string]string{
1853+ "PATH": os.Getenv("PATH"),
1854+ })
1855+ resolver := NewShellVariableResolver(testEnv)
1856+
1857+ err := cfg.configureProviders(testStore(cfg), testEnv, resolver, knownProviders)
1858+ require.NoError(t, err)
16421859
1643− // Literal template preserved for the two failing headers; the
1644− // happy-path header is resolved through the shell (pass-through
1645− // for a literal value).
1646− require.Equal(t, "$(false)", pc.ExtraHeaders["X-Broken"])
1647− require.Equal(t, "$PROVIDER_HEADER_NEVER_SET", pc.ExtraHeaders["X-Missing"])
1648− require.Equal(t, "kept-literal", pc.ExtraHeaders["X-Static"])
1860+ require.Equal(t, 0, cfg.Providers.Len(), "azure provider with unset endpoint must be skipped")
1861+ _, exists := cfg.Providers.Get("azure")
1862+ require.False(t, exists)
16491863}