| 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 |