返回 DeepSeek-Reasonix
chat_tui_reclaim_test.go
根目录 / internal / cli / chat_tui_reclaim_test.go
1 package cli
2
3 import (
4 "net/http"
5 "testing"
6 "time"
7
8 "reasonix/internal/event"
9 )
10
11 // TestLegacyMirrorReclaimRoutesTakeoverByPath proves a reclaim of a legacy
12 // path takeover is remembered as that path, not as the identity the exclusive
13 // engine imported it under, so "/takeover takes it back" goes through the
14 // serve's path handoff instead of posting session-id:<branch id> to a serve
15 // that holds a path lease. It also pins that the reclaim drops a stale picker.
16 func TestLegacyMirrorReclaimRoutesTakeoverByPath(t *testing.T) {
17 m, ctrl, _, _ := newCanonicalTakeoverTUI(t)
18 legacy := saveQueryTestSession(t, ctrl.SessionDir(), "mirrored-legacy.jsonl", "mirrored legacy")
19 if err := m.leases.Rebind(legacy); err != nil {
20 t.Fatal(err)
21 }
22 fake := newFakeCanonicalServe(t, legacy)
23 withFakeCanonicalDiscovery(t, fake.base)
24 m.takeover.AttachController(ctrl)
25 m.takeover.Activate(&cliTakeoverBinding{
26 path: legacy, record: cliServeRecord{base: fake.base}, client: &http.Client{},
27 grant: cliTakeoverGrant{MirrorID: "mirror-legacy", SourceWriterID: "serve-writer", ReturnHandoffID: "return-legacy"},
28 })
29 yielded := make(chan struct{}, 1)
30 m.takeover.SetYieldCallback(func() { yielded <- struct{}{} })
31 fake.reclaim.Store(true)
32 m.takeover.Emit(event.Event{Kind: event.Text, Text: "answer"})
33 select {
34 case <-yielded:
35 case <-time.After(5 * time.Second):
36 t.Fatal("reclaim did not yield the legacy mirror")
37 }
38
39 m.resumePick = &resumePicker{}
40 next, cmd := m.Update(tuiSessionReclaimedMsg{})
41 if cmd != nil {
42 t.Fatalf("reclaim returned %T, want no command", cmd)
43 }
44 updated := next.(chatTUI)
45 m = &updated
46 if m.reclaimedTarget.canonical() || m.reclaimedTarget.path != legacy {
47 t.Fatalf("reclaimed target = %+v, want the legacy path %q", m.reclaimedTarget, legacy)
48 }
49 if m.resumePick != nil {
50 t.Fatal("reclaim left a stale resume picker overlay open")
51 }
52
53 // The identity route would reach the fake serve through the discovery
54 // override; the path route must not post an identity handoff there.
55 handoffs := fake.handoffCount()
56 fake.reclaim.Store(false)
57 m.runTakeoverCommand("/takeover")
58 if got := fake.handoffCount(); got != handoffs {
59 t.Fatalf("/takeover after a legacy reclaim posted %d identity handoff(s) for the imported id", got-handoffs)
60 }
61 }
62
63 // TestTakeoverOfFreeLegacySessionAfterReclaimSkipsSnapshot covers /takeover
64 // of a legacy row while detached: the reclaimed runtime is already released,
65 // so the switch must not try to snapshot it, and a target nobody holds is
66 // resumed with the reclaim state cleared.
67 func TestTakeoverOfFreeLegacySessionAfterReclaimSkipsSnapshot(t *testing.T) {
68 m, ctrl, _, _ := newCanonicalTakeoverTUI(t)
69 legacy := saveQueryTestSession(t, ctrl.SessionDir(), "free-legacy.jsonl", "FREE-LEGACY-PROMPT")
70 m.releaseCanonicalRuntime(ctrl)
71 if _, bound := ctrl.SessionRef(); bound {
72 t.Fatal("test setup: runtime still bound after release")
73 }
74 m.sessionReclaimed = true
75 m.reclaimedTarget = cliResumeTarget{path: legacy}
76
77 m.runTakeoverCommand("/takeover")
78
79 if m.sessionReclaimed {
80 t.Fatal("/takeover of the remembered legacy session left the TUI in reclaimed mode")
81 }
82 if _, bound := ctrl.SessionRef(); !bound {
83 t.Fatal("controller did not attach to the legacy session")
84 }
85 loaded := false
86 for _, msg := range ctrl.History() {
87 loaded = loaded || msg.Content == "FREE-LEGACY-PROMPT"
88 }
89 if !loaded {
90 t.Fatal("history not loaded from the legacy target")
91 }
92 }
93
93 lines GO