返回 DeepSeek-Reasonix
generate-manual-session-fixtures.py
根目录 / scripts / generate-manual-session-fixtures.py
1 #!/usr/bin/env python3
2 """Generate rollback fixtures with the tagged Desktop storage implementations.
3
4 Run from any directory. Historical source is transient; generated files contain
5 only synthetic identities and content. No user home or application is opened.
6 """
7 import pathlib
8 import shutil
9 import subprocess
10 import tempfile
11
12 ROOT = pathlib.Path(__file__).resolve().parents[1]
13 DESKTOP = ROOT / "desktop"
14 OUTPUT = DESKTOP / "testdata" / "manual-session-upgrade"
15
16
17 def git(*args):
18 return subprocess.check_output(["git", *args], cwd=ROOT)
19
20
21 for version, tag in [(v, "desktop-v" + v) for v in ("1.38.9", "1.38.10", "1.38.11")] + [
22 ("main-v2-pr10469", "e8762acc9"), ("main-v2-pr10572", "54826e46a")]:
23 output = OUTPUT / version
24 output.mkdir(parents=True, exist_ok=True)
25 temporary = pathlib.Path(tempfile.mkdtemp(prefix="rollbackfixture", dir=DESKTOP / "internal"))
26 try:
27 imports = ['"context"', '"os"', '"path/filepath"', 'currentstate "reasonix/desktop/internal/workspacestate"']
28 prefix = "reasonix/desktop/internal/" + temporary.name
29 for package in ("workspacestate", "draftstate"):
30 source = "desktop/internal/" + package
31 paths = git("ls-tree", "-r", "--name-only", tag, source).decode().splitlines()
32 paths = [p for p in paths if str(pathlib.PurePosixPath(p).parent) == source
33 and p.endswith(".go") and not p.endswith("_test.go")]
34 if not paths:
35 continue
36 destination = temporary / package
37 destination.mkdir()
38 for path in paths:
39 (destination / pathlib.Path(path).name).write_bytes(git("show", tag + ":" + path))
40 imports.append(f'{package} "{prefix}/{package}"')
41 body = '''
42 ctx:=context.Background(); output:=os.Args[1]
43 registry:=workspacestate.NewStore(filepath.Join(output,"workspace-state.json"))
44 must(registry.EnsureWorkspace(ctx,workspacestate.Workspace{ID:"global",Root:"/fixture/workspace",Title:"Synthetic upgrade fixture",Visible:true}))
45 must(registry.AttachSession(ctx,"","global","existing-session",""))
46 must(registry.BeginCreate(ctx,workspacestate.PendingCreate{OperationID:"interrupted-create",WorkspaceID:"global",SessionID:"reserved-session"}))
47 // Upgrade a separate copy, then exercise the real tagged reader and writer.
48 roundtrip,err:=os.MkdirTemp("","reasonix-registry-roundtrip-");must(err);defer os.RemoveAll(roundtrip)
49 source,err:=os.ReadFile(filepath.Join(output,"workspace-state.json"));must(err)
50 checkPath:=filepath.Join(roundtrip,"workspace-state.json");must(os.WriteFile(checkPath,source,0600))
51 current:=currentstate.NewStore(checkPath);must(current.AttachSession(ctx,"","global","new-version-session",""))
52 old:=workspacestate.NewStore(checkPath);_,oldReadError:=old.Load(ctx)
53 '''
54 if version == "1.38.9":
55 body += 'if oldReadError==nil {panic("1.38.9 unexpectedly accepted registry v3")}\n'
56 else:
57 body += '''must(oldReadError)
58 must(old.AttachSession(ctx,"","global","continued-in-old-version",""))
59 reupgraded,err:=currentstate.NewStore(checkPath).Load(ctx);must(err)
60 found:=false;for _,id:=range reupgraded.Workspaces["global"].SessionIDs {if id=="continued-in-old-version" {found=true}}
61 if !found {panic("old-version mutation lost on reupgrade")}
62 '''
63 if (temporary / "draftstate").exists():
64 imports.append('"fmt"')
65 body += '''
66 drafts:=draftstate.New(filepath.Join(output,"drafts.sqlite")); defer drafts.Close()
67 for _,phase:=range []string{"editable","settings-only","converted","reserved","starting","dispatching","dispatching_shell","dispatch_unknown","accepted","cancel_requested","resume_required","failed","cancelled"} {
68 draft,_,err:=drafts.Open(ctx,"workspace-"+phase,"project","/fixture/"+phase,"draft-"+phase,`{"model":"fixture/model","modelSource":"explicit","mode":"normal","toolApprovalMode":"ask","disabledMcp":{},"mcpOrder":[]}`);must(err)
69 if phase=="settings-only" {continue}
70 revision:=draft.Revision
71 draft,err=drafts.Save(ctx,draft.ID,revision,`{"text":"unsent upgrade fixture","attachments":[{"path":"missing-historical-file"}],"future":{"preserve":true}}`,draft.SettingsJSON,false);must(err)
72 if phase=="editable" {_,err=drafts.Save(ctx,draft.ID,revision,`{"text":"losing historical writer"}`,draft.SettingsJSON,false);if err==nil {panic("expected conflict")};continue}
73 frozen:=fmt.Sprintf(`{"snapshotVersion":%d,"draftId":%q,"expectedRevision":%d,"content":{"text":"frozen historical request"},"settings":{"model":"fixture/frozen","modelSource":"explicit","toolApprovalMode":"read-only"},"futureSnapshot":{"preserve":true}}`,draftstate.SnapshotVersion,draft.ID,draft.Revision)
74 op,_,err:=drafts.BeginOperation(ctx,draftstate.Operation{ID:"operation-"+phase,DraftID:draft.ID,WorkspaceID:draft.WorkspaceID,DraftRevision:draft.Revision,SessionID:"session-"+phase,TopicID:"topic-"+phase,SubmissionID:"submit-"+phase,Fingerprint:"synthetic",RequestJSON:frozen});must(err)
75 if phase=="converted" {_,err=drafts.SetOperationPhase(ctx,op.ID,"accepted","");must(err);must(drafts.Convert(ctx,draft.ID,op.ID))} else if phase!="reserved" {_,err=drafts.SetOperationPhase(ctx,op.ID,phase,"");must(err)}
76 }
77 '''
78 (temporary / "main.go").write_text("package main\nimport (\n" + "\n".join(imports) + "\n)\nfunc must(err error){if err!=nil{panic(err)}}\nfunc main(){" + body + "}\n")
79 # A fresh output avoids idempotently reopening an earlier fixture.
80 for name in ("workspace-state.json", "drafts.sqlite", "drafts.sqlite-wal", "drafts.sqlite-shm"):
81 (output / name).unlink(missing_ok=True)
82 subprocess.run(["go", "run", "./internal/" + temporary.name, str(output)], cwd=DESKTOP, check=True)
83 print(f"PASS {tag}: fixture generation and registry downgrade/reupgrade boundary", flush=True)
84 (output / "SOURCE").write_text(tag + "\n" + git("rev-parse", tag + "^{commit}").decode())
85 for lock in output.glob("*.lock"):
86 lock.unlink()
87 finally:
88 shutil.rmtree(temporary)
89
89 lines PYTHON