| 1 | package main |
| 2 | |
| 3 | import ( |
| 4 | "context" |
| 5 | "errors" |
| 6 | "testing" |
| 7 | |
| 8 | "reasonix/internal/boot" |
| 9 | "reasonix/internal/control" |
| 10 | "reasonix/internal/event" |
| 11 | "reasonix/internal/session" |
| 12 | ) |
| 13 | |
| 14 | func TestOpenSessionSelectsLocalTabAfterRemote(t *testing.T) { |
| 15 | app, _, target, _, _ := canonicalWorkspaceOpenFixture(t) |
| 16 | app.remoteTabMu.Lock() |
| 17 | app.remoteTabs = map[string]*remoteTab{ |
| 18 | "remote": {id: "remote", ref: RemoteTabRef{HostID: "box", Workspace: "/work"}, state: "disconnected"}, |
| 19 | } |
| 20 | app.remoteTabLayout = remoteTabLayoutState{activeID: "remote", order: []string{"remote"}, stripOrder: []string{"remote"}} |
| 21 | app.remoteTabMu.Unlock() |
| 22 | |
| 23 | if _, err := app.OpenSession(target.Ref()); err != nil { |
| 24 | t.Fatal(err) |
| 25 | } |
| 26 | localActive, remoteActive := false, false |
| 27 | for _, tab := range app.ListTabs() { |
| 28 | if tab.Remote == nil && tab.SessionID == target.Ref().SessionID { |
| 29 | localActive = tab.Active |
| 30 | } |
| 31 | if tab.Remote != nil { |
| 32 | remoteActive = tab.Active |
| 33 | } |
| 34 | } |
| 35 | if !localActive || remoteActive { |
| 36 | t.Fatalf("OpenSession left localActive=%v remoteActive=%v", localActive, remoteActive) |
| 37 | } |
| 38 | } |
| 39 | |
| 40 | func TestCanonicalNavigationLastRequestWins(t *testing.T) { |
| 41 | app, tab, target, _, _ := canonicalWorkspaceOpenFixture(t) |
| 42 | ctrl := tab.Ctrl |
| 43 | tab.Ctrl = &activeNavigationController{SessionAPI: ctrl, IdentityLifecycle: ctrl.(control.IdentityLifecycle), status: control.RuntimeStatus{Running: true}} |
| 44 | source := session.SessionRef{HostID: localDesktopHostID, SessionID: tab.SessionID} |
| 45 | app.runtimeRebuildMu.Lock() |
| 46 | locked := true |
| 47 | defer func() { |
| 48 | if locked { |
| 49 | app.runtimeRebuildMu.Unlock() |
| 50 | } |
| 51 | }() |
| 52 | first, second := make(chan error, 1), make(chan error, 1) |
| 53 | go func() { _, err := app.OpenSession(target.Ref()); first <- err }() |
| 54 | waitFor(t, "first navigation admission", func() bool { return app.desktopSessions.navigationSeq.Load() == 1 }) |
| 55 | go func() { _, err := app.OpenSession(source); second <- err }() |
| 56 | waitFor(t, "second navigation admission", func() bool { return app.desktopSessions.navigationSeq.Load() == 2 }) |
| 57 | app.runtimeRebuildMu.Unlock() |
| 58 | locked = false |
| 59 | if err := <-first; !errors.Is(err, errSessionNavigationSuperseded) { |
| 60 | t.Fatalf("first open = %v", err) |
| 61 | } |
| 62 | if err := <-second; err != nil { |
| 63 | t.Fatalf("last open = %v", err) |
| 64 | } |
| 65 | if tab.SessionID != source.SessionID { |
| 66 | t.Fatal("superseded open won") |
| 67 | } |
| 68 | } |
| 69 | |
| 70 | func TestTicketedCanonicalNavigationKeepsOriginalIntent(t *testing.T) { |
| 71 | app, source, target, _, _ := canonicalWorkspaceOpenFixture(t) |
| 72 | app.readyHook = func() {} |
| 73 | installNoopRuntimeEvents(app, source.sink) |
| 74 | events := newActivationEventRecorder(app) |
| 75 | t.Cleanup(func() { app.shutdown(context.Background()) }) |
| 76 | ticket, err := app.StartTopicActivation(TopicActivationRequest{SessionPath: sessionRoute(target.Ref().SessionID)}) |
| 77 | if err != nil { |
| 78 | t.Fatal(err) |
| 79 | } |
| 80 | if got := app.desktopSessions.navigationSeq.Load(); got != 1 { |
| 81 | t.Fatalf("one navigation claimed %d intents; a nested open can overtake a newer queued request", got) |
| 82 | } |
| 83 | events.waitFor(t, activationEventFor(ticket.RequestID, "ready")) |
| 84 | } |
| 85 | |
| 86 | func TestDelayedCanonicalNavigationCannotReclaimSupersededIntent(t *testing.T) { |
| 87 | app, tab, target, _, _ := canonicalWorkspaceOpenFixture(t) |
| 88 | intent := app.desktopSessions.navigationSeq.Add(1) |
| 89 | if err := app.SetActiveTab(tab.ID); err != nil { |
| 90 | t.Fatal(err) |
| 91 | } |
| 92 | before := tab.SessionID |
| 93 | if _, err := app.openTopicSessionWithNavigation("project", tab.WorkspaceRoot, "", sessionRoute(target.Ref().SessionID), intent); !errors.Is(err, errSessionNavigationSuperseded) { |
| 94 | t.Fatalf("late adopted source open = %v", err) |
| 95 | } |
| 96 | if tab.SessionID != before { |
| 97 | t.Fatal("late source adoption displaced the newer selection") |
| 98 | } |
| 99 | } |
| 100 | |
| 101 | func TestCanonicalOpenReattachesDetachedWorkspace(t *testing.T) { |
| 102 | app, tab, target, root, workspaceID := canonicalWorkspaceOpenFixture(t) |
| 103 | ctrl, err := app.buildTabControllerBoot(app.ctx, boot.Options{Model: tab.model, WorkspaceRoot: root, SessionDir: desktopSessionDir(root), Sink: event.Discard}) |
| 104 | if err != nil { |
| 105 | t.Fatal(err) |
| 106 | } |
| 107 | t.Cleanup(ctrl.Close) |
| 108 | if _, err := ctrl.(control.IdentityLifecycle).OpenSession(t.Context(), target.Ref()); err != nil { |
| 109 | t.Fatal(err) |
| 110 | } |
| 111 | detached := &WorkspaceTab{ID: "detached-B", Scope: "project", WorkspaceRoot: root, SessionID: target.Ref().SessionID, Ready: true, Ctrl: ctrl, model: tab.model, sink: &tabEventSink{tabID: "detached-B", app: app}} |
| 112 | detached.SessionWorkspace.ID = workspaceID |
| 113 | key := sessionRuntimeKey(sessionRoute(target.Ref().SessionID)) |
| 114 | app.ensureDetachedSessionsLocked() |
| 115 | app.detachedSessions[key] = detached |
| 116 | app.newSessionRuntimeLocked(detached, key) |
| 117 | if _, err := app.OpenSession(target.Ref()); err != nil { |
| 118 | t.Fatal(err) |
| 119 | } |
| 120 | if tab.Ctrl != ctrl || !sameDesktopPath(tab.WorkspaceRoot, root) || app.detachedSessions[key] != nil { |
| 121 | t.Fatal("did not transfer the exact detached owner") |
| 122 | } |
| 123 | if runtime := app.runtimeBySessionKey[key]; runtime == nil || runtime.Owner != tab { |
| 124 | t.Fatal("runtime map has a stale owner") |
| 125 | } |
| 126 | } |
| 127 | |
| 128 | func TestCanonicalLegacyResumeUsesTargetWorkspace(t *testing.T) { |
| 129 | for _, page := range []bool{false, true} { |
| 130 | t.Run(map[bool]string{false: "messages", true: "page"}[page], func(t *testing.T) { |
| 131 | app, tab, target, root, _ := canonicalWorkspaceOpenFixture(t) |
| 132 | tab.HistoricalSource = &SessionSourceRef{Path: "/fixture/old.jsonl"} |
| 133 | var err error |
| 134 | if page { |
| 135 | _, err = app.ResumeSessionPageForTab(tab.ID, sessionRoute(target.Ref().SessionID), 32) |
| 136 | } else { |
| 137 | _, err = app.ResumeSessionForTab(tab.ID, sessionRoute(target.Ref().SessionID)) |
| 138 | } |
| 139 | if err != nil { |
| 140 | t.Fatal(err) |
| 141 | } |
| 142 | if !sameDesktopPath(tab.WorkspaceRoot, root) { |
| 143 | t.Fatal("compatibility entry kept source workspace") |
| 144 | } |
| 145 | if tab.HistoricalSource != nil { |
| 146 | t.Fatal("compatibility entry retained the preparation action") |
| 147 | } |
| 148 | }) |
| 149 | } |
| 150 | } |
| 151 | |
| 152 | func TestCanonicalOpenGlobalWorkspace(t *testing.T) { |
| 153 | app, tab, _, _, _ := canonicalWorkspaceOpenFixture(t) |
| 154 | workspaceID, err := app.ensureDesktopWorkspace(t.Context(), "global", "") |
| 155 | if err != nil { |
| 156 | t.Fatal(err) |
| 157 | } |
| 158 | target, err := app.desktopSessionService("").Create(t.Context(), session.CreateOptions{SessionID: "global-target", CWD: globalWorkspaceRoot(), Origin: session.SessionOriginNew}) |
| 159 | if err != nil { |
| 160 | t.Fatal(err) |
| 161 | } |
| 162 | if err := app.workspaceRegistry().AttachSession(t.Context(), "", workspaceID, target.Ref().SessionID, ""); err != nil { |
| 163 | t.Fatal(err) |
| 164 | } |
| 165 | if _, err := app.OpenSession(target.Ref()); err != nil { |
| 166 | t.Fatal(err) |
| 167 | } |
| 168 | if tab.Scope != "global" || tab.SessionWorkspace.ID != workspaceID || !sameDesktopPath(tab.WorkspaceRoot, globalWorkspaceRoot()) { |
| 169 | t.Fatal("global target inherited project context") |
| 170 | } |
| 171 | } |
| 172 |