fix(review round 1): daemon PAT-header auth, job pruning, session-split batches, streaming tars, seed cleanup, ping/pong, byte caps, branch switch, slug --, ctx cancel, Init:true, pagination, latest/before events; web newest-window history + load-older, offline busy gate, staleness guards, memo store, auth probe, scroll key, focus-visible, draft restore, poll dedup; 106+181 tests, coverage 96.2%/95.1%+
This commit is contained in:
@@ -491,3 +491,226 @@ func TestWebClientDeliverToDropped(t *testing.T) {
|
||||
t.Fatalf("dropped client queued %d events, want %d", len(c.events), webMaxPending)
|
||||
}
|
||||
}
|
||||
|
||||
func TestWebClientByteOverflowDrops(t *testing.T) {
|
||||
hub := NewHub(openTestStore(t))
|
||||
c := &webClient{hub: hub, conn: throwawayConn(t), done: make(chan struct{})}
|
||||
// stay under the frame-count cap; cross the byte cap on the last event.
|
||||
meg := make([]byte, 1<<20)
|
||||
var i int
|
||||
for ; (i+1)*(1<<20) <= webMaxPendingByte; i++ {
|
||||
c.deliverEvent(pendingEvent{sessionID: "s", seq: int64(i), raw: meg})
|
||||
}
|
||||
if c.dropped {
|
||||
t.Fatalf("client dropped at %dMiB, want only beyond %dMiB", i, webMaxPendingByte>>20)
|
||||
}
|
||||
c.deliverEvent(pendingEvent{sessionID: "s", seq: int64(i), raw: meg}) // one over the cap
|
||||
select {
|
||||
case <-c.done:
|
||||
case <-time.After(2 * time.Second):
|
||||
t.Fatal("byte-overflowed web client was not dropped")
|
||||
}
|
||||
if !c.dropped {
|
||||
t.Fatal("client must be marked dropped on byte overflow")
|
||||
}
|
||||
}
|
||||
|
||||
func TestUnregisterReplacedConnKeepsOnline(t *testing.T) {
|
||||
store := openTestStore(t)
|
||||
hub := NewHub(store)
|
||||
if err := store.UpsertSession(SessionInfo{ID: "s1", Cwd: "/w", Model: "m", Provider: "p"}); err != nil {
|
||||
t.Fatalf("upsert: %v", err)
|
||||
}
|
||||
if err := store.SetOnline("s1", true); err != nil {
|
||||
t.Fatalf("online: %v", err)
|
||||
}
|
||||
old := &agentConn{hub: hub, conn: throwawayConn(t), send: make(chan []byte, 1), done: make(chan struct{})}
|
||||
old.sessionID = "s1"
|
||||
fresh := &agentConn{hub: hub, conn: throwawayConn(t), send: make(chan []byte, 1), done: make(chan struct{})}
|
||||
fresh.sessionID = "s1"
|
||||
hub.agents["s1"] = old
|
||||
hub.agents["s1"] = fresh // reconnect replaced old
|
||||
|
||||
hub.unregister(old)
|
||||
rows, err := store.Sessions()
|
||||
if err != nil || len(rows) != 1 {
|
||||
t.Fatalf("sessions: %v %d", err, len(rows))
|
||||
}
|
||||
if !rows[0].OnlineDB {
|
||||
t.Fatal("replaced conn unregistering must not flip the live session offline")
|
||||
}
|
||||
|
||||
hub.unregister(fresh)
|
||||
rows, err = store.Sessions()
|
||||
if err != nil {
|
||||
t.Fatalf("sessions: %v", err)
|
||||
}
|
||||
if rows[0].OnlineDB {
|
||||
t.Fatal("last live conn unregistering must mark the session offline")
|
||||
}
|
||||
}
|
||||
|
||||
func TestPingPongConstants(t *testing.T) {
|
||||
if pingPeriod >= pongWait {
|
||||
t.Fatalf("pingPeriod %v must be < pongWait %v", pingPeriod, pongWait)
|
||||
}
|
||||
}
|
||||
|
||||
func TestWebClientSendPing(t *testing.T) {
|
||||
serverConn := make(chan *websocket.Conn, 1)
|
||||
up := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
c, err := (&websocket.Upgrader{}).Upgrade(w, r, nil)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
serverConn <- c
|
||||
for {
|
||||
if _, _, err := c.ReadMessage(); err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
}))
|
||||
t.Cleanup(up.Close)
|
||||
clientConn, _, err := websocket.DefaultDialer.Dial("ws"+strings.TrimPrefix(up.URL, "http"), nil)
|
||||
if err != nil {
|
||||
t.Fatalf("dial: %v", err)
|
||||
}
|
||||
t.Cleanup(func() { _ = clientConn.Close() })
|
||||
var server *websocket.Conn
|
||||
select {
|
||||
case server = <-serverConn:
|
||||
case <-time.After(2 * time.Second):
|
||||
t.Fatal("server conn never upgraded")
|
||||
}
|
||||
hub := NewHub(openTestStore(t))
|
||||
c := &webClient{hub: hub, conn: server, done: make(chan struct{})}
|
||||
if !c.sendPing() {
|
||||
t.Fatal("sendPing over a live conn must succeed")
|
||||
}
|
||||
ac := &agentConn{hub: hub, conn: server, send: make(chan []byte, 1), done: make(chan struct{})}
|
||||
if !ac.sendPing() {
|
||||
t.Fatal("agent sendPing over a live conn must succeed")
|
||||
}
|
||||
}
|
||||
|
||||
func TestPongFramesKeepReadLoopsAlive(t *testing.T) {
|
||||
// unsolicited pongs must pass through both read pumps (refreshing the
|
||||
// read deadline) without killing the conn.
|
||||
ts, _ := newTestServer(t)
|
||||
ws := dialAgent(t, ts)
|
||||
if err := ws.WriteMessage(websocket.PongMessage, nil); err != nil {
|
||||
t.Fatalf("agent pong: %v", err)
|
||||
}
|
||||
_ = ws.WriteJSON(helloFrame("s1"))
|
||||
if welcome := readFrame(t, ws); welcome["type"] != evWelcome {
|
||||
t.Fatalf("agent read loop died after pong: %v", welcome)
|
||||
}
|
||||
|
||||
web := dialWeb(t, ts)
|
||||
if first := readFrame(t, web); first["type"] != frameSessionList {
|
||||
t.Fatalf("first web frame = %v", first)
|
||||
}
|
||||
if err := web.WriteMessage(websocket.PongMessage, nil); err != nil {
|
||||
t.Fatalf("web pong: %v", err)
|
||||
}
|
||||
// subscribe processed by the (still live) read pump → live events flow.
|
||||
if err := web.WriteJSON(map[string]any{"type": frameSubscribe, "sessionId": "s1"}); err != nil {
|
||||
t.Fatalf("subscribe: %v", err)
|
||||
}
|
||||
// A reader goroutine avoids deadline-based reads (gorilla conns fail
|
||||
// permanently after a read timeout); resend until a batch arrives.
|
||||
frames := make(chan map[string]any, 16)
|
||||
go func() {
|
||||
defer close(frames)
|
||||
for {
|
||||
var m map[string]any
|
||||
if err := web.ReadJSON(&m); err != nil {
|
||||
return
|
||||
}
|
||||
frames <- m
|
||||
}
|
||||
}()
|
||||
start := time.Now()
|
||||
for i := 0; ; i++ {
|
||||
_ = ws.WriteJSON(map[string]any{"v": 1, "type": evMessageUpdate, "sessionId": "s1", "seq": int64(i + 1), "ts": 10, "delta": "x"})
|
||||
select {
|
||||
case m, ok := <-frames:
|
||||
if !ok {
|
||||
t.Fatal("web conn closed; read loop died after pong")
|
||||
}
|
||||
if m["type"] == frameEvents {
|
||||
return // read pump survived the pong and routed the subscription
|
||||
}
|
||||
case <-time.After(200 * time.Millisecond):
|
||||
}
|
||||
if time.Since(start) > 3*time.Second {
|
||||
t.Fatal("no events frame delivered after pong")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestWebResubscribeSplitsEventBatches(t *testing.T) {
|
||||
ts, _, hub := newTestServerHub(t)
|
||||
agent1 := dialAgent(t, ts)
|
||||
_ = agent1.WriteJSON(helloFrame("s1"))
|
||||
_ = readFrame(t, agent1)
|
||||
agent2 := dialAgent(t, ts)
|
||||
_ = agent2.WriteJSON(helloFrame("s2"))
|
||||
_ = readFrame(t, agent2)
|
||||
|
||||
web := dialWeb(t, ts)
|
||||
if first := readFrame(t, web); first["type"] != frameSessionList {
|
||||
t.Fatalf("first web frame = %v", first)
|
||||
}
|
||||
|
||||
webSubscribed := func(want string) func() bool {
|
||||
return func() bool {
|
||||
hub.mu.Lock()
|
||||
defer hub.mu.Unlock()
|
||||
for c := range hub.webs {
|
||||
c.mu.Lock()
|
||||
sub := c.sub
|
||||
c.mu.Unlock()
|
||||
if sub == want {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
}
|
||||
_ = web.WriteJSON(map[string]any{"type": frameSubscribe, "sessionId": "s1"})
|
||||
waitFor(t, 2*time.Second, webSubscribed("s1"))
|
||||
_ = agent1.WriteJSON(map[string]any{"v": 1, "type": evMessageUpdate, "sessionId": "s1", "seq": 1, "ts": 10, "delta": "one"})
|
||||
|
||||
readBatch := func(want string) map[string]any {
|
||||
deadline := time.Now().Add(3 * time.Second)
|
||||
for time.Now().Before(deadline) {
|
||||
_ = web.SetReadDeadline(time.Now().Add(2 * time.Second))
|
||||
var m map[string]any
|
||||
if err := web.ReadJSON(&m); err != nil {
|
||||
t.Fatalf("read frame (want batch %s): %v", want, err)
|
||||
}
|
||||
if m["type"] != frameEvents {
|
||||
continue
|
||||
}
|
||||
if m["sessionId"] != want {
|
||||
t.Fatalf("events frame sessionId = %v, want %q (sessions must not mix)", m["sessionId"], want)
|
||||
}
|
||||
for _, e := range m["events"].([]any) {
|
||||
if e.(map[string]any)["sessionId"] != want {
|
||||
t.Fatalf("batch for %s contains event of %v", want, e.(map[string]any)["sessionId"])
|
||||
}
|
||||
}
|
||||
return m
|
||||
}
|
||||
t.Fatalf("no events frame for %s before timeout", want)
|
||||
return nil
|
||||
}
|
||||
|
||||
readBatch("s1") // event A flushed while subscribed to A
|
||||
|
||||
_ = web.WriteJSON(map[string]any{"type": frameSubscribe, "sessionId": "s2"})
|
||||
waitFor(t, 2*time.Second, webSubscribed("s2"))
|
||||
_ = agent2.WriteJSON(map[string]any{"v": 1, "type": evMessageUpdate, "sessionId": "s2", "seq": 1, "ts": 11, "delta": "two"})
|
||||
readBatch("s2") // event B must arrive as its own frame, never mixed with A's
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user