返回 DeepSeek-Reasonix
manual_creation_target_test.go
根目录 / desktop / manual_creation_target_test.go
1 package main
2
3 import (
4 "context"
5 "encoding/json"
6 "sync"
7 "sync/atomic"
8 "testing"
9 "time"
10
11 "reasonix/desktop/internal/workspacestate"
12 )
13
14 // The registry has already merged an old workspace ID into its canonical
15 // owner, but the independent creation journal still contains that old ID.
16 func seedAbsorbedManualCreation(t *testing.T, a *App, id string) ManualSessionCreationView {
17 t.Helper()
18 v, r := seedManualCreation(t, a, id, "reserved")
19 if _, err := ensureGlobalWorkspaceRoot(); err != nil {
20 t.Fatal(err)
21 }
22 v.WorkspaceID = "absorbed-workspace"
23 v.Scope, v.WorkspaceRoot = "project", globalWorkspaceRoot()
24 body, err := json.Marshal(v)
25 if err != nil {
26 t.Fatal(err)
27 }
28 if _, err := a.sessionUIStore().Save(t.Context(), "creation", id, r.Revision, body); err != nil {
29 t.Fatal(err)
30 }
31 return v
32 }
33
34 func TestManualCreationAutomaticallyRetriesUnavailableDirectory(t *testing.T) {
35 a := newManualSessionTestApp(t)
36 v, _ := seedManualCreation(t, a, "directory-reappears", "reserved")
37 m := a.creationManager()
38 var attempts atomic.Int32
39 m.execute = func(_ context.Context, got ManualSessionCreationView, _ func(string)) error {
40 if got.Ref != v.Ref || got.OperationID != v.OperationID {
41 return errManualCreationIdentity
42 }
43 if attempts.Add(1) == 1 {
44 return manualCreationTargetError(workspacestate.ErrCreationWorkspaceUnavailable)
45 }
46 return nil
47 }
48 m.Ensure(v.OperationID, "begin", "")
49 a.manualCreationTasks.Wait()
50 got, err := a.GetManualSessionCreation(v.OperationID)
51 if err != nil || got.Phase != "ready" || attempts.Load() != 2 {
52 t.Fatalf("automatic retry = %+v attempts=%d %v", got, attempts.Load(), err)
53 }
54 r, err := a.sessionUIStore().Get(t.Context(), "creation", v.OperationID)
55 if err != nil {
56 t.Fatal(err)
57 }
58 var fields map[string]json.RawMessage
59 if err := json.Unmarshal(r.Payload, &fields); err != nil {
60 t.Fatal(err)
61 }
62 if string(fields["future"]) != `{"preserve":true}` || fields["surfaceReady"] != nil || fields["progress"] != nil {
63 t.Fatalf("persisted contract changed: %s", r.Payload)
64 }
65 }
66
67 func TestManualCreationRecoversPreviouslyFailedTargetChange(t *testing.T) {
68 a := newManualSessionTestApp(t)
69 v := seedAbsorbedManualCreation(t, a, "old-target-changed-failure")
70 r, err := a.sessionUIStore().Get(t.Context(), "creation", v.OperationID)
71 if err != nil {
72 t.Fatal(err)
73 }
74 m := a.creationManager()
75 if _, err := m.savePhase(r, "failed", "session_operation:target_changed:old failure"); err != nil {
76 t.Fatal(err)
77 }
78 m.scan()
79 a.manualCreationTasks.Wait()
80 got, err := a.GetManualSessionCreation(v.OperationID)
81 if err != nil || got.Phase != "ready" || got.Ref != v.Ref {
82 t.Fatalf("old failed request = %+v %v", got, err)
83 }
84 }
85
86 func TestManualCreationOpensInputBeforeRuntimeReady(t *testing.T) {
87 a := newManualSessionTestApp(t)
88 started, release := make(chan struct{}), make(chan struct{})
89 var releaseOnce sync.Once
90 unblock := func() { releaseOnce.Do(func() { close(release) }) }
91 t.Cleanup(unblock)
92 a.tabBuildStartHook = func(string) { close(started); <-release }
93 v, err := a.BeginManualSessionCreation(ManualSessionCreationRequest{OperationID: "input-before-runtime", Scope: "global"})
94 if err != nil {
95 t.Fatal(err)
96 }
97 <-started
98 v, err = a.GetManualSessionCreation(v.OperationID)
99 if err != nil || !v.SurfaceReady || v.Phase == "ready" {
100 t.Fatalf("early surface = %+v %v", v, err)
101 }
102 opened := make(chan error, 1)
103 go func() { _, err := a.OpenSession(v.Ref); opened <- err }()
104 select {
105 case err := <-opened:
106 if err != nil {
107 t.Fatal(err)
108 }
109 case <-time.After(3 * time.Second):
110 unblock()
111 <-opened
112 t.Fatal("opening the input waited for runtime initialization")
113 }
114 a.mu.RLock()
115 early := a.activeTabLocked()
116 preservedBuild := early != nil && early.SessionID == v.Ref.SessionID && early.Ctrl == nil && early.buildExecution != nil && len(a.tabs) == 1
117 a.mu.RUnlock()
118 if !preservedBuild {
119 t.Fatal("opening input built a competing runtime")
120 }
121 input, err := a.GetSessionComposerState(v.Ref)
122 if err != nil {
123 t.Fatal(err)
124 }
125 _, err = a.SaveSessionComposerState(SessionComposerSaveRequest{Ref: v.Ref, ExpectedRevision: input.Revision, ContentVersion: 1, ContentJSON: `{"text":"typed while starting"}`})
126 if err != nil {
127 t.Fatal(err)
128 }
129 unblock()
130 a.manualCreationTasks.Wait()
131 input, err = a.GetSessionComposerState(v.Ref)
132 if err != nil || input.ContentJSON != `{"text":"typed while starting"}` {
133 t.Fatalf("input lost: %+v %v", input, err)
134 }
135 a.mu.RLock()
136 active := a.activeTabLocked()
137 correct := active != nil && active.SessionID == v.Ref.SessionID && active.Ctrl != nil
138 a.mu.RUnlock()
139 if !correct {
140 t.Fatal("initialization did not preserve the selected session")
141 }
142 }
143
144 func TestManualCreationResolvesAbsorbedWorkspaceOnRecovery(t *testing.T) {
145 a := newManualSessionTestApp(t)
146 v := seedAbsorbedManualCreation(t, a, "absorbed-create-recovery")
147 a.creationManager().Ensure(v.OperationID, "recovery", "")
148 a.manualCreationTasks.Wait()
149 got, err := a.GetManualSessionCreation(v.OperationID)
150 if err != nil || got.Phase != "ready" || got.Ref != v.Ref {
151 t.Fatalf("recovery changed identity or failed: %+v %v", got, err)
152 }
153 state, err := a.workspaceRegistry().Load(t.Context())
154 if err != nil {
155 t.Fatal(err)
156 }
157 if ids := state.Workspaces[workspacestate.GlobalWorkspaceID].SessionIDs; len(ids) != 1 || ids[0] != v.Ref.SessionID {
158 t.Fatalf("canonical membership = %v", ids)
159 }
160 }
161
162 func TestManualCreationReplayResolvesAbsorbedWorkspace(t *testing.T) {
163 a := newManualSessionTestApp(t)
164 v := seedAbsorbedManualCreation(t, a, "absorbed-create-replay")
165 for _, id := range []string{workspacestate.GlobalWorkspaceID, v.WorkspaceID} {
166 got, err := a.BeginManualSessionCreation(ManualSessionCreationRequest{
167 OperationID: v.OperationID, WorkspaceID: id, Scope: "project", WorkspaceRoot: v.WorkspaceRoot,
168 })
169 if err != nil || got.Ref != v.Ref {
170 t.Fatalf("replay %s = %+v %v", id, got, err)
171 }
172 }
173 a.manualCreationTasks.Wait()
174 }
175
176 func TestManualCreationReplayDoesNotRegisterDifferentWorkspace(t *testing.T) {
177 a := newManualSessionTestApp(t)
178 v, _ := seedManualCreation(t, a, "replay-different-directory", "ready")
179 before, err := a.workspaceRegistry().Load(t.Context())
180 if err != nil {
181 t.Fatal(err)
182 }
183 _, err = a.BeginManualSessionCreation(ManualSessionCreationRequest{
184 OperationID: v.OperationID, Scope: "project", WorkspaceRoot: t.TempDir(),
185 })
186 if err == nil {
187 t.Fatal("same operation accepted a different directory")
188 }
189 after, err := a.workspaceRegistry().Load(t.Context())
190 if err != nil {
191 t.Fatal(err)
192 }
193 if len(after.Workspaces) != len(before.Workspaces) {
194 t.Fatal("replay registered an unrelated workspace")
195 }
196 }
197
197 lines GO