| 1 | package main |
| 2 | |
| 3 | import ( |
| 4 | "os" |
| 5 | "path/filepath" |
| 6 | "slices" |
| 7 | "testing" |
| 8 | |
| 9 | "reasonix/internal/config" |
| 10 | "reasonix/internal/netclient" |
| 11 | ) |
| 12 | |
| 13 | func TestNetworkProxySpecForRootMatchesEffectiveProjectConfig(t *testing.T) { |
| 14 | isolateDesktopUserDirs(t) |
| 15 | if err := os.MkdirAll(filepath.Dir(config.UserConfigPath()), 0o700); err != nil { |
| 16 | t.Fatal(err) |
| 17 | } |
| 18 | if err := os.WriteFile(config.UserConfigPath(), []byte("[network]\nproxy_mode = \"off\"\n"), 0o600); err != nil { |
| 19 | t.Fatal(err) |
| 20 | } |
| 21 | root := t.TempDir() |
| 22 | if err := os.WriteFile(filepath.Join(root, ".env"), []byte("PROJECT_PROXY_URL=http://127.0.0.1:9876\n"), 0o600); err != nil { |
| 23 | t.Fatal(err) |
| 24 | } |
| 25 | if err := os.WriteFile(filepath.Join(root, "reasonix.toml"), []byte("[network]\nproxy_mode = \"custom\"\nproxy_url = \"${PROJECT_PROXY_URL}\"\n"), 0o600); err != nil { |
| 26 | t.Fatal(err) |
| 27 | } |
| 28 | |
| 29 | spec := NewApp().networkProxySpecForRoot(root) |
| 30 | if spec.Mode != netclient.ModeCustom || spec.URL != "http://127.0.0.1:9876" { |
| 31 | t.Fatalf("model probe proxy = %+v, want effective project proxy", spec) |
| 32 | } |
| 33 | } |
| 34 | |
| 35 | // TestSaveProviderPersistsNoProxy pins the #9560 escape hatch: the custom |
| 36 | // provider editor's "connect directly" toggle must round-trip through the |
| 37 | // config entry so the provider host lands in the chat transport's direct list. |
| 38 | func TestSaveProviderPersistsNoProxy(t *testing.T) { |
| 39 | isolateDesktopUserDirs(t) |
| 40 | |
| 41 | app := NewApp() |
| 42 | if err := app.SaveProvider(ProviderView{ |
| 43 | Name: "custom-gateway", |
| 44 | Kind: "openai", |
| 45 | BaseURL: "https://gw.example.internal/v1", |
| 46 | Models: []string{"gw-model"}, |
| 47 | APIKeyEnv: "GW_API_KEY", |
| 48 | NoProxy: true, |
| 49 | }); err != nil { |
| 50 | t.Fatalf("SaveProvider: %v", err) |
| 51 | } |
| 52 | |
| 53 | cfg := config.LoadForEdit(config.UserConfigPath()) |
| 54 | got, ok := cfg.Provider("custom-gateway") |
| 55 | if !ok { |
| 56 | t.Fatal("saved provider not found") |
| 57 | } |
| 58 | if !got.NoProxy { |
| 59 | t.Fatal("saved provider no_proxy = false, want true") |
| 60 | } |
| 61 | view := providerViewFromEntry(*got, false, true) |
| 62 | if !view.NoProxy { |
| 63 | t.Fatal("provider view noProxy = false, want true") |
| 64 | } |
| 65 | if spec := cfg.NetworkProxySpec(); !slices.Contains(spec.DirectHosts, "gw.example.internal") { |
| 66 | t.Fatalf("NetworkProxySpec.DirectHosts = %v, want the no_proxy provider host", spec.DirectHosts) |
| 67 | } |
| 68 | } |
| 69 | |
| 70 | // TestWithProbeDirectHostMirrorsProviderNoProxy covers the unsaved-editor probe |
| 71 | // path: refreshing models for a no_proxy provider must bypass the proxy even |
| 72 | // before the entry is persisted, and a custom proxy mode must still win. |
| 73 | func TestWithProbeDirectHostMirrorsProviderNoProxy(t *testing.T) { |
| 74 | auto := netclient.ProxySpec{Mode: netclient.ModeAuto, DirectHosts: []string{"preset.example.cn"}} |
| 75 | got := withProbeDirectHost(auto, "https://gw.example.internal/v1", true) |
| 76 | if !slices.Contains(got.DirectHosts, "gw.example.internal") { |
| 77 | t.Fatalf("probe DirectHosts = %v, want the edited provider host", got.DirectHosts) |
| 78 | } |
| 79 | if !slices.Contains(got.DirectHosts, "preset.example.cn") { |
| 80 | t.Fatalf("probe DirectHosts = %v, lost existing entries", got.DirectHosts) |
| 81 | } |
| 82 | |
| 83 | custom := netclient.ProxySpec{Mode: netclient.ModeCustom, URL: "http://corp-proxy.internal:3128"} |
| 84 | if got := withProbeDirectHost(custom, "https://gw.example.internal/v1", true); len(got.DirectHosts) != 0 { |
| 85 | t.Fatalf("custom proxy must override provider no_proxy, got DirectHosts %v", got.DirectHosts) |
| 86 | } |
| 87 | |
| 88 | direct := withProbeDirectHost(auto, "https://gw.example.internal/v1", false) |
| 89 | if slices.Contains(direct.DirectHosts, "gw.example.internal") { |
| 90 | t.Fatalf("provider without no_proxy must stay proxied, got DirectHosts %v", direct.DirectHosts) |
| 91 | } |
| 92 | |
| 93 | ipv6 := withProbeDirectHost(auto, "https://[2001:db8::1]:8443/v1", true) |
| 94 | if !slices.Contains(ipv6.DirectHosts, "2001:db8::1") { |
| 95 | t.Fatalf("IPv6 provider host = %v, want brackets and port removed", ipv6.DirectHosts) |
| 96 | } |
| 97 | } |
| 98 |