返回 DeepSeek-Reasonix
session_draft_model_test.go
根目录 / desktop / session_draft_model_test.go
1 package main
2
3 import (
4 "encoding/json"
5 "errors"
6 "fmt"
7 "os"
8 "path/filepath"
9 "testing"
10
11 "reasonix/desktop/internal/draftstate"
12 "reasonix/internal/config"
13 )
14
15 func writeDraftDefaultModelConfig(t *testing.T, model string) {
16 t.Helper()
17 if err := os.MkdirAll(filepath.Dir(config.UserConfigPath()), 0o755); err != nil {
18 t.Fatal(err)
19 }
20 payload := fmt.Sprintf(`default_model = "fixture/%s"
21
22 [desktop]
23 provider_access = ["fixture"]
24
25 [[providers]]
26 name = "fixture"
27 kind = "openai"
28 base_url = "https://example.invalid/v1"
29 models = ["model-a", "model-b"]
30 default = "model-a"
31 api_key_env = "DRAFT_DEFAULT_MODEL_KEY"
32 `, model)
33 if err := os.WriteFile(config.UserConfigPath(), []byte(payload), 0o600); err != nil {
34 t.Fatal(err)
35 }
36 if _, err := config.SetCredential("DRAFT_DEFAULT_MODEL_KEY", "test-key"); err != nil {
37 t.Fatal(err)
38 }
39 }
40
41 func TestInheritedDraftModelFollowsDefaultUntilExplicitlySelected(t *testing.T) {
42 isolateDesktopUserDirs(t)
43 writeDraftDefaultModelConfig(t, "model-a")
44 a := newDraftTestApp(t)
45 root := t.TempDir()
46
47 draft, err := a.OpenSessionDraftForTarget("project", root)
48 if err != nil {
49 t.Fatal(err)
50 }
51 if draft.Settings.Model != "fixture/model-a" || draft.Settings.ModelSource != draftModelSourceDefault {
52 t.Fatalf("initial model = %+v, want inherited fixture/model-a", draft.Settings)
53 }
54
55 writeDraftDefaultModelConfig(t, "model-b")
56 reopened, err := a.OpenSessionDraftForTarget("project", root)
57 if err != nil {
58 t.Fatal(err)
59 }
60 if reopened.ID != draft.ID || reopened.Settings.Model != "fixture/model-b" || reopened.Settings.ModelSource != draftModelSourceDefault {
61 t.Fatalf("reopened draft = %+v, want same draft following fixture/model-b", reopened)
62 }
63
64 explicit := reopened.Settings
65 explicit.Model = "fixture/model-a"
66 explicit.ModelSource = draftModelSourceExplicit
67 saved, err := a.SaveSessionDraft(SessionDraftSaveRequest{
68 DraftID: reopened.ID, Revision: reopened.Revision, ContentJSON: reopened.ContentJSON, Settings: explicit,
69 })
70 if err != nil {
71 t.Fatal(err)
72 }
73 final, err := a.OpenSessionDraftForTarget("project", root)
74 if err != nil {
75 t.Fatal(err)
76 }
77 if final.Revision != saved.Draft.Revision || final.Settings.Model != "fixture/model-a" || final.Settings.ModelSource != draftModelSourceExplicit {
78 t.Fatalf("explicit model was not retained: %+v", final.Settings)
79 }
80 }
81
82 func TestUntouchedLegacyDraftMigratesToLiveDefaultButEditedLegacyDraftDoesNot(t *testing.T) {
83 isolateDesktopUserDirs(t)
84 writeDraftDefaultModelConfig(t, "model-a")
85 a := newDraftTestApp(t)
86
87 openLegacy := func(root, workspaceID, draftID string) draftstate.Draft {
88 t.Helper()
89 settings := a.defaultDraftSettings("project", root)
90 settings.ModelSource = ""
91 payload, err := json.Marshal(settings)
92 if err != nil {
93 t.Fatal(err)
94 }
95 record, _, err := a.draftStore().Open(t.Context(), workspaceID, "project", root, draftID, string(payload))
96 if err != nil {
97 t.Fatal(err)
98 }
99 return record
100 }
101
102 untouchedRoot := t.TempDir()
103 untouchedWorkspace, err := a.ensureDesktopWorkspace(t.Context(), "project", untouchedRoot)
104 if err != nil {
105 t.Fatal(err)
106 }
107 untouched := openLegacy(untouchedRoot, untouchedWorkspace, "legacy-untouched")
108
109 editedRoot := t.TempDir()
110 editedWorkspace, err := a.ensureDesktopWorkspace(t.Context(), "project", editedRoot)
111 if err != nil {
112 t.Fatal(err)
113 }
114 edited := openLegacy(editedRoot, editedWorkspace, "legacy-edited")
115 edited, err = a.draftStore().Save(t.Context(), edited.ID, edited.Revision, `{"text":"saved"}`, edited.SettingsJSON, false)
116 if err != nil {
117 t.Fatal(err)
118 }
119
120 writeDraftDefaultModelConfig(t, "model-b")
121 migrated, err := a.OpenSessionDraftForTarget("project", untouchedRoot)
122 if err != nil {
123 t.Fatal(err)
124 }
125 if migrated.ID != untouched.ID || migrated.Settings.Model != "fixture/model-b" || migrated.Settings.ModelSource != draftModelSourceDefault {
126 t.Fatalf("untouched legacy draft = %+v, want migrated live default", migrated)
127 }
128 preserved, err := a.OpenSessionDraftForTarget("project", editedRoot)
129 if err != nil {
130 t.Fatal(err)
131 }
132 if preserved.ID != edited.ID || preserved.Settings.Model != "fixture/model-a" || preserved.Settings.ModelSource != "" {
133 t.Fatalf("edited legacy draft = %+v, want preserved concrete model", preserved)
134 }
135 }
136
137 func TestLegacyDraftWithOperationKeepsFrozenModelAndStillOpens(t *testing.T) {
138 isolateDesktopUserDirs(t)
139 writeDraftDefaultModelConfig(t, "model-a")
140 a := newDraftTestApp(t)
141 root := t.TempDir()
142 workspaceID, err := a.ensureDesktopWorkspace(t.Context(), "project", root)
143 if err != nil {
144 t.Fatal(err)
145 }
146 settings := a.defaultDraftSettings("project", root)
147 settings.ModelSource = ""
148 payload, err := json.Marshal(settings)
149 if err != nil {
150 t.Fatal(err)
151 }
152 draft, _, err := a.draftStore().Open(t.Context(), workspaceID, "project", root, "legacy-operation", string(payload))
153 if err != nil {
154 t.Fatal(err)
155 }
156 request, err := json.Marshal(SessionDraftSubmissionRequest{
157 SnapshotVersion: 3,
158 Settings: settings,
159 })
160 if err != nil {
161 t.Fatal(err)
162 }
163 if _, _, err := a.draftStore().BeginOperation(t.Context(), draftstate.Operation{
164 ID: "legacy-operation-op", DraftID: draft.ID, WorkspaceID: workspaceID, DraftRevision: draft.Revision,
165 SessionID: "legacy-operation-session", TopicID: "legacy-operation-topic",
166 SubmissionID: "legacy-operation-submission", Fingerprint: "legacy-operation-fingerprint", RequestJSON: string(request),
167 }); err != nil {
168 t.Fatal(err)
169 }
170
171 writeDraftDefaultModelConfig(t, "model-b")
172 reopened, err := a.OpenSessionDraftForTarget("project", root)
173 if err != nil {
174 t.Fatal(err)
175 }
176 if reopened.Revision != draft.Revision || reopened.Settings.Model != "fixture/model-a" || reopened.Settings.ModelSource != "" {
177 t.Fatalf("operation-owned legacy draft changed during migration: %+v", reopened)
178 }
179 op, err := a.draftStore().Operation(t.Context(), "legacy-operation-op")
180 if err != nil {
181 t.Fatal(err)
182 }
183 frozen, err := a.draftOperationSettings(op)
184 if err != nil || frozen.Model != "fixture/model-a" {
185 t.Fatalf("frozen operation model = %+v, err %v", frozen, err)
186 }
187 }
188
189 func TestDraftSubmissionFingerprintIgnoresInheritedDefaultMirror(t *testing.T) {
190 base := SessionDraftSubmissionRequest{DraftID: "draft", Revision: 1, Display: "hi", Input: "hi",
191 Settings: SessionDraftSettings{Model: "fixture/model-a", ModelSource: draftModelSourceDefault}}
192 first, _, err := draftSubmissionFingerprint(base)
193 if err != nil {
194 t.Fatal(err)
195 }
196 base.Settings.Model = "fixture/model-b"
197 second, _, err := draftSubmissionFingerprint(base)
198 if err != nil {
199 t.Fatal(err)
200 }
201 if first != second {
202 t.Fatalf("inherited mirror changed request identity: %s != %s", first, second)
203 }
204 base.Settings.ModelSource = draftModelSourceExplicit
205 explicit, _, err := draftSubmissionFingerprint(base)
206 if err != nil {
207 t.Fatal(err)
208 }
209 if explicit == second {
210 t.Fatal("explicit model did not participate in request identity")
211 }
212 }
213
214 func TestDraftAdmissionPreservesModelAliasRequestIdentity(t *testing.T) {
215 isolateDesktopUserDirs(t)
216 writeDraftDefaultModelConfig(t, "model-a")
217 a := newDraftTestApp(t)
218 draft, err := a.OpenSessionDraftForTarget("project", t.TempDir())
219 if err != nil {
220 t.Fatal(err)
221 }
222 settings := draft.Settings
223 settings.Model, settings.ModelSource = "model-a", draftModelSourceExplicit
224 saved, err := a.SaveSessionDraft(SessionDraftSaveRequest{
225 DraftID: draft.ID, Revision: draft.Revision, ContentJSON: `{"text":"hello"}`, Settings: settings,
226 })
227 if err != nil {
228 t.Fatal(err)
229 }
230 request := SessionDraftSubmissionRequest{
231 SnapshotVersion: draftstate.SnapshotVersion, DraftID: draft.ID, Revision: saved.Draft.Revision,
232 SourceDigest: saved.Draft.SnapshotDigest, Display: "hello", Input: "hello", Settings: settings,
233 }
234 fingerprint, _, err := draftSubmissionFingerprint(request)
235 if err != nil {
236 t.Fatal(err)
237 }
238 // Seed an already admitted operation to exercise admission without starting a
239 // provider. Its execution snapshot is canonical, but identity belongs to the
240 // original wire request, including its explicit /model alias.
241 frozen := request
242 frozen.Settings.Model = "fixture/model-a"
243 _, payload, err := draftSubmissionFingerprint(frozen)
244 if err != nil {
245 t.Fatal(err)
246 }
247 op, _, err := a.draftStore().BeginOperation(t.Context(), draftstate.Operation{
248 ID: "alias-operation", RequestID: "original-request", DraftID: draft.ID, WorkspaceID: draft.WorkspaceID,
249 DraftRevision: saved.Draft.Revision, SourceDigest: saved.Draft.SnapshotDigest,
250 SessionID: "alias-session", TopicID: "alias-topic", SubmissionID: "alias-submission",
251 Fingerprint: fingerprint, RequestJSON: payload,
252 })
253 if err != nil {
254 t.Fatal(err)
255 }
256 if _, err := a.draftStore().SetOperationPhase(t.Context(), op.ID, "accepted", ""); err != nil {
257 t.Fatal(err)
258 }
259 for _, requestID := range []string{"original-request", "second-equivalent-request"} {
260 request.RequestID = requestID
261 got, err := a.BeginDraftSubmission(request)
262 if err != nil || got.OperationID != op.ID {
263 t.Fatalf("equivalent request %s: %+v, %v", requestID, got, err)
264 }
265 }
266 request.RequestID, request.Input = "original-request", "different input"
267 if _, err := a.BeginDraftSubmission(request); !errors.Is(err, draftstate.ErrOperationConflict) {
268 t.Fatalf("changed retry payload = %v, want operation conflict", err)
269 }
270 }
271
272 func TestDraftRecoveryProjectsFrozenModelUntilEditingResumes(t *testing.T) {
273 isolateDesktopUserDirs(t)
274 writeDraftDefaultModelConfig(t, "model-a")
275 a := newDraftTestApp(t)
276 draft, err := a.OpenSessionDraftForTarget("project", t.TempDir())
277 if err != nil {
278 t.Fatal(err)
279 }
280 request := SessionDraftSubmissionRequest{SnapshotVersion: draftstate.SnapshotVersion, Settings: draft.Settings}
281 _, payload, err := draftSubmissionFingerprint(request)
282 if err != nil {
283 t.Fatal(err)
284 }
285 op, _, err := a.draftStore().BeginOperation(t.Context(), draftstate.Operation{
286 ID: "frozen-view-operation", DraftID: draft.ID, WorkspaceID: draft.WorkspaceID, DraftRevision: draft.Revision,
287 SessionID: "frozen-view-session", TopicID: "frozen-view-topic", SubmissionID: "frozen-view-submission",
288 Fingerprint: "frozen-view", RequestJSON: payload,
289 })
290 if err != nil {
291 t.Fatal(err)
292 }
293 writeDraftDefaultModelConfig(t, "model-b")
294 for _, phase := range []string{"reserved", "resume_required", "runtime_failed", "terminal_failed", "cancelled"} {
295 if _, err := a.draftStore().SetOperationPhase(t.Context(), op.ID, phase, ""); err != nil {
296 t.Fatal(err)
297 }
298 want := "fixture/model-a"
299 if phase == "terminal_failed" || phase == "cancelled" {
300 want = "fixture/model-b"
301 }
302 state, err := a.GetSessionDraftState(draft.ID)
303 if err != nil || state.Draft.Settings.Model != want {
304 t.Fatalf("%s state model = %q, %v; want %s", phase, state.Draft.Settings.Model, err, want)
305 }
306 current := ""
307 for _, model := range a.ModelsForDraft(draft.ID) {
308 if model.Current {
309 current = model.Ref
310 }
311 }
312 if current != want {
313 t.Fatalf("%s picker model = %q; want %s (catalog: %+v)", phase, current, want, a.ModelsForDraft(draft.ID))
314 }
315 }
316 }
317
317 lines GO