| 1 | package main |
| 2 | |
| 3 | import ( |
| 4 | "net/http" |
| 5 | "testing" |
| 6 | ) |
| 7 | |
| 8 | // The explicit-intent listing contract: a remote group click may cold-start |
| 9 | // the serve, while the passive listing keeps refusing to wake one. |
| 10 | |
| 11 | // TestRemoteProjectSessionsEnsureColdStartsServe pins the group-click path: |
| 12 | // with nothing running, EnsureRemoteProjectSessions boots the serve through |
| 13 | // EnsureServer and returns the mapped rows. |
| 14 | func TestRemoteProjectSessionsEnsureColdStartsServe(t *testing.T) { |
| 15 | fs := newFakeServe(t, "s3cret", []serveSessionEntry{ |
| 16 | {Name: "s1", Path: "/remote/sessions/s1.jsonl", Title: "First chat", Turns: 2}, |
| 17 | }) |
| 18 | kernel := &fakeRemoteKernel{ |
| 19 | statuses: []RemoteConnectionStatusView{{HostID: "box", State: "connected"}}, |
| 20 | ensureView: RemoteServerView{HostID: "box", State: "ready", LocalURL: fs.server.URL}, |
| 21 | ensureToken: "s3cret", |
| 22 | snapshotMiss: true, // registry says nothing is running; EnsureServer can boot it |
| 23 | } |
| 24 | seedBridgeTestHost(t, "box") |
| 25 | a := &App{remoteRuntime: kernel} |
| 26 | cleanupRemoteTabPumps(t, a) |
| 27 | |
| 28 | // The read-only listing must still refuse while the serve is down. |
| 29 | if _, err := a.RemoteProjectSessions("box", "~/app"); err == nil { |
| 30 | t.Fatal("read-only listing must fail while the serve is stopped") |
| 31 | } |
| 32 | |
| 33 | rows, err := a.EnsureRemoteProjectSessions("box", "~/app") |
| 34 | if err != nil { |
| 35 | t.Fatal(err) |
| 36 | } |
| 37 | if kernel.ensureCalls == 0 { |
| 38 | t.Fatal("ensure listing never cold-started the serve") |
| 39 | } |
| 40 | if len(rows) != 1 || rows[0].Name != "s1" || rows[0].Title != "First chat" || rows[0].Turns != 2 { |
| 41 | t.Fatalf("ensured sessions = %+v, want the mapped s1 entry", rows) |
| 42 | } |
| 43 | cleanupRemoteTabPumps(t, a) |
| 44 | } |
| 45 | |
| 46 | // TestRemoteProjectSessionsEnsureReusesRunningServe pins the warm path: when |
| 47 | // a serve registration is already live, the ensure listing must not run a |
| 48 | // second EnsureServer round. |
| 49 | func TestRemoteProjectSessionsEnsureReusesRunningServe(t *testing.T) { |
| 50 | fs := newFakeServe(t, "s3cret", []serveSessionEntry{ |
| 51 | {Name: "s1", Path: "/remote/sessions/s1.jsonl", Title: "First chat", Turns: 1}, |
| 52 | }) |
| 53 | kernel := &fakeRemoteKernel{ |
| 54 | statuses: []RemoteConnectionStatusView{{HostID: "box", State: "connected"}}, |
| 55 | ensureView: RemoteServerView{HostID: "box", State: "ready", LocalURL: fs.server.URL}, |
| 56 | ensureToken: "s3cret", |
| 57 | } |
| 58 | seedBridgeTestHost(t, "box") |
| 59 | a := &App{remoteRuntime: kernel} |
| 60 | cleanupRemoteTabPumps(t, a) |
| 61 | |
| 62 | rows, err := a.EnsureRemoteProjectSessions("box", "~/app") |
| 63 | if err != nil { |
| 64 | t.Fatal(err) |
| 65 | } |
| 66 | if kernel.ensureCalls != 0 { |
| 67 | t.Fatalf("ensure listing booted a serve that was already running: %d calls", kernel.ensureCalls) |
| 68 | } |
| 69 | if len(rows) != 1 || rows[0].Name != "s1" { |
| 70 | t.Fatalf("ensured sessions = %+v, want s1", rows) |
| 71 | } |
| 72 | cleanupRemoteTabPumps(t, a) |
| 73 | } |
| 74 | |
| 75 | // TestRemoteProjectSessionsEnsureRejectsSuspendedTabClient pins reconnect |
| 76 | // recovery: a suspended tab retains its old client for later reattachment, but |
| 77 | // an explicit group refresh must not reuse that client as a live Serve path. |
| 78 | func TestRemoteProjectSessionsEnsureRejectsSuspendedTabClient(t *testing.T) { |
| 79 | fs := newFakeServe(t, "s3cret", []serveSessionEntry{ |
| 80 | {Name: "s1", Path: "/remote/sessions/s1.jsonl", Title: "Recovered chat", Turns: 3}, |
| 81 | }) |
| 82 | kernel := &fakeRemoteKernel{ |
| 83 | statuses: []RemoteConnectionStatusView{{HostID: "box", State: "connected"}}, |
| 84 | ensureView: RemoteServerView{HostID: "box", State: "ready", LocalURL: fs.server.URL}, |
| 85 | ensureToken: "s3cret", |
| 86 | snapshotMiss: true, |
| 87 | } |
| 88 | seedBridgeTestHost(t, "box") |
| 89 | a := &App{ |
| 90 | remoteRuntime: kernel, |
| 91 | remoteTabs: map[string]*remoteTab{ |
| 92 | "suspended": { |
| 93 | id: "suspended", ref: RemoteTabRef{HostID: "box", Workspace: "~/app"}, |
| 94 | state: "reconnecting", client: &http.Client{}, base: "http://127.0.0.1:1", |
| 95 | }, |
| 96 | }, |
| 97 | } |
| 98 | cleanupRemoteTabPumps(t, a) |
| 99 | |
| 100 | rows, err := a.EnsureRemoteProjectSessions("box", "~/app") |
| 101 | if err != nil { |
| 102 | t.Fatal(err) |
| 103 | } |
| 104 | if kernel.ensureCalls == 0 { |
| 105 | t.Fatal("ensure listing reused the suspended tab client instead of recovering Serve") |
| 106 | } |
| 107 | if len(rows) != 1 || rows[0].Name != "s1" { |
| 108 | t.Fatalf("ensured sessions = %+v, want recovered s1", rows) |
| 109 | } |
| 110 | } |
| 111 |