返回 DeepSeek-Reasonix
remote_tab_identity_test.go
根目录 / desktop / remote_tab_identity_test.go
1 package main
2
3 import (
4 "strings"
5 "testing"
6 )
7
8 // Blank sessions expose host\0workspace\0; after materialization the tab
9 // adopts Serve's Current name/path so the tree highlight migrates.
10
11 // TestRemoteTabBlankSessionIdentity pins the "+ new session" flow: after a
12 // named session was active, resetting to a blank session must flip the tab
13 // meta TopicID to the blank-row identity.
14 func TestRemoteTabBlankSessionIdentity(t *testing.T) {
15 fs := newFakeServe(t, "s3cret", []serveSessionEntry{
16 {Name: "s1", Path: "/remote/sessions/s1.jsonl", Title: "First chat", Turns: 2, Current: true},
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 }
23 seedBridgeTestHost(t, "box")
24 a := &App{remoteRuntime: kernel}
25 cleanupRemoteTabPumps(t, a)
26
27 named := openReadyRemoteTab(t, a, RemoteTabOpenOptions{
28 SessionName: "s1",
29 SessionPath: "/remote/sessions/s1.jsonl",
30 SessionTitle: "First chat",
31 })
32 if want := "box\x00~/app\x00s1"; named.TopicID != want {
33 t.Fatalf("named TopicID = %q, want %q", named.TopicID, want)
34 }
35
36 blank, err := a.OpenRemoteProjectTab("box", "~/app", RemoteTabOpenOptions{NewSession: true})
37 if err != nil {
38 t.Fatal(err)
39 }
40 if blank.ID != named.ID {
41 t.Fatalf("blank open created a second tab %q; want reuse of %q", blank.ID, named.ID)
42 }
43 wantBlank := "box\x00~/app\x00"
44 if blank.TopicID != wantBlank {
45 t.Fatalf("blank TopicID = %q, want %q", blank.TopicID, wantBlank)
46 }
47 for _, tab := range a.ListTabs() {
48 if tab.ID == blank.ID && tab.TopicID != wantBlank {
49 t.Fatalf("ListTabs TopicID = %q, want %q", tab.TopicID, wantBlank)
50 }
51 }
52 cleanupRemoteTabPumps(t, a)
53 }
54
55 // TestRemoteTabAdoptsMaterializedSessionIdentity pins the migration: once the
56 // serve listing marks the materialized session as Current, the tab adopts its
57 // Name/Path (not just the title) so the tree highlight moves off the blank
58 // row onto the named row.
59 func TestRemoteTabAdoptsMaterializedSessionIdentity(t *testing.T) {
60 fs := newFakeServe(t, "s3cret", []serveSessionEntry{
61 {Name: "s1", Path: "/remote/sessions/s1.jsonl", Title: "First chat", Turns: 2},
62 })
63 kernel := &fakeRemoteKernel{
64 statuses: []RemoteConnectionStatusView{{HostID: "box", State: "connected"}},
65 ensureView: RemoteServerView{HostID: "box", State: "ready", LocalURL: fs.server.URL},
66 ensureToken: "s3cret",
67 }
68 seedBridgeTestHost(t, "box")
69 log := &eventLog{}
70 a := &App{remoteRuntime: kernel, remoteEventHook: log.add}
71 cleanupRemoteTabPumps(t, a)
72
73 meta := openReadyRemoteTab(t, a, RemoteTabOpenOptions{NewSession: true})
74 if want := "box\x00~/app\x00"; meta.TopicID != want {
75 t.Fatalf("blank TopicID = %q, want %q", meta.TopicID, want)
76 }
77 // Ready is published immediately before the fresh-session metadata update.
78 // Wait for that bootstrap event so it cannot race the adoption event below,
79 // which is especially visible under Windows scheduling.
80 waitForRemoteEventCount(t, log, "remote-tab:updated", 1)
81
82 // The first turn lands: the serve now lists the fresh session as Current.
83 fs.mu.Lock()
84 fs.sessions = append(fs.sessions, serveSessionEntry{
85 Name: "fresh-123", Path: "/remote/sessions/fresh.jsonl", Title: "Earned title", Turns: 1, Current: true,
86 })
87 fs.mu.Unlock()
88
89 updatesBefore := log.count("remote-tab:updated")
90 a.refreshRemoteTabTitle(meta.ID)
91
92 wantTopic := "box\x00~/app\x00fresh-123"
93 tab := a.remoteTabs[meta.ID]
94 if tab == nil {
95 t.Fatal("remote tab vanished")
96 }
97 if got := remoteTabTopicID(tab); got != wantTopic {
98 t.Fatalf("adopted topic id = %q, want %q", got, wantTopic)
99 }
100 if tab.session.path != "/remote/sessions/fresh.jsonl" {
101 t.Fatalf("adopted session path = %q, want /remote/sessions/fresh.jsonl", tab.session.path)
102 }
103 if tab.session.reset {
104 t.Fatal("sessionReset must clear once the materialized session is adopted")
105 }
106 if got := log.count("remote-tab:updated"); got != updatesBefore+1 {
107 t.Fatalf("materialized identity emitted %d updates after refresh, want 1", got-updatesBefore)
108 }
109 cleanupRemoteTabPumps(t, a)
110 }
111
112 // TestRemoteTabTopicIDSeparatorShape guards the identity shape itself: the
113 // blank id ends with the separator, so it can never equal a named row id.
114 func TestRemoteTabTopicIDSeparatorShape(t *testing.T) {
115 blank := remoteTabTopicID(&remoteTab{ref: RemoteTabRef{HostID: "box", Workspace: "~/app"}})
116 if !strings.HasSuffix(blank, "\x00") || blank != "box\x00~/app\x00" {
117 t.Fatalf("blank topic id = %q, want box\\0~/app\\0", blank)
118 }
119 named := remoteTabTopicID(&remoteTab{ref: RemoteTabRef{HostID: "box", Workspace: "~/app"}, session: remoteTabSessionState{name: "s1"}})
120 if named != "box\x00~/app\x00s1" {
121 t.Fatalf("named topic id = %q, want box\\0~/app\\0s1", named)
122 }
123 canonical := remoteTabTopicID(&remoteTab{ref: RemoteTabRef{HostID: "box", Workspace: "~/app"}, session: remoteTabSessionState{name: "legacy-name", sessionID: "canonical-id"}})
124 if canonical != "box\x00~/app\x00canonical-id" {
125 t.Fatalf("canonical topic id = %q, want immutable session identity", canonical)
126 }
127 }
128
128 lines GO