返回 DeepSeek-Reasonix
generate-session-history-fixtures.py
根目录 / scripts / generate-session-history-fixtures.py
1 #!/usr/bin/env python3
2 """Run each release's real Go writers in an isolated checkout (no user data).
3
4 The generated histories, event sidecars and content objects are synthetic.
5 Unlike schema-shaped JSON fixtures, these exercise the actual tagged encoder,
6 checksums, framing and externalization. Requires the historical Go dependencies.
7 """
8 import argparse
9 import hashlib
10 import json
11 import os
12 import pathlib
13 import re
14 import shutil
15 import subprocess
16 import tempfile
17
18 ROOT = pathlib.Path(__file__).resolve().parents[1]
19 OUTPUT = ROOT / "desktop/testdata/session-history-upgrade"
20 TEMPLATE = ROOT / "scripts/fixtures/session-history.go.txt"
21
22
23 def redact_writer_hosts(output):
24 # Writer announcements are environment metadata, not conversation content.
25 # Preserve byte lengths so tagged event-index offsets stay valid.
26 for path in output.rglob("*.events.jsonl"):
27 lines = []
28 for line in path.read_bytes().splitlines(keepends=True):
29 record = json.loads(line)
30 if record.get("type") == "writer":
31 line = re.sub(rb'("hostname":")([^"\\]*)(")',
32 lambda match: match[1] + b"fixture-host".ljust(len(match[2]), b"-")[:len(match[2])] + match[3], line)
33 lines.append(line)
34 path.write_bytes(b"".join(lines))
35
36
37 def generate(version, writers_dir, build_only=False):
38 tag = "desktop-v" + version
39 commit = subprocess.check_output(["git", "rev-parse", tag + "^{commit}"], cwd=ROOT, text=True).strip()
40 with tempfile.TemporaryDirectory(prefix="reasonix-tagged-history-") as temporary:
41 checkout = pathlib.Path(temporary) / "source"
42 checkout.mkdir()
43 archive = pathlib.Path(temporary) / "source.tar"
44 with archive.open("wb") as destination:
45 subprocess.run(["git", "archive", tag, "go.mod", "go.sum", "internal", "sdk"], cwd=ROOT, stdout=destination, check=True)
46 subprocess.run(["tar", "-xf", str(archive), "-C", str(checkout)], check=True)
47 # The encoder remains the tagged implementation. Supply a synthetic
48 # process identity before its first write so fixtures contain no host name.
49 (checkout / "internal/agent/rollback_fixture_identity.go").write_text(
50 'package agent\nfunc init() { sessionWriterID = "historical-fixture-writer" }\n')
51 program = checkout / "cmd/rollback-history-fixture"
52 program.mkdir(parents=True)
53 source = TEMPLATE.read_text()
54 canonical = int(version.rsplit(".", 1)[1]) >= 8
55 source = source.replace("// CANONICAL_IMPORT", '"context"; "errors"; "reasonix/internal/session"' if canonical else "")
56 start, end = source.index("// CANONICAL_START"), source.index("// CANONICAL_END")
57 if not canonical:
58 source = source[:start] + source[end:]
59 source = source.replace("// CANONICAL_CALL", "writeCanonical(output, messages)" if canonical else "")
60 source = source.replace("// CONTINUE_CALL", 'if len(os.Args)>2 { continueCanonical(os.Args[1],os.Args[2]); return }' if canonical else "")
61 source = source.replace("// FORMAL_CALL", '''
62 service,err:=session.NewService("desktop",session.NewFilesystemPersistence(filepath.Join(output,"formal")));must(err)
63 runtime,err:=service.Create(ctx,session.CreateOptions{SessionID:"formal-session",CWD:"/synthetic/workspace",Origin:session.SessionOriginNew});must(err)
64 var events []session.Event
65 for _,message:=range messages {events=append(events,session.Event{Kind:"message/complete",Payload:body(map[string]any{"message":message})})}
66 _,err=runtime.Session().Append(ctx,session.Batch{OperationID:"formal-history",Events:events});must(err)
67 must(service.Close(ctx,runtime.Ref()))
68 ''' if int(version.rsplit(".", 1)[1]) >= 9 else "")
69 (program / "main.go").write_text(source)
70 output = OUTPUT / version
71 # Old writers can consult config paths. Keep every such path disposable.
72 isolated_home = pathlib.Path(temporary) / "home"
73 env = dict(os.environ, REASONIX_HOME=str(isolated_home), REASONIX_STATE_HOME=str(isolated_home), REASONIX_CACHE_HOME=str(isolated_home / "cache"))
74 executable = pathlib.Path(writers_dir).resolve() / version if writers_dir else pathlib.Path(temporary) / "writer"
75 executable.parent.mkdir(parents=True, exist_ok=True)
76 subprocess.run(["go", "build", "-o", str(executable), "./cmd/rollback-history-fixture"], cwd=checkout, env=env, check=True)
77 if build_only:
78 print(f"PASS {tag}: built historical reader/writer {executable}", flush=True)
79 return
80 if output.exists():
81 shutil.rmtree(output)
82 output.mkdir(parents=True)
83 subprocess.run([str(executable), str(output)], cwd=checkout, env=env, check=True)
84 redact_writer_hosts(output)
85 # Locks/caches are rebuildable, not part of the durable fixture.
86 for path in list(output.rglob("*")):
87 if path.is_file() and (path.name.endswith((".lock", ".lease")) or ".query-cache" in path.parts):
88 path.unlink()
89 files = {str(path.relative_to(output)): hashlib.sha256(path.read_bytes()).hexdigest()
90 for path in sorted(output.rglob("*")) if path.is_file()}
91 (output / "SOURCE.json").write_text(json.dumps({"tag": tag, "commit": commit,
92 "normalizations": ["synthetic writer identity", "length-preserving writer hostname"], "files": files}, indent=2) + "\n")
93 print(f"PASS {tag} ({commit[:12]}): historical writer, {len(files)} durable files", flush=True)
94
95
96 if __name__ == "__main__":
97 parser = argparse.ArgumentParser(description=__doc__)
98 parser.add_argument("--writers-dir", help="retain isolated writer executables for previous-reader roundtrip tests")
99 parser.add_argument("--build-only", action="store_true", help="build old readers without changing existing fixture files")
100 parser.add_argument("versions", nargs="*", default=[f"1.38.{n}" for n in range(3, 12)])
101 args = parser.parse_args()
102 if args.build_only and not args.writers_dir:
103 parser.error("--build-only requires --writers-dir")
104 for version in args.versions:
105 generate(version, args.writers_dir, args.build_only)
106
106 lines PYTHON