TestAIBridgeProviderHotReload exercises the end-to-end CRUD -> reload -> routing path: every provider mutation made through codersdk must, within a short window, change the routing observed at api/v2/aibridge/{name}/v1/models. The OpenAI passthrough route v1/models reverse-proxies to BaseURL, so the
(t *testing.T)
| 113 | // /v1/models reverse-proxies to BaseURL, so the upstream that responds |
| 114 | // identifies which provider the daemon's mux dispatched to. |
| 115 | func TestAIBridgeProviderHotReload(t *testing.T) { |
| 116 | t.Parallel() |
| 117 | |
| 118 | // Two distinct upstreams so an Update that swings the BaseURL is |
| 119 | // observable: which upstream answers tells us which BaseURL the |
| 120 | // freshly-built provider is pointed at. |
| 121 | upstreamA := newMockUpstream(t, "a") |
| 122 | upstreamB := newMockUpstream(t, "b") |
| 123 | |
| 124 | dv := coderdtest.DeploymentValues(t) |
| 125 | dv.AI.BridgeConfig.Enabled = serpent.Bool(true) |
| 126 | |
| 127 | client, _, api, _ := coderdenttest.NewWithAPI(t, &coderdenttest.Options{ |
| 128 | Options: &coderdtest.Options{DeploymentValues: dv}, |
| 129 | LicenseOptions: &coderdenttest.LicenseOptions{ |
| 130 | Features: license.Features{codersdk.FeatureAIBridge: 1}, |
| 131 | }, |
| 132 | }) |
| 133 | |
| 134 | metrics := startTestAIBridgeDaemon(t, api.AGPL) |
| 135 | |
| 136 | // requireProviderStatus polls until the provider_info series for |
| 137 | // (name, status) settles to value 1. Reloads happen via pubsub, so |
| 138 | // the assertion has to be eventual. |
| 139 | requireProviderStatus := func(t *testing.T, name, status string) { |
| 140 | t.Helper() |
| 141 | require.Eventuallyf(t, func() bool { |
| 142 | return promtest.ToFloat64(metrics.ProviderInfo.WithLabelValues(name, "openai", status)) == 1 |
| 143 | }, testutil.WaitShort, testutil.IntervalFast, |
| 144 | "expected provider_info{provider_name=%q, status=%q} == 1", name, status) |
| 145 | } |
| 146 | |
| 147 | // requireProviderAbsent polls until no series exists for the |
| 148 | // provider name in any status. After a delete the Reset on the |
| 149 | // next reload must clear all previous status labels for the name. |
| 150 | requireProviderAbsent := func(t *testing.T, name string) { |
| 151 | t.Helper() |
| 152 | require.Eventuallyf(t, func() bool { |
| 153 | for _, status := range []string{"enabled", "disabled", "error"} { |
| 154 | if promtest.ToFloat64(metrics.ProviderInfo.WithLabelValues(name, "openai", status)) != 0 { |
| 155 | return false |
| 156 | } |
| 157 | } |
| 158 | return true |
| 159 | }, testutil.WaitShort, testutil.IntervalFast, |
| 160 | "expected provider_info series for %q to be cleared after delete", name) |
| 161 | } |
| 162 | |
| 163 | ctx := testutil.Context(t, testutil.WaitLong) |
| 164 | |
| 165 | // sendRequest issues GET /api/v2/aibridge/{name}/v1/models and |
| 166 | // returns the status and the upstream marker decoded from the |
| 167 | // JSON body (empty if the body was not the marker JSON). |
| 168 | sendRequest := func(providerName string) (int, string) { |
| 169 | url := client.URL.String() + "/api/v2/aibridge/" + providerName + "/v1/models" |
| 170 | req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) |
| 171 | require.NoError(t, err) |
| 172 | req.Header.Set("Authorization", "Bearer "+client.SessionToken()) |
nothing calls this directly
no test coverage detected