返回 DeepSeek-Reasonix
schema_upgrade_test.go
根目录 / desktop / internal / draftstate / schema_upgrade_test.go
1 package draftstate
2
3 import (
4 "database/sql"
5 "fmt"
6 "path/filepath"
7 "testing"
8 )
9
10 // Structural schema fixtures complement tagged v4 databases: no public tag in
11 // 1.38.3–11 emitted schemas 1–3, but the existing migration contract accepts them.
12 func TestEveryPreviousDraftSchemaPreservesWALAndOpaqueRecords(t *testing.T) {
13 for version := 1; version <= 3; version++ {
14 t.Run(fmt.Sprint(version), func(t *testing.T) {
15 path := filepath.Join(t.TempDir(), "drafts.sqlite")
16 initial := New(path)
17 draft, _, err := initial.Open(t.Context(), "workspace", "project", "/synthetic/project", "draft", `{"model":"frozen","futureSetting":{"keep":true}}`)
18 if err != nil {
19 t.Fatal(err)
20 }
21 const content = `{"text":"unsent","attachments":[{"path":"missing-original-file"}],"futureContent":[1,2,3]}`
22 draft, err = initial.Save(t.Context(), draft.ID, draft.Revision, content, draft.SettingsJSON, false)
23 if err != nil {
24 t.Fatal(err)
25 }
26 if _, err := initial.Save(t.Context(), draft.ID, draft.Revision-1, `{"text":"conflict copy"}`, draft.SettingsJSON, false); err == nil {
27 t.Fatal("fixture must contain a conflict")
28 }
29 const request = `{"snapshotVersion":3,"settings":{"model":"frozen"},"futureRequest":true}`
30 _, _, err = initial.BeginOperation(t.Context(), Operation{ID: "operation", DraftID: draft.ID, WorkspaceID: draft.WorkspaceID, DraftRevision: draft.Revision, SessionID: "session", TopicID: "topic", SubmissionID: "submission", Fingerprint: "fingerprint", RequestJSON: request})
31 if err != nil {
32 t.Fatal(err)
33 }
34 if err := initial.SetRestore(t.Context(), draft.ID); err != nil {
35 t.Fatal(err)
36 }
37 if err := initial.Close(); err != nil {
38 t.Fatal(err)
39 }
40 db, err := sql.Open("sqlite", path)
41 if err != nil {
42 t.Fatal(err)
43 }
44 defer db.Close()
45 statements := []string{`PRAGMA wal_autocheckpoint=0`, `DROP INDEX operations_by_request`, `ALTER TABLE operations DROP COLUMN request_id`, `ALTER TABLE operations DROP COLUMN source_digest`, `ALTER TABLE operations DROP COLUMN operation_revision`, `ALTER TABLE operations DROP COLUMN execution_json`}
46 if version == 1 {
47 statements = append(statements, `ALTER TABLE operations DROP COLUMN topic_id`)
48 }
49 statements = append(statements, fmt.Sprintf("PRAGMA user_version=%d", version), `CREATE TABLE future_data(value TEXT)`, `INSERT INTO future_data VALUES('committed only in WAL')`)
50 for _, statement := range statements {
51 if _, err := db.Exec(statement); err != nil {
52 t.Fatal(err)
53 }
54 }
55 for range 2 {
56 upgraded := New(path)
57 got, err := upgraded.Restore(t.Context())
58 if err != nil || got.ID != draft.ID || got.ContentJSON != content || got.SettingsJSON != draft.SettingsJSON || got.Revision != draft.Revision {
59 t.Fatalf("draft changed across upgrade: %+v %v", got, err)
60 }
61 op, err := upgraded.Operation(t.Context(), "operation")
62 if err != nil || op.SessionID != "session" || op.SubmissionID != "submission" || op.RequestJSON != request || op.Phase != "reserved" {
63 t.Fatalf("operation changed across upgrade: %+v %v", op, err)
64 }
65 if version > 1 && op.TopicID != "topic" {
66 t.Fatal("existing topic identity changed")
67 }
68 if err := upgraded.Close(); err != nil {
69 t.Fatal(err)
70 }
71 }
72 backup, err := sql.Open("sqlite", path+".pre-v4.sqlite")
73 if err != nil {
74 t.Fatal(err)
75 }
76 defer backup.Close()
77 var backupVersion int
78 if err := backup.QueryRow(`PRAGMA user_version`).Scan(&backupVersion); err != nil || backupVersion != version {
79 t.Fatalf("backup version=%d %v", backupVersion, err)
80 }
81 for _, reader := range []*sql.DB{db, backup} {
82 var marker, conflict string
83 if err := reader.QueryRow(`SELECT value FROM future_data`).Scan(&marker); err != nil || marker != "committed only in WAL" {
84 t.Fatalf("opaque WAL data lost: %q %v", marker, err)
85 }
86 if err := reader.QueryRow(`SELECT content_json FROM conflicts`).Scan(&conflict); err != nil || conflict != `{"text":"conflict copy"}` {
87 t.Fatalf("conflict copy lost: %q %v", conflict, err)
88 }
89 }
90 })
91 }
92 }
93
93 lines GO