//go:build unix // These scenarios drive the detached CLI and a real daemon: a unit handler test // cannot catch leaked daemon keys, wrong child argv, or stale restart discovery. package e2e import ( "context" "encoding/json" "io" "net" "net/http" "os" "path/filepath" "runtime" "slices" "strconv" "strings" "testing" "time" "albedo/cli/internal/daemon" "albedo/cli/internal/webui" ) func proxyExtension(t *testing.T, session string, enabled bool) { t.Helper() settings, err := daemon.GetSettings(context.Background(), conn(t)) if err != nil { t.Fatal(err) } if _, err := daemon.SelectExtension(context.Background(), conn(t), session, daemon.ExtensionSelectionRequest{Name: "proxy", Scope: "global", ETag: settings.ETags["extensions"], Enabled: &enabled}); err != nil { t.Fatal(err) } } func proxyCall(t *testing.T, base, method, path, key, body string) (int, string) { t.Helper() request, err := http.NewRequestWithContext(t.Context(), method, base+path, strings.NewReader(body)) if err != nil { t.Fatal(err) } if key != "" { request.Header.Set("Authorization", "Bearer "+key) } if body != "" { request.Header.Set("Content-Type", "application/json") } client := &http.Client{Timeout: 15 * time.Second} response, err := client.Do(request) if err != nil { t.Fatal(err) } defer response.Body.Close() raw, err := io.ReadAll(response.Body) if err != nil { t.Fatal(err) } return response.StatusCode, string(raw) } func proxyRecord(t *testing.T) webui.Manifest { t.Helper() raw, err := os.ReadFile(filepath.Join(suite.home, "proxy.json")) if err != nil { t.Fatal(err) } var record webui.Manifest if err := json.Unmarshal(raw, &record); err != nil { t.Fatal(err) } return record } func TestProxyBackgroundScopesKeyAndRecoversAfterDaemonRestart(t *testing.T) { profile := providerRoute(t, echoReply) session := newSession(t, t.TempDir()) proxyExtension(t, session, false) off := cli(t, "proxy") if off != "the proxy extension is off. turn it on in /extensions, then run albedo proxy again.\n" { t.Fatalf("off: %q", off) } if _, err := os.Stat(filepath.Join(suite.home, "proxy-key")); !os.IsNotExist(err) { t.Fatalf("disabled proxy created key: %v", err) } proxyExtension(t, session, true) t.Cleanup(func() { cli(t, "proxy", "--stop") proxyExtension(t, session, false) }) started := cli(t, "proxy", "--port", "0") record := proxyRecord(t) keyPath := filepath.Join(suite.home, "proxy-key") rawKey, err := os.ReadFile(keyPath) if err != nil { t.Fatal(err) } key := string(rawKey) if info, err := os.Stat(keyPath); err != nil || info.Mode().Perm() != 0o600 { t.Fatalf("key permissions: %v %v", info, err) } for _, text := range []string{"running in the background", "stop it with: albedo proxy --stop", record.URL + "/v1", key, "openai-compatible", "/fixture-model"} { if !strings.Contains(started, text) { t.Fatalf("summary missing %q: %s", text, started) } } if strings.Contains(started, conn(t).Snapshot().Token) { t.Fatal("summary leaked daemon token") } if _, _, err := runCLI("proxy", "--port", "1"); err == nil { t.Fatal("running proxy accepted changed listen options") } repeated := cli(t, "proxy") if !strings.Contains(repeated, "was already running since") || proxyRecord(t).PID != record.PID { t.Fatalf("repeat replaced server: %s", repeated) } for _, candidate := range []string{"", "wrong", conn(t).Snapshot().Token} { if code, _ := proxyCall(t, record.URL, "GET", "/v1/models", candidate, ""); code != 401 { t.Errorf("key %q accepted: %d", candidate, code) } } if code, body := proxyCall(t, record.URL, "GET", "/v1/models", key, ""); code != 200 || !strings.Contains(body, profile+"/fixture-model") { t.Fatalf("models: %d %s", code, body) } for _, path := range []string{"/server", "/api/settings", "/extensions/proxy/v1/models", "/v1/models/../settings", "/v1/%6dodels"} { if code, _ := proxyCall(t, record.URL, "GET", path, key, ""); code != 404 { t.Errorf("non-proxy route %s: %d", path, code) } } if code, _ := proxyCall(t, record.URL, "POST", "/v1/models", key, "{}"); code != 405 { t.Fatalf("models accepted wrong method: %d", code) } body := `{"model":"` + profile + `/fixture-model","messages":[{"role":"user","content":"proxy test"}]}` if code, reply := proxyCall(t, record.URL, "POST", "/v1/chat/completions", key, body); code != 200 || !strings.Contains(reply, "echo: proxy test") { t.Fatalf("completion: %d %s", code, reply) } streaming := strings.Replace(body, `"messages":`, `"stream":true,"messages":`, 1) if code, reply := proxyCall(t, record.URL, "POST", "/v1/chat/completions", key, streaming); code != 200 || !strings.Contains(reply, "data: [DONE]") { t.Fatalf("stream: %d %s", code, reply) } // Rotation revokes the old key without replacing the listener. rotated := cli(t, "proxy", "--new-key") newRaw, err := os.ReadFile(keyPath) if err != nil { t.Fatal(err) } newKey := string(newRaw) if newKey == key || !strings.Contains(rotated, newKey) || proxyRecord(t).PID != record.PID { t.Fatal("rotation did not keep listener and replace key") } if code, _ := proxyCall(t, record.URL, "GET", "/v1/models", key, ""); code != 401 { t.Fatalf("old key accepted: %d", code) } if code, _ := proxyCall(t, record.URL, "GET", "/v1/models", newKey, ""); code != 200 { t.Fatalf("new key refused: %d", code) } // The forwarder stays up while the daemon is down and never restarts it. oldPID := suite.daemonPID if err := daemon.StopDaemon(context.Background(), conn(t), false); err != nil { t.Fatal(err) } if !awaitExit(oldPID, 15*time.Second) { t.Fatal("daemon did not stop") } if code, reply := proxyCall(t, record.URL, "GET", "/v1/models", newKey, ""); code != 503 || !strings.Contains(reply, "the albedo daemon isn't running on this machine") { t.Fatalf("daemon down: %d %s", code, reply) } cli(t, "sessions") snapshot, err := awaitDaemon(suite.home) if err != nil { t.Fatal(err) } suite.daemonPID = snapshot.Pid if err := conn(t).Refresh(t.Context()); err != nil { t.Fatal(err) } // One request may discover the new endpoint; it is not replayed automatically. proxyCall(t, record.URL, "GET", "/v1/models", newKey, "") if code, reply := proxyCall(t, record.URL, "POST", "/v1/chat/completions", newKey, body); code != 200 || !strings.Contains(reply, "echo: proxy test") { t.Fatalf("restart: %d %s", code, reply) } log, err := os.ReadFile(filepath.Join(suite.home, "proxy.log")) if err != nil || !strings.Contains(string(log), profile+"/fixture-model") || strings.Contains(string(log), key) || strings.Contains(string(log), snapshot.Token) { t.Fatalf("request log missing or leaked key: %v", err) } stopped := cli(t, "proxy", "--stop") if !strings.Contains(stopped, "stopped albedo proxy on "+record.URL+"/v1") { t.Fatalf("stop: %s", stopped) } if got := cli(t, "proxy", "--stop"); got != "albedo proxy is not running.\n" { t.Fatalf("second stop: %s", got) } // A normal restart preserves the client key. cli(t, "proxy", "--port", "0", "--quiet") persisted, err := os.ReadFile(keyPath) if err != nil || string(persisted) != newKey { t.Fatal("restart changed key") } quietRecord := proxyRecord(t) proxyCall(t, quietRecord.URL, "GET", "/v1/models", newKey, "") if log, err := os.ReadFile(filepath.Join(suite.home, "proxy.log")); err != nil || len(log) != 0 { t.Fatalf("quiet request logged: %q %v", log, err) } } func TestProxySaveUsesOwnNetworksAndForgetsThem(t *testing.T) { loginManager(t) providerRoute(t, echoReply) addresses, err := webui.NetworkAddresses() if err != nil { t.Fatal(err) } if len(addresses) == 0 { t.Skip("no non-loopback IPv4 address") } networks := []string{addresses[0] + "/32", "127.0.0.0/8"} session := newSession(t, t.TempDir()) proxyExtension(t, session, true) settings, err := daemon.GetSettings(t.Context(), conn(t)) if err != nil { t.Fatal(err) } webNetworks := []string{"192.0.2.0/24"} if err := daemon.SaveWebAllow(t.Context(), conn(t), settings.UI.ETag, webNetworks); err != nil { t.Fatal(err) } t.Cleanup(func() { cli(t, "proxy", "--save") ctx := context.Background() current, err := daemon.GetSettings(ctx, conn(t)) if err != nil { t.Fatal(err) } if err := daemon.SaveProxyAllow(ctx, conn(t), current.UI.ETag, settings.UI.ProxyAllow); err != nil { t.Fatal(err) } current, err = daemon.GetSettings(ctx, conn(t)) if err != nil { t.Fatal(err) } if err := daemon.SaveWebAllow(ctx, conn(t), current.UI.ETag, settings.UI.WebAllow); err != nil { t.Fatal(err) } proxyExtension(t, session, false) }) checkSaved := func(want []string) { t.Helper() current, err := daemon.GetSettings(t.Context(), conn(t)) if err != nil { t.Fatal(err) } if !slices.Equal(current.UI.ProxyAllow, want) || !slices.Equal(current.UI.WebAllow, webNetworks) { t.Fatalf("saved proxy %v, web %v; want proxy %v, web %v", current.UI.ProxyAllow, current.UI.WebAllow, want, webNetworks) } } cli(t, "proxy", "--port", "0", "--allow", networks[0], "--allow", networks[1], "--save") checkSaved(networks) first := proxyRecord(t) failure := filepath.Join(suite.home, "service-stub-fail") if err := os.WriteFile(failure, nil, 0o600); err != nil { t.Fatal(err) } if _, _, err := runCLI("proxy", "--stop"); err == nil { t.Fatal("manager failure reported a successful stop") } if err := os.Remove(failure); err != nil { t.Fatal(err) } if !webui.ManifestAlive(first) { t.Fatal("manager failure killed listener outside the manager") } cli(t, "proxy") if second := proxyRecord(t); second.PID != first.PID { t.Fatal("bare start duplicated the managed listener") } // Updating the service works; invalid input must not change its saved networks. cli(t, "proxy", "--port", "0", "--allow", networks[0], "--allow", networks[1], "--save") if _, _, err := runCLI("proxy", "--allow", "invalid", "--save"); err == nil { t.Fatal("accepted invalid save") } checkSaved(networks) cli(t, "proxy", "--stop") cli(t, "proxy") record := proxyRecord(t) if !slices.Equal(record.Allow, networks) || !slices.Contains(record.Addresses, addresses[0]) { t.Fatalf("bare start ignored saved networks: %+v", record) } key, err := os.ReadFile(filepath.Join(suite.home, "proxy-key")) if err != nil { t.Fatal(err) } shared := "http://" + net.JoinHostPort(addresses[0], strconv.Itoa(record.Port)) if code, body := proxyCall(t, shared, "GET", "/v1/models", string(key), ""); code != http.StatusOK { t.Fatalf("shared listener: %d %s", code, body) } cli(t, "proxy", "--stop") cli(t, "proxy", "--save") checkSaved(nil) if _, err := os.Stat(filepath.Join(suite.home, "proxy.json")); !os.IsNotExist(err) { t.Fatalf("forget left listener: %v", err) } raw, err := os.ReadFile(filepath.Join(suite.home, "service-stub.log")) if err != nil { t.Fatal(err) } for _, operation := range []string{"start", "stop"} { if runtime.GOOS == "darwin" { operation = map[string]string{"start": "bootstrap", "stop": "bootout"}[operation] } if !strings.Contains(string(raw), operation) { t.Fatalf("manager never received %s: %s", operation, raw) } } state, err := os.ReadFile(filepath.Join(suite.home, "proxy-service-stub.json")) if err == nil { t.Fatalf("forget left managed process: %s", state) } cli(t, "proxy") if record := proxyRecord(t); len(record.Allow) != 0 || len(record.Addresses) != 0 { t.Fatalf("bare start restored forgotten networks: %+v", record) } }