返回 DeepSeek-Reasonix
remote_tab_commands_proxy_test.go
根目录 / desktop / remote_tab_commands_proxy_test.go
1 package main
2
3 import (
4 "slices"
5 "strings"
6 "testing"
7 )
8
9 func TestDeleteRemoteProjectSessionSendsCanonicalIdentity(t *testing.T) {
10 fs := newFakeServe(t, "s3cret", nil)
11 kernel := &fakeRemoteKernel{
12 statuses: []RemoteConnectionStatusView{{HostID: "box", State: "connected"}},
13 ensureView: RemoteServerView{HostID: "box", State: "ready", LocalURL: fs.server.URL},
14 ensureToken: "s3cret",
15 }
16 seedBridgeTestHost(t, "box")
17 a := &App{remoteRuntime: kernel}
18 meta := openReadyRemoteTab(t, a, RemoteTabOpenOptions{NewSession: true})
19 defer cleanupRemoteTabPumps(t, a)
20 if err := a.DeleteRemoteProjectSession("box", "~/app", "canonical-target"); err != nil {
21 t.Fatal(err)
22 }
23 want := `POST /delete-session {"name":"canonical-target","sessionId":"canonical-target"}`
24 if !slices.Contains(fs.recorded(), want) {
25 t.Fatalf("delete request missing canonical identity: %v", fs.recorded())
26 }
27 if strings.TrimSpace(meta.ID) == "" {
28 t.Fatal("remote tab id is empty")
29 }
30 }
31
32 // TestRemoteTabCommandsForwardedToServe pins that every command binding
33 // reaches the right serve endpoint with the mapped body.
34 func TestRemoteTabCommandsForwardedToServe(t *testing.T) {
35 fs := newFakeServe(t, "s3cret", nil)
36 const currentPath = "/sessions/current.jsonl"
37 fs.newSessionPath = currentPath
38 kernel := &fakeRemoteKernel{
39 statuses: []RemoteConnectionStatusView{{HostID: "box", State: "connected"}},
40 ensureView: RemoteServerView{HostID: "box", State: "ready", LocalURL: fs.server.URL},
41 ensureToken: "s3cret",
42 }
43 seedBridgeTestHost(t, "box")
44 a := &App{remoteRuntime: kernel}
45 cleanupRemoteTabPumps(t, a)
46 meta := openReadyRemoteTab(t, a, RemoteTabOpenOptions{NewSession: true})
47 a.remoteTabMu.Lock()
48 a.remoteTabs[meta.ID].capabilities[serveCapabilityGoalLifecycleV2] = true
49 a.remoteTabMu.Unlock()
50 var profileDrained []string
51 beforeRetiredFloorCall := len(fs.recorded())
52 if err := a.SetRemoteTabQualityFloor(meta.ID, "delivery"); err != nil {
53 t.Fatalf("retired quality-floor compatibility call: %v", err)
54 }
55 if got := len(fs.recorded()); got != beforeRetiredFloorCall {
56 t.Fatalf("retired quality-floor call reached the remote server: before=%d after=%d", beforeRetiredFloorCall, got)
57 }
58
59 steps := []struct {
60 name string
61 call func() error
62 want string
63 }{
64 {"submit", func() error { return a.SubmitRemoteTab(meta.ID, "hello") }, `POST /submit {"input":"hello"}`},
65 {"cancel", func() error { return a.CancelRemoteTab(meta.ID) }, "POST /cancel {}"},
66 {"approve", func() error { return a.ApproveRemoteTab(meta.ID, "call-1", "allow") }, `POST /approve {"allow":true,"id":"call-1","persist":false,"session":false}`},
67 {"approve-session", func() error { return a.ApproveRemoteTab(meta.ID, "call-2", "session") }, `POST /approve {"allow":true,"id":"call-2","persist":false,"session":true}`},
68 {"approve-deny", func() error { return a.ApproveRemoteTab(meta.ID, "call-4", "deny") }, `POST /approve {"allow":false,"id":"call-4","persist":false,"session":false}`},
69 {"plan-start", func() error { return a.ResolveRemoteTabPlanDecision(meta.ID, "plan-1", "start_execution", "") }, `POST /plan-decision {"action":"start_execution","feedback":"","id":"plan-1"}`},
70 {"plan-revise", func() error {
71 return a.ResolveRemoteTabPlanDecision(meta.ID, "plan-2", "revise_plan", "check the fallback")
72 }, `POST /plan-decision {"action":"revise_plan","feedback":"check the fallback","id":"plan-2"}`},
73 {"answer", func() error {
74 return a.AnswerRemoteTab(meta.ID, "ask-1", []RemoteAskAnswer{{QuestionID: "question-1", Selected: []string{"yes"}}})
75 }, `POST /answer {"answers":[{"QuestionID":"question-1","Selected":["yes"]}],"id":"ask-1"}`},
76 {"rewind", func() error { return a.RewindRemoteTab(meta.ID, "3", "code") }, `POST /rewind {"scope":"code","turn":3}`},
77 {"permission-preset", func() error { return a.SetRemoteTabToolApprovalMode(meta.ID, "workspace-write") }, `POST /permission/preset {"expectedRevision":7,"preset":"workspace-write"}`},
78 {"composer-profile", func() (err error) {
79 profileDrained, err = a.SetRemoteTabComposerProfile(meta.ID, "plan", "workspace-write", "")
80 return
81 }, `POST /composer-profile {"collaborationMode":"plan","expectedPermissionRevision":7,"goal":"","toolApprovalMode":"workspace-write"}`},
82 {"goal", func() error { return a.SetRemoteTabGoal(meta.ID, "ship it") }, `POST /goal {"goal":"ship it"}`},
83 {"edit-goal", func() error {
84 limit := uint64(12)
85 return a.EditRemoteTabGoal(meta.ID, "ship it safely", &limit)
86 }, `POST /goal/edit {"maxGoalRounds":12,"objective":"ship it safely"}`},
87 {"effort", func() error { return a.SetRemoteTabEffort(meta.ID, "high") }, `POST /effort {"level":"high"}`},
88 {"pause-goal", func() error { return a.PauseRemoteTabGoal(meta.ID) }, "POST /goal/pause {}"},
89 {"resume-goal", func() error { return a.ResumeRemoteTabGoal(meta.ID) }, "POST /goal/resume {}"},
90 {"cancel-jobs", func() error { return a.CancelRemoteTabJobs(meta.ID, []string{"job-1"}) }, `POST /jobs/cancel {"ids":["job-1"]}`},
91 {"steer", func() error { return a.SteerRemoteTab(meta.ID, "keep it narrow") }, `POST /inbox/items {"input":"keep it narrow","intent":"steer"}`},
92 {"plan-on", func() error { return a.SetRemoteTabPlanMode(meta.ID, true) }, `POST /plan {"on":true}`},
93 {"compact", func() error { return a.CompactRemoteTab(meta.ID, "preserve tests") }, `POST /compact {"instructions":"preserve tests"}`},
94 {"fork", func() error { return a.ForkRemoteTab(meta.ID, 2, "try-auth") }, `POST /fork {"name":"try-auth","turn":2}`},
95 {"summarize", func() error { return a.SummarizeRemoteTab(meta.ID, 4, "upto") }, `POST /summarize {"mode":"upto","turn":4}`},
96 {"forget", func() error { return a.ForgetRemoteTab(meta.ID, "api-key") }, `POST /forget {"name":"api-key"}`},
97 {"clear", func() error { return a.ClearRemoteTabSession(meta.ID) }, "POST /clear {}"},
98 }
99 for _, step := range steps {
100 if err := step.call(); err != nil {
101 t.Fatalf("%s: %v", step.name, err)
102 }
103 }
104 for i, got := range fs.recordedExpectedPaths() {
105 if got != currentPath {
106 t.Fatalf("command %d expected-session header = %q, want %q", i, got, currentPath)
107 }
108 }
109 if !slices.Equal(profileDrained, []string{"approval-1"}) {
110 t.Fatalf("composer profile drained ids = %v, want [approval-1]", profileDrained)
111 }
112 calls := fs.recorded()
113 if slices.ContainsFunc(calls, func(call string) bool {
114 return call == `POST /inbox/items {"idempotencyKey":"plan-revision:plan-2","input":"check the fallback","intent":"followup"}`
115 }) {
116 t.Fatalf("plan revision was split into a second request: %v", calls)
117 }
118 for _, step := range steps {
119 if !slices.Contains(calls, step.want) {
120 t.Fatalf("%s: serve saw %v, want %q", step.name, calls, step.want)
121 }
122 }
123 if _, err := a.RemoteTabBranches(meta.ID); err != nil {
124 t.Fatalf("branches: %v", err)
125 }
126 if _, err := a.RemoteTabSkills(meta.ID); err != nil {
127 t.Fatalf("skills: %v", err)
128 }
129 if _, err := a.ReplayRemoteTabPrompts(meta.ID); err != nil {
130 t.Fatalf("pending prompts: %v", err)
131 }
132 foundBranches, foundSkills, foundPrompts := false, false, false
133 for _, c := range fs.recorded() {
134 if c == "GET /branches " {
135 foundBranches = true
136 }
137 if c == "GET /skills " {
138 foundSkills = true
139 }
140 if c == "GET /pending-prompts " {
141 foundPrompts = true
142 }
143 }
144 if !foundBranches || !foundSkills || !foundPrompts {
145 t.Fatalf("branches/skills/prompt reads missing: %v", fs.recorded())
146 }
147 }
148
148 lines GO