okami.mom/albedo

git clone https://git.nkp.pet/okami.mom/albedo

dawnadd colab extension47735ec

main
3.8 KiB114 linesraw
1// A login response can arrive after Bubble Tea has stopped. A controlled peer
2// holds that response until actual terminal shutdown, which E2E cannot schedule.
3package terminal
4
5import (
6	"context"
7	"encoding/json"
8	"errors"
9	"io"
10	"net"
11	"net/http"
12	"net/http/httptest"
13	"os"
14	"strings"
15	"sync"
16	"sync/atomic"
17	"testing"
18	"time"
19
20	"albedo/cli/internal/app"
21	"albedo/cli/internal/daemon"
22	"albedo/cli/internal/testwire"
23)
24
25func TestLoginProgramExitCancelsCreationCompletedAfterShutdown(t *testing.T) {
26	started, cancelled := make(chan string, 1), make(chan string, 1)
27	release := make(chan struct{})
28	var releaseOnce sync.Once
29	releaseLogin := func() { releaseOnce.Do(func() { close(release) }) }
30	var opened, shutdown atomic.Bool
31	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
32		switch {
33		case r.URL.Path == "/server":
34			_, _ = io.WriteString(w, testwire.Server)
35		case r.URL.Path == "/settings":
36			_ = json.NewEncoder(w).Encode(testwire.Settings())
37		case r.URL.Path == "/auth":
38			_, _ = io.WriteString(w, `{"providers":[{"id":"provider","label":"Provider","detail":"","kind":"model","flows":["browser"],"fields":[]}],"accounts":[]}`)
39		case strings.HasPrefix(r.URL.Path, "/auth/logins/"):
40			id := strings.TrimPrefix(r.URL.Path, "/auth/logins/")
41			if r.Method == http.MethodDelete {
42				cancelled <- id
43				w.WriteHeader(http.StatusNoContent)
44				return
45			}
46			if r.Method != http.MethodPut {
47				t.Errorf("unexpected login request: %s", r.Method)
48				http.Error(w, "unexpected", 500)
49				return
50			}
51			started <- id
52			<-release
53			w.Header().Set("ETag", `"login-a"`)
54			w.WriteHeader(http.StatusCreated)
55			_ = json.NewEncoder(w).Encode(map[string]any{"id": id, "provider": "provider", "url": "https://provider.example/login", "expires_at": "2099-01-01T00:00:00Z", "state": "waiting", "instructions": nil, "progress": "waiting", "accounts": []any{}, "failure": nil})
56		case r.URL.Path == "/server/shutdown":
57			shutdown.Store(true)
58			http.Error(w, "unexpected shutdown", 500)
59		default:
60			t.Errorf("unexpected request: %s %s", r.Method, r.URL)
61			http.NotFound(w, r)
62		}
63	}))
64	t.Cleanup(server.Close)
65	t.Cleanup(releaseLogin)
66	conn, err := daemon.Attach(t.Context(), daemon.ConnectionSnapshot{Port: server.Listener.Addr().(*net.TCPAddr).Port, Token: "token"}, nil)
67	if err != nil {
68		t.Fatal(err)
69	}
70	t.Cleanup(conn.HTTPClient().CloseIdleConnections)
71	input, writer, err := os.Pipe()
72	if err != nil {
73		t.Fatal(err)
74	}
75	t.Cleanup(func() { input.Close(); writer.Close() })
76	service := Service{In: input, Out: io.Discard, OpenBrowser: func(string) { opened.Store(true) }}
77	ctx, cancel := context.WithCancel(t.Context())
78	t.Cleanup(cancel)
79	finished := make(chan error, 1)
80	go func() { finished <- service.Login(ctx, app.PreparedOpen{Connection: conn}, "", "provider") }()
81	var id string
82	select {
83	case id = <-started:
84	case err := <-finished:
85		t.Fatalf("login exited before creation began: %v", err)
86	case <-time.After(5 * time.Second):
87		t.Fatal("login creation did not start")
88	}
89	cancel()
90	select {
91	case err := <-finished:
92		if !errors.Is(err, context.Canceled) {
93			t.Fatalf("program cancellation lost: %v", err)
94		}
95	case <-time.After(5 * time.Second):
96		t.Fatal("program kept waiting for the held login response")
97	}
98	// The event loop has returned before the daemon finishes its admitted request.
99	releaseLogin()
100	select {
101	case got := <-cancelled:
102		if got != id {
103			t.Fatalf("cancelled %q instead of the created login %q", got, id)
104		}
105	case <-time.After(5 * time.Second):
106		t.Fatal("late login creation survived terminal shutdown")
107	}
108	if opened.Load() || shutdown.Load() {
109		t.Fatal("shutdown opened a browser or stopped the shared daemon")
110	}
111	if _, err := daemon.ProbeServer(t.Context(), conn); err != nil {
112		t.Fatalf("shared daemon stopped responding: %v", err)
113	}
114}