diff --git a/docs/architecture/persisted-contracts.md b/docs/architecture/persisted-contracts.md index fc8a5b3..2c97ed9 100644 --- a/docs/architecture/persisted-contracts.md +++ b/docs/architecture/persisted-contracts.md @@ -54,7 +54,7 @@ A golden over the current writer does not protect these. A reader can stop accep | Artifacts | run `manifest.json`, `result.json`, `winner.patch`, apply receipts (`applications/*.json`), in every state a standalone `run` leaves them (room-, session- and workflow-origin artifacts are frozen with those families) | `runs list` / `show` / `prune`, `conversations list` / `show`, `diff`, `apply`, `recover` (`_bound_receipt`), `continue`, the TUI artifact view | `test_frozen_artifacts.py` (v0.1.2, v0.3.0) | | Rooms | the room database (`rooms.sqlite3`, with the WAL a killed process leaves): rooms, rounds, candidates, members, workspaces, events, `sessions_json`, `selection_json` v1, `provenance_json` v1/v2, `conversation_json` v1; candidate `manifest.json`, patch and output; room-bound run artifacts. No release wrote `conversation_json` v2 or judgment rows | `room list` / `rounds` / `show` / `candidates` / `diff` / `compare` / `export`, `select`, `apply`, `judge`, `recover`, `close`, `prune` `RoomStore.member_sessions` (and through these `RoomCandidateStore.load`, `_parse_sessions_json`, `selection_record_status`, `room_outcome_status`, `round_provenance_status`, `round_conversation_status`); the standalone `apply` / `recover` refusal and skip for room-bound artifacts; a HEAD `room round` on a released room (`branch-from` resuming the released sessions on the released base patch, `fresh`, a first round on a draft, and the `continue-own` refusal when the released workspaces are gone); `room attach` over the released member log paths (tmux faked); the `apply --verify` settings notices; raw export (`--include` preview and confirm); the TUI Rooms tab's readers and pure renders (`build_room_evidence`, `render_room`, `render_round_table`, and the Diff, Output and Details views), not the Textual app | `test_frozen_rooms.py` (v0.1.2, v0.2.0, v0.3.0) | | Workflows | the workflow database (`workflows.db`): rows with `spec_json`, manifest `schema_version` 3 with `calls_used`, events, and from v0.3.0 the lineage columns and `file`/`revision` origins; per workflow `spec.json`, planner output, node bundles (`result.json`, `winner.patch`, `commit.json` v3) and stream logs; workflow-origin run artifacts, approved or not, with `plan.final_agent` from v0.3.0. No release wrote manifest v1 or v2 (both normalize to v3 before the insert); those readers stay covered by `test_workflow_durable.py::test_marked_v1_migration_preserves_exact_count_and_control_fields` and `test_workflow.py::test_a_workflow_whose_call_accounting_is_ambiguous_is_refused` | `workflow list` / `show` (`--prompts`) / `status` / `logs` / `lineage` / `export` / `cancel` / `prune`, a dead release-written lease recovered by `show` and `status`, `WorkflowStore.events` and `read_node_bundle`; for the final artifact `diff`, `runs show`, `apply` (approved, after a prune, approval healed from the database, refused for a workflow that never completed, `no_changes`), the standalone `recover` skip. A final artifact naming another run, and an unreadable store: `test_cli.py::test_apply_refuses_an_artifact_the_named_workflow_did_not_finalize` and `test_apply_guard_fails_closed_when_the_workflow_store_cannot_be_read`. HEAD execution (`WorkflowCoordinator.run`, what the job worker calls): `run` of released planned, created and revised rows (accounting, stored prompts, dependency output); `resume` of released crashes and failures against the stored deadline (expired: fails before dispatch; before it: the completed dependency's bundle is reused and the unfinished node's attempt refunded, a published final artifact is reused and approved without backfilling `final_agent`, an exhausted node dispatches nothing), refused from a final or unstarted state and while the owner lives, with the released execution history and events kept; `workflow revise` (a v0.2.0 parent gains a lineage); the launch's team peek and state check; `apply --verify` narrowing (v0.2.0: the gate's list only, with a notice; also after a prune); control plans enabled refuse a released workflow planned without one. Written later by the same releases (`freeze.py --part 2b`, `bindings-2b.json`): a node bundle saved just before the crash, reclaimed on resume through `read_node_result` / `read_node_bundle` with its usage added once (`_record_usage`), and re-run when its patch, its commit record or its digest disagrees; control-plan workflows, whose released `control-plan.json` is the control preflight's request, whose binding and validation topology refuse a changed configuration, and whose stored limits and node timeouts bound a run with control plans on, off, or Toolgraph off (`_execution_limits`), plus `show`, `read_control_plan`, `conversations show` and `apply` of a completed one; an interrupted apply of a final artifact, classified by `recover` (completed, not applied, manual recovery; `_bound_receipt`) without touching its approval or the workflow row. Not covered: the TUI's `_read_artifact_result`; no reader revalidates a released execution's `control-preflight.json` | `test_frozen_workflows.py` (v0.2.0, v0.3.0) | -| Sessions and project | isolated-session documents and their checkpoint run artifacts, project marker `.syncmill/project.json` | `IsolatedSessionStore.load` / `list`, `_marker_project_id`; the artifact readers for checkpoint runs | not yet | +| Sessions and project | isolated-session documents in every state a release leaves (open, checkpointed, closed, and from injected exits creating, closing, recovery_required) and their checkpoint run artifacts; the project marker `.syncmill/project.json` and the `syncmill.toml` that `init` writes | `session isolated-list` (`IsolatedSessionStore.list`: live, source staleness in the stored fingerprint's version), `IsolatedSessionStore.load` path binding (repository, workspace run root, worktree, derived tmux name) through `attach-isolated` (state, liveness, pane cwd), `checkpoint` (a missing worktree) and `close` (each state, a missing workspace), `tui_collaboration.render_session`; for the checkpoint artifact `runs list` / `show`, `diff`, `conversations show` / `list` / `close`, `continue` (refused), `recover` (skipped), `apply` and `validate_applicable`, `discard`, `prune` (pending kept), the TUI `render_artifact` / `_read_artifact_result`; `project_id` with the marker untracked, tracked or symlinked, `load_project_config` of the released config, `initialize_project` falling back to the marker. Not covered: a successful checkpoint or worktree cleanup (a release-written workspace is not relocatable) | `test_frozen_sessions.py` (v0.1.2, v0.2.0, v0.3.0) | | Board | item rows and their evidence and inputs, decisions | `_row_to_item`, `_load_evidence`, `_row_to_decision` | not yet | | Memory backends | Mem0 and LangGraph stored payloads | the backends' read paths | not yet | | TUI jobs | job documents, `schema_version` 1 and 2 | `JobStore.list_jobs` | not yet | diff --git a/tests/fixtures/persisted/sessions/freeze.py b/tests/fixtures/persisted/sessions/freeze.py new file mode 100644 index 0000000..8199f2e --- /dev/null +++ b/tests/fixtures/persisted/sessions/freeze.py @@ -0,0 +1,345 @@ +"""Produce the frozen isolated-session documents under this directory (#271). + +Run once per release, with THAT release's interpreter and source, never the +current one. The output directory must not exist; frozen fixtures are never +regenerated. + + git worktree add --detach /tmp/wt-v0.3.0 v0.3.0 + (cd /tmp/wt-v0.3.0 && uv sync --frozen --no-dev) + python tests/fixtures/persisted/sessions/freeze.py \\ + --release-root /tmp/wt-v0.3.0 --out tests/fixtures/persisted/sessions/v0.3.0 + +The release's own `syncmill init` initializes one repository; its marker +(`.syncmill/project.json`) stays untracked and ignored, and `syncmill.toml` is +committed, so every session snapshots the same tree. Each session scenario is +its own project (`SYNCMILL_PROJECT__ID`), made with the release's +`session open codex --no-attach`, `session checkpoint` and `session close`. +`session open` launches a fake interactive `codex` in a real tmux. A `tmux` on +the fake PATH execs the real one with a private socket and no config, so the +user's tmux server is never touched; that private server is killed at the end. + +Three states need a process to stop at one exact point. A `.pth` hook loaded +into the release venv for the run only (active only while HOME holds a +`crash-at` file) provides them: +- `creating`: exit on entry to the launch, after the `creating` document; +- `closing`: exit right after the `closing` document is written; +- `recovery-required`: the worktree cleanup reports failure. + +Frozen for `project`: `syncmill.toml` and the marker directory's two files, +stored as `marker.json` and `marker.gitignore` (the latter, from v0.2.0, names +`project.json`, so Git would ignore the marker under its own name). + +Frozen per session scenario: `//sessions/*.json` and the +project's `runs/` (checkpoint artifacts). Workspaces are not frozen: they are +Git repositories holding absolute paths. `bindings.json` records the values +bound to the generating machine. +""" + +from __future__ import annotations + +import argparse +import json +import os +import shutil +import stat +import subprocess +import sys +import tempfile +import time +from pathlib import Path + +TREE = {"calc.py": "def add(a, b):\n return a - b\n"} +FIXED = "def add(a, b):\n return a + b\n" + +# The interactive agent `session open codex` launches in tmux: it repairs +# calc.py in its workspace, says so under HOME/started, then waits to be killed. +FAKE_CODEX = '''\ +import os, sys, time +from pathlib import Path +from uuid import uuid4 +if sys.argv[1:2] in (["--version"], ["--help"]) or sys.argv[1:3] == ["login", "status"]: + print("codex (fake) - logged in"); sys.exit(0) +Path("calc.py").write_text(FIXED, encoding="utf-8") +started = Path(os.environ["HOME"]) / "started" +started.mkdir(exist_ok=True) +marker = started / uuid4().hex +marker.with_suffix(".tmp").write_text(os.getcwd(), encoding="utf-8") +os.replace(marker.with_suffix(".tmp"), marker) +time.sleep(3600) +'''.replace("FIXED", repr(FIXED)) + +CRASH_HOOK = '''\ +import os +from pathlib import Path + + +def _install(): + flag = Path(os.environ.get("HOME", "/nonexistent")) / "crash-at" + if not flag.is_file(): + return + point = flag.read_text().strip() + import syncmill.isolated_session as isolated + + if point == "create": + def _create_session(*args, **kwargs): + os._exit(0) # the creating document is written; tmux never starts + isolated._create_session = _create_session + elif point == "closing": + original = isolated.IsolatedSessionStore._write + + def _write(self, value, *args, **kwargs): + original(self, value, *args, **kwargs) + if value.get("state") == "closing": + os._exit(0) # closing recorded; tmux and the worktree untouched + isolated.IsolatedSessionStore._write = _write + elif point == "cleanup": + isolated.cleanup_worktree = lambda *args, **kwargs: False + + +_install() +''' +HOOK_MODULE = "_syncmill_freeze_sessions_hook" + +GIT_ENV = { + "GIT_AUTHOR_NAME": "frozen", "GIT_AUTHOR_EMAIL": "frozen@example.invalid", + "GIT_COMMITTER_NAME": "frozen", "GIT_COMMITTER_EMAIL": "frozen@example.invalid", + "GIT_AUTHOR_DATE": "2026-01-01T00:00:00Z", "GIT_COMMITTER_DATE": "2026-01-01T00:00:00Z", +} + +#: scenario -> (state, revision) the release must leave its session in. +EXPECTED = { + "open": ("open", 1), + "checkpointed": ("open", 2), + "closed": ("closed", 4), + "creating": ("creating", 0), + "closing": ("closing", 2), + "recovery-required": ("recovery_required", 3), +} + + +def _site_packages(python: Path) -> Path: + return Path(subprocess.run( + [str(python), "-c", "import sysconfig; print(sysconfig.get_paths()['purelib'])"], + check=True, capture_output=True, text=True, + ).stdout.strip()) + + +def main() -> None: + parser = argparse.ArgumentParser() + parser.add_argument("--release-root", type=Path, required=True) + parser.add_argument("--out", type=Path, required=True) + args = parser.parse_args() + release = args.release_root.resolve() + out = args.out.resolve() + if out.exists(): + sys.exit(f"{out} exists; frozen fixtures are never regenerated") + real_tmux = shutil.which("tmux") + if real_tmux is None: + sys.exit("a real tmux is required: the release launches the agent in one") + python = release / ".venv" / "bin" / "python" + syncmill = release / ".venv" / "bin" / "syncmill" + version = subprocess.run( + [str(python), "-c", "import importlib.metadata as m; print(m.version('syncmill'))"], + check=True, capture_output=True, text=True, + ).stdout.strip() + site = _site_packages(python) + hook_files = [site / f"{HOOK_MODULE}.py", site / f"zz{HOOK_MODULE}.pth"] + if any(path.exists() for path in hook_files): + sys.exit(f"{site} already holds the freeze hook") + + # A tmux socket path may not exceed ~104 bytes: a short directory of its own. + sockets = Path(tempfile.mkdtemp(prefix="smss.", dir="/tmp")) + socket_path = sockets / "s" + box = Path(tempfile.mkdtemp()).resolve() + stopped = False + try: + hook_files[0].write_text(CRASH_HOOK, encoding="utf-8") + hook_files[1].write_text(f"import {HOOK_MODULE}\n", encoding="utf-8") + try: + _generate(box, real_tmux, socket_path, syncmill, version, out) + finally: + # Before the box goes: the agents run inside it. + stopped = _stop_private_server(real_tmux, socket_path) + finally: + for path in hook_files: + path.unlink(missing_ok=True) + if stopped: + shutil.rmtree(box, ignore_errors=True) + shutil.rmtree(sockets, ignore_errors=True) + if not stopped: + # Nothing is removed while agents may still run in the box. + sys.exit( + f"the private tmux server did not stop; stop it with " + f"`tmux -S {socket_path} kill-server`, then remove {box} and {sockets}" + ) + + +def _stop_private_server(real_tmux: str, socket_path: Path) -> bool: + """Kill the private server and prove it is gone: a server that answers + `list-sessions` afterwards still runs agents.""" + subprocess.run([real_tmux, "-S", str(socket_path), "kill-server"], capture_output=True) + deadline = time.monotonic() + 10 + while time.monotonic() < deadline: + probe = subprocess.run([real_tmux, "-S", str(socket_path), "list-sessions"], + capture_output=True) + if probe.returncode != 0: + return True + time.sleep(0.2) + return False + + +def _generate(box: Path, real_tmux: str, socket_path: Path, syncmill: Path, version: str, + out: Path) -> None: + repo, home, state, fakebin = (box / n for n in ("repo", "home", "state", "fakebin")) + for folder in (repo, home, state, fakebin): + folder.mkdir() + (fakebin / "codex").write_text(f"#!{sys.executable}\n" + FAKE_CODEX, encoding="utf-8") + (fakebin / "codex").chmod(0o755) + (fakebin / "tmux").write_text( + f'#!/bin/sh\nexec {real_tmux} -S {socket_path} -f /dev/null "$@"\n', encoding="utf-8" + ) + (fakebin / "tmux").chmod(0o755) + (fakebin / "python3").symlink_to(sys.executable) + base_env = { + "HOME": str(home), + "PATH": os.pathsep.join([str(fakebin), "/usr/bin", "/bin"]), + "TMPDIR": str(box), + "TERM": "xterm-256color", + "GIT_CONFIG_NOSYSTEM": "1", + "PYTHONDONTWRITEBYTECODE": "1", + "SYNCMILL_AGENT_PRIORITY": json.dumps(["codex"]), + "SYNCMILL_RUNS__ROOT": str(state / "runs"), + "SYNCMILL_WORKSPACE__ROOT": str(state / "workspaces"), + "SYNCMILL_MEMORY__DB_PATH": str(state / "memory" / "memtomem.db"), + "SYNCMILL_MEMORY__MEMORY_DIR": str(state / "memory" / "files"), + "SYNCMILL_BREAKER__STATE_PATH": str(state / "breaker.json"), + "SYNCMILL_TUI__STATE_DIR": str(state / "tui"), + **GIT_ENV, + } + + def git(*argv: str) -> str: + return subprocess.run(["git", *argv], cwd=repo, env=base_env, check=True, + capture_output=True, text=True).stdout + + def cli(env: dict, *argv: str, ok: bool = True) -> str: + done = subprocess.run([str(syncmill), *argv], cwd=repo, env=env, + capture_output=True, text=True) + if ok and done.returncode != 0: + sys.exit(f"{argv}: {done.stdout}{done.stderr}") + return done.stdout + done.stderr + + git("init", "-q", "-b", "main") + for name, text in TREE.items(): + (repo / name).write_text(text, encoding="utf-8") + git("add", "-A") + git("commit", "-q", "-m", "frozen tree") + cli(base_env, "init") + marker = repo / ".syncmill" / "project.json" + marker_mode = stat.S_IMODE(marker.stat().st_mode) + if marker_mode != 0o600: + sys.exit(f"init wrote the marker with mode {oct(marker_mode)}") + # The marker stays untracked and ignored; the config is committed. Every + # session then snapshots the same tree, and HEAD trusts the marker. + (repo / ".git" / "info" / "exclude").write_text(".syncmill/\n", encoding="utf-8") + git("add", "syncmill.toml") + git("commit", "-q", "-m", "syncmill init") + if git("status", "--porcelain"): + sys.exit("the initialized repository is not clean") + + out.mkdir(parents=True) + # Stored under names Git acts on neither in the fixture tree: from v0.2.0 + # `init` writes `.syncmill/.gitignore` naming `project.json` itself. + project_dir = out / "project" + project_dir.mkdir() + for relative, stored in (("syncmill.toml", "syncmill.toml"), + (".syncmill/project.json", "marker.json"), + (".syncmill/.gitignore", "marker.gitignore")): + shutil.copyfile(repo / relative, project_dir / stored) + bindings: dict = { + "release": version, + "box_root": str(box), + "tree": {name: (repo / name).read_text(encoding="utf-8") + for name in (*TREE, "syncmill.toml")}, + "marker": {"id": json.loads(marker.read_text())["id"], "mode": oct(marker_mode)}, + "scenarios": {}, + } + + def env_for(name: str) -> dict: + return {**base_env, "SYNCMILL_PROJECT__ID": f"frozen-sessions-{name}"} + + def sessions_dir(name: str) -> Path: + return state / "runs" / f"frozen-sessions-{name}" / "sessions" + + def open_session(name: str, *, crash_at: str | None = None) -> str: + flag = home / "crash-at" + flag.unlink(missing_ok=True) + if crash_at: + flag.write_text(crash_at) + started = set((home / "started").glob("*")) if (home / "started").is_dir() else set() + cli(env_for(name), "session", "open", "codex", "--no-attach", ok=crash_at is None) + flag.unlink(missing_ok=True) + [document] = sessions_dir(name).glob("*.json") + session_id = document.stem + if crash_at is None: + deadline = time.monotonic() + 30 + while not (set((home / "started").glob("*")) - started + if (home / "started").is_dir() else set()): + if time.monotonic() > deadline: + sys.exit(f"{name}: the fake codex never started") + time.sleep(0.1) + return session_id + + def crash(name: str, point: str, *argv: str) -> None: + (home / "crash-at").write_text(point) + cli(env_for(name), *argv, ok=False) + (home / "crash-at").unlink() + + def freeze(name: str, session_id: str) -> None: + source = state / "runs" / f"frozen-sessions-{name}" + target = out / name + (target / "sessions").mkdir(parents=True) + document = sessions_dir(name) / f"{session_id}.json" + shutil.copyfile(document, target / "sessions" / document.name) + if (source / "runs").is_dir(): + shutil.copytree(source / "runs", target / "runs") + value = json.loads(document.read_text()) + expected = EXPECTED[name] + if (value["state"], value["revision"]) != expected: + sys.exit(f"{name}: left {value['state']}/{value['revision']}, not {expected}") + bindings["scenarios"][name] = { + "project_id": f"frozen-sessions-{name}", + "session_id": session_id, + "agent_id": value["agent_id"], + "state": value["state"], + "revision": value["revision"], + "source_fingerprint": value["source_fingerprint"], + "checkpoints": list(value["checkpoints"]), + } + + # ── the lifecycle ──────────────────────────────────────────────────── + freeze("open", open_session("open")) + + session_id = open_session("checkpointed") + cli(env_for("checkpointed"), "session", "checkpoint", session_id) + freeze("checkpointed", session_id) + + session_id = open_session("closed") + cli(env_for("closed"), "session", "checkpoint", session_id) + cli(env_for("closed"), "session", "close", session_id) + freeze("closed", session_id) + + freeze("creating", open_session("creating", crash_at="create")) + + session_id = open_session("closing") + crash("closing", "closing", "session", "close", session_id) + freeze("closing", session_id) + + session_id = open_session("recovery-required") + crash("recovery-required", "cleanup", "session", "close", session_id) + freeze("recovery-required", session_id) + + (out / "bindings.json").write_text(json.dumps(bindings, indent=2) + "\n") + + +if __name__ == "__main__": + main() diff --git a/tests/fixtures/persisted/sessions/v0.1.2/bindings.json b/tests/fixtures/persisted/sessions/v0.1.2/bindings.json new file mode 100644 index 0000000..76965c0 --- /dev/null +++ b/tests/fixtures/persisted/sessions/v0.1.2/bindings.json @@ -0,0 +1,72 @@ +{ + "release": "0.1.2", + "box_root": "/private/var/folders/4n/mt3km5rs1bn_b0w_d6hvlmhc0000gn/T/tmpmktquzd3", + "tree": { + "calc.py": "def add(a, b):\n return a - b\n", + "syncmill.toml": "config_version = 2\nrepo_root = \".\"\ndefault_strategy = \"route\"\n\n[project]\nid = \"36570eff-a2ea-492b-ae5d-427c2ed92f64\"\nmemory_dir = \".syncmill/memory\"\n\n[gate]\ncommands = []\n" + }, + "marker": { + "id": "36570eff-a2ea-492b-ae5d-427c2ed92f64", + "mode": "0o600" + }, + "scenarios": { + "open": { + "project_id": "frozen-sessions-open", + "session_id": "c5542802-fb6b-44e2-ac1d-7fcce5135a27", + "agent_id": "codex", + "state": "open", + "revision": 1, + "source_fingerprint": "8868703dabc72e8fe3597cc38fca3b8958e86257d1456e68b1676561c292c954", + "checkpoints": [] + }, + "checkpointed": { + "project_id": "frozen-sessions-checkpointed", + "session_id": "71ca0b08-1bf9-4947-aa90-d86420aa8173", + "agent_id": "codex", + "state": "open", + "revision": 2, + "source_fingerprint": "8868703dabc72e8fe3597cc38fca3b8958e86257d1456e68b1676561c292c954", + "checkpoints": [ + "e3d49c5a-1659-4984-9946-4ce0e6963cb7" + ] + }, + "closed": { + "project_id": "frozen-sessions-closed", + "session_id": "b56d6a0a-a9f0-4f07-8629-b611bb2f76ea", + "agent_id": "codex", + "state": "closed", + "revision": 4, + "source_fingerprint": "8868703dabc72e8fe3597cc38fca3b8958e86257d1456e68b1676561c292c954", + "checkpoints": [ + "e3337972-fcea-4722-8a32-fe81f859c352" + ] + }, + "creating": { + "project_id": "frozen-sessions-creating", + "session_id": "73852047-5d3b-404f-868a-ce8f003bf911", + "agent_id": "codex", + "state": "creating", + "revision": 0, + "source_fingerprint": "8868703dabc72e8fe3597cc38fca3b8958e86257d1456e68b1676561c292c954", + "checkpoints": [] + }, + "closing": { + "project_id": "frozen-sessions-closing", + "session_id": "e207d0db-7b0e-4e62-8158-a10c870480b0", + "agent_id": "codex", + "state": "closing", + "revision": 2, + "source_fingerprint": "8868703dabc72e8fe3597cc38fca3b8958e86257d1456e68b1676561c292c954", + "checkpoints": [] + }, + "recovery-required": { + "project_id": "frozen-sessions-recovery-required", + "session_id": "335de61e-1fa2-4532-b5ed-57f5f05c0633", + "agent_id": "codex", + "state": "recovery_required", + "revision": 3, + "source_fingerprint": "8868703dabc72e8fe3597cc38fca3b8958e86257d1456e68b1676561c292c954", + "checkpoints": [] + } + } +} diff --git a/tests/fixtures/persisted/sessions/v0.1.2/checkpointed/runs/e3d49c5a-1659-4984-9946-4ce0e6963cb7/manifest.json b/tests/fixtures/persisted/sessions/v0.1.2/checkpointed/runs/e3d49c5a-1659-4984-9946-4ce0e6963cb7/manifest.json new file mode 100644 index 0000000..f22c7a8 --- /dev/null +++ b/tests/fixtures/persisted/sessions/v0.1.2/checkpointed/runs/e3d49c5a-1659-4984-9946-4ce0e6963cb7/manifest.json @@ -0,0 +1,30 @@ +{ + "schema_version": 1, + "run_id": "e3d49c5a-1659-4984-9946-4ce0e6963cb7", + "project_id": "frozen-sessions-checkpointed", + "parent_run_id": null, + "created_at": "2026-11-10T01:17:51.840840+00:00", + "updated_at": "2026-11-10T01:17:51.841375+00:00", + "state": "pending", + "repo_root": "/private/var/folders/4n/mt3km5rs1bn_b0w_d6hvlmhc0000gn/T/tmpmktquzd3/repo", + "source_fingerprint": "8868703dabc72e8fe3597cc38fca3b8958e86257d1456e68b1676561c292c954", + "snapshot_commit": "74807703b1a8b86574740d09eb416c71205bc309", + "task_id": "3b010a07-c250-4d54-a65f-223a6c0a63e6", + "prompt": "Interactive session checkpoint 71ca0b08-1bf9-4947-aa90-d86420aa8173", + "strategy": "interactive", + "agents": [ + "codex" + ], + "plan": { + "origin": "isolated-session", + "session_id": "71ca0b08-1bf9-4947-aa90-d86420aa8173" + }, + "agent_id": "codex", + "files_changed": [ + "calc.py" + ], + "patch_sha256": "35dc711418e60edc7974186bd853c7ab08db843f16e43066417b07a53d8566ba", + "patch_bytes": 157, + "provider_session_id": null, + "conversation_id": null +} diff --git a/tests/fixtures/persisted/sessions/v0.1.2/checkpointed/runs/e3d49c5a-1659-4984-9946-4ce0e6963cb7/result.json b/tests/fixtures/persisted/sessions/v0.1.2/checkpointed/runs/e3d49c5a-1659-4984-9946-4ce0e6963cb7/result.json new file mode 100644 index 0000000..21df948 --- /dev/null +++ b/tests/fixtures/persisted/sessions/v0.1.2/checkpointed/runs/e3d49c5a-1659-4984-9946-4ce0e6963cb7/result.json @@ -0,0 +1,7 @@ +{ + "status": "completed", + "output": "Checkpoint from isolated session 71ca0b08-1bf9-4947-aa90-d86420aa8173", + "error": null, + "exit_code": 0, + "duration_seconds": 0.0 +} diff --git a/tests/fixtures/persisted/sessions/v0.1.2/checkpointed/runs/e3d49c5a-1659-4984-9946-4ce0e6963cb7/winner.patch b/tests/fixtures/persisted/sessions/v0.1.2/checkpointed/runs/e3d49c5a-1659-4984-9946-4ce0e6963cb7/winner.patch new file mode 100644 index 0000000..95e886e --- /dev/null +++ b/tests/fixtures/persisted/sessions/v0.1.2/checkpointed/runs/e3d49c5a-1659-4984-9946-4ce0e6963cb7/winner.patch @@ -0,0 +1,8 @@ +diff --git a/calc.py b/calc.py +index 12ee743..4693ad3 100644 +--- a/calc.py ++++ b/calc.py +@@ -1,2 +1,2 @@ + def add(a, b): +- return a - b ++ return a + b diff --git a/tests/fixtures/persisted/sessions/v0.1.2/checkpointed/sessions/71ca0b08-1bf9-4947-aa90-d86420aa8173.json b/tests/fixtures/persisted/sessions/v0.1.2/checkpointed/sessions/71ca0b08-1bf9-4947-aa90-d86420aa8173.json new file mode 100644 index 0000000..c005730 --- /dev/null +++ b/tests/fixtures/persisted/sessions/v0.1.2/checkpointed/sessions/71ca0b08-1bf9-4947-aa90-d86420aa8173.json @@ -0,0 +1,22 @@ +{ + "schema_version": 1, + "session_id": "71ca0b08-1bf9-4947-aa90-d86420aa8173", + "project_id": "frozen-sessions-checkpointed", + "repo_root": "/private/var/folders/4n/mt3km5rs1bn_b0w_d6hvlmhc0000gn/T/tmpmktquzd3/repo", + "agent_id": "codex", + "state": "open", + "tmux_name": "syncmill-p-frozen-s-codex-71ca0b08", + "run_root": "/private/var/folders/4n/mt3km5rs1bn_b0w_d6hvlmhc0000gn/T/tmpmktquzd3/state/workspaces/session-71ca0b08-1bf9-4947-aa90-d86420aa8173", + "control_repo": "/private/var/folders/4n/mt3km5rs1bn_b0w_d6hvlmhc0000gn/T/tmpmktquzd3/state/workspaces/session-71ca0b08-1bf9-4947-aa90-d86420aa8173/control", + "worktree_path": "/private/var/folders/4n/mt3km5rs1bn_b0w_d6hvlmhc0000gn/T/tmpmktquzd3/state/workspaces/session-71ca0b08-1bf9-4947-aa90-d86420aa8173/worktrees/codex-session--0", + "worktree_branch": "syncmill/codex/session--0", + "base_commit": "74807703b1a8b86574740d09eb416c71205bc309", + "source_fingerprint": "8868703dabc72e8fe3597cc38fca3b8958e86257d1456e68b1676561c292c954", + "source_head": "705c9f393585a2c8ba487630817039ad43752e66", + "checkpoints": [ + "e3d49c5a-1659-4984-9946-4ce0e6963cb7" + ], + "revision": 2, + "created_at": "2026-11-10T01:17:51.325193+00:00", + "updated_at": "2026-11-10T01:17:51.842047+00:00" +} diff --git a/tests/fixtures/persisted/sessions/v0.1.2/closed/runs/e3337972-fcea-4722-8a32-fe81f859c352/manifest.json b/tests/fixtures/persisted/sessions/v0.1.2/closed/runs/e3337972-fcea-4722-8a32-fe81f859c352/manifest.json new file mode 100644 index 0000000..768862a --- /dev/null +++ b/tests/fixtures/persisted/sessions/v0.1.2/closed/runs/e3337972-fcea-4722-8a32-fe81f859c352/manifest.json @@ -0,0 +1,30 @@ +{ + "schema_version": 1, + "run_id": "e3337972-fcea-4722-8a32-fe81f859c352", + "project_id": "frozen-sessions-closed", + "parent_run_id": null, + "created_at": "2026-11-10T01:17:53.062236+00:00", + "updated_at": "2026-11-10T01:17:53.062775+00:00", + "state": "pending", + "repo_root": "/private/var/folders/4n/mt3km5rs1bn_b0w_d6hvlmhc0000gn/T/tmpmktquzd3/repo", + "source_fingerprint": "8868703dabc72e8fe3597cc38fca3b8958e86257d1456e68b1676561c292c954", + "snapshot_commit": "15ad887ba5dc4e620b2f58385fa1f8ccd965c038", + "task_id": "d6815383-afb1-4cda-acfc-d0e87adae37d", + "prompt": "Interactive session checkpoint b56d6a0a-a9f0-4f07-8629-b611bb2f76ea", + "strategy": "interactive", + "agents": [ + "codex" + ], + "plan": { + "origin": "isolated-session", + "session_id": "b56d6a0a-a9f0-4f07-8629-b611bb2f76ea" + }, + "agent_id": "codex", + "files_changed": [ + "calc.py" + ], + "patch_sha256": "35dc711418e60edc7974186bd853c7ab08db843f16e43066417b07a53d8566ba", + "patch_bytes": 157, + "provider_session_id": null, + "conversation_id": null +} diff --git a/tests/fixtures/persisted/sessions/v0.1.2/closed/runs/e3337972-fcea-4722-8a32-fe81f859c352/result.json b/tests/fixtures/persisted/sessions/v0.1.2/closed/runs/e3337972-fcea-4722-8a32-fe81f859c352/result.json new file mode 100644 index 0000000..da1acd0 --- /dev/null +++ b/tests/fixtures/persisted/sessions/v0.1.2/closed/runs/e3337972-fcea-4722-8a32-fe81f859c352/result.json @@ -0,0 +1,7 @@ +{ + "status": "completed", + "output": "Checkpoint from isolated session b56d6a0a-a9f0-4f07-8629-b611bb2f76ea", + "error": null, + "exit_code": 0, + "duration_seconds": 0.0 +} diff --git a/tests/fixtures/persisted/sessions/v0.1.2/closed/runs/e3337972-fcea-4722-8a32-fe81f859c352/winner.patch b/tests/fixtures/persisted/sessions/v0.1.2/closed/runs/e3337972-fcea-4722-8a32-fe81f859c352/winner.patch new file mode 100644 index 0000000..95e886e --- /dev/null +++ b/tests/fixtures/persisted/sessions/v0.1.2/closed/runs/e3337972-fcea-4722-8a32-fe81f859c352/winner.patch @@ -0,0 +1,8 @@ +diff --git a/calc.py b/calc.py +index 12ee743..4693ad3 100644 +--- a/calc.py ++++ b/calc.py +@@ -1,2 +1,2 @@ + def add(a, b): +- return a - b ++ return a + b diff --git a/tests/fixtures/persisted/sessions/v0.1.2/closed/sessions/b56d6a0a-a9f0-4f07-8629-b611bb2f76ea.json b/tests/fixtures/persisted/sessions/v0.1.2/closed/sessions/b56d6a0a-a9f0-4f07-8629-b611bb2f76ea.json new file mode 100644 index 0000000..f6040d6 --- /dev/null +++ b/tests/fixtures/persisted/sessions/v0.1.2/closed/sessions/b56d6a0a-a9f0-4f07-8629-b611bb2f76ea.json @@ -0,0 +1,22 @@ +{ + "schema_version": 1, + "session_id": "b56d6a0a-a9f0-4f07-8629-b611bb2f76ea", + "project_id": "frozen-sessions-closed", + "repo_root": "/private/var/folders/4n/mt3km5rs1bn_b0w_d6hvlmhc0000gn/T/tmpmktquzd3/repo", + "agent_id": "codex", + "state": "closed", + "tmux_name": "syncmill-p-frozen-s-codex-b56d6a0a", + "run_root": "/private/var/folders/4n/mt3km5rs1bn_b0w_d6hvlmhc0000gn/T/tmpmktquzd3/state/workspaces/session-b56d6a0a-a9f0-4f07-8629-b611bb2f76ea", + "control_repo": "/private/var/folders/4n/mt3km5rs1bn_b0w_d6hvlmhc0000gn/T/tmpmktquzd3/state/workspaces/session-b56d6a0a-a9f0-4f07-8629-b611bb2f76ea/control", + "worktree_path": "/private/var/folders/4n/mt3km5rs1bn_b0w_d6hvlmhc0000gn/T/tmpmktquzd3/state/workspaces/session-b56d6a0a-a9f0-4f07-8629-b611bb2f76ea/worktrees/codex-session--0", + "worktree_branch": "syncmill/codex/session--0", + "base_commit": "15ad887ba5dc4e620b2f58385fa1f8ccd965c038", + "source_fingerprint": "8868703dabc72e8fe3597cc38fca3b8958e86257d1456e68b1676561c292c954", + "source_head": "705c9f393585a2c8ba487630817039ad43752e66", + "checkpoints": [ + "e3337972-fcea-4722-8a32-fe81f859c352" + ], + "revision": 4, + "created_at": "2026-11-10T01:17:52.547804+00:00", + "updated_at": "2026-11-10T01:17:53.551917+00:00" +} diff --git a/tests/fixtures/persisted/sessions/v0.1.2/closing/sessions/e207d0db-7b0e-4e62-8158-a10c870480b0.json b/tests/fixtures/persisted/sessions/v0.1.2/closing/sessions/e207d0db-7b0e-4e62-8158-a10c870480b0.json new file mode 100644 index 0000000..f34d232 --- /dev/null +++ b/tests/fixtures/persisted/sessions/v0.1.2/closing/sessions/e207d0db-7b0e-4e62-8158-a10c870480b0.json @@ -0,0 +1,20 @@ +{ + "schema_version": 1, + "session_id": "e207d0db-7b0e-4e62-8158-a10c870480b0", + "project_id": "frozen-sessions-closing", + "repo_root": "/private/var/folders/4n/mt3km5rs1bn_b0w_d6hvlmhc0000gn/T/tmpmktquzd3/repo", + "agent_id": "codex", + "state": "closing", + "tmux_name": "syncmill-p-frozen-s-codex-e207d0db", + "run_root": "/private/var/folders/4n/mt3km5rs1bn_b0w_d6hvlmhc0000gn/T/tmpmktquzd3/state/workspaces/session-e207d0db-7b0e-4e62-8158-a10c870480b0", + "control_repo": "/private/var/folders/4n/mt3km5rs1bn_b0w_d6hvlmhc0000gn/T/tmpmktquzd3/state/workspaces/session-e207d0db-7b0e-4e62-8158-a10c870480b0/control", + "worktree_path": "/private/var/folders/4n/mt3km5rs1bn_b0w_d6hvlmhc0000gn/T/tmpmktquzd3/state/workspaces/session-e207d0db-7b0e-4e62-8158-a10c870480b0/worktrees/codex-session--0", + "worktree_branch": "syncmill/codex/session--0", + "base_commit": "64c593248293d861cfaba60f356e5342d9077ac7", + "source_fingerprint": "8868703dabc72e8fe3597cc38fca3b8958e86257d1456e68b1676561c292c954", + "source_head": "705c9f393585a2c8ba487630817039ad43752e66", + "checkpoints": [], + "revision": 2, + "created_at": "2026-11-10T01:17:54.944169+00:00", + "updated_at": "2026-11-10T01:17:55.425370+00:00" +} diff --git a/tests/fixtures/persisted/sessions/v0.1.2/creating/sessions/73852047-5d3b-404f-868a-ce8f003bf911.json b/tests/fixtures/persisted/sessions/v0.1.2/creating/sessions/73852047-5d3b-404f-868a-ce8f003bf911.json new file mode 100644 index 0000000..131fbc6 --- /dev/null +++ b/tests/fixtures/persisted/sessions/v0.1.2/creating/sessions/73852047-5d3b-404f-868a-ce8f003bf911.json @@ -0,0 +1,20 @@ +{ + "schema_version": 1, + "session_id": "73852047-5d3b-404f-868a-ce8f003bf911", + "project_id": "frozen-sessions-creating", + "repo_root": "/private/var/folders/4n/mt3km5rs1bn_b0w_d6hvlmhc0000gn/T/tmpmktquzd3/repo", + "agent_id": "codex", + "state": "creating", + "tmux_name": "syncmill-p-frozen-s-codex-73852047", + "run_root": "/private/var/folders/4n/mt3km5rs1bn_b0w_d6hvlmhc0000gn/T/tmpmktquzd3/state/workspaces/session-73852047-5d3b-404f-868a-ce8f003bf911", + "control_repo": "/private/var/folders/4n/mt3km5rs1bn_b0w_d6hvlmhc0000gn/T/tmpmktquzd3/state/workspaces/session-73852047-5d3b-404f-868a-ce8f003bf911/control", + "worktree_path": "/private/var/folders/4n/mt3km5rs1bn_b0w_d6hvlmhc0000gn/T/tmpmktquzd3/state/workspaces/session-73852047-5d3b-404f-868a-ce8f003bf911/worktrees/codex-session--0", + "worktree_branch": "syncmill/codex/session--0", + "base_commit": "a23048b893335ee4c43e16fb844d42403fda14a8", + "source_fingerprint": "8868703dabc72e8fe3597cc38fca3b8958e86257d1456e68b1676561c292c954", + "source_head": "705c9f393585a2c8ba487630817039ad43752e66", + "checkpoints": [], + "revision": 0, + "created_at": "2026-11-10T01:17:54.262624+00:00", + "updated_at": "2026-11-10T01:17:54.263096+00:00" +} diff --git a/tests/fixtures/persisted/sessions/v0.1.2/open/sessions/c5542802-fb6b-44e2-ac1d-7fcce5135a27.json b/tests/fixtures/persisted/sessions/v0.1.2/open/sessions/c5542802-fb6b-44e2-ac1d-7fcce5135a27.json new file mode 100644 index 0000000..37fb8e3 --- /dev/null +++ b/tests/fixtures/persisted/sessions/v0.1.2/open/sessions/c5542802-fb6b-44e2-ac1d-7fcce5135a27.json @@ -0,0 +1,20 @@ +{ + "schema_version": 1, + "session_id": "c5542802-fb6b-44e2-ac1d-7fcce5135a27", + "project_id": "frozen-sessions-open", + "repo_root": "/private/var/folders/4n/mt3km5rs1bn_b0w_d6hvlmhc0000gn/T/tmpmktquzd3/repo", + "agent_id": "codex", + "state": "open", + "tmux_name": "syncmill-p-frozen-s-codex-c5542802", + "run_root": "/private/var/folders/4n/mt3km5rs1bn_b0w_d6hvlmhc0000gn/T/tmpmktquzd3/state/workspaces/session-c5542802-fb6b-44e2-ac1d-7fcce5135a27", + "control_repo": "/private/var/folders/4n/mt3km5rs1bn_b0w_d6hvlmhc0000gn/T/tmpmktquzd3/state/workspaces/session-c5542802-fb6b-44e2-ac1d-7fcce5135a27/control", + "worktree_path": "/private/var/folders/4n/mt3km5rs1bn_b0w_d6hvlmhc0000gn/T/tmpmktquzd3/state/workspaces/session-c5542802-fb6b-44e2-ac1d-7fcce5135a27/worktrees/codex-session--0", + "worktree_branch": "syncmill/codex/session--0", + "base_commit": "4d312ff74b7c8e2acb2934291d9dd5b95e97d6b7", + "source_fingerprint": "8868703dabc72e8fe3597cc38fca3b8958e86257d1456e68b1676561c292c954", + "source_head": "705c9f393585a2c8ba487630817039ad43752e66", + "checkpoints": [], + "revision": 1, + "created_at": "2026-11-10T01:17:50.237909+00:00", + "updated_at": "2026-11-10T01:17:50.499750+00:00" +} diff --git a/tests/fixtures/persisted/sessions/v0.1.2/project/marker.gitignore b/tests/fixtures/persisted/sessions/v0.1.2/project/marker.gitignore new file mode 100644 index 0000000..6349da1 --- /dev/null +++ b/tests/fixtures/persisted/sessions/v0.1.2/project/marker.gitignore @@ -0,0 +1 @@ +memory/ diff --git a/tests/fixtures/persisted/sessions/v0.1.2/project/marker.json b/tests/fixtures/persisted/sessions/v0.1.2/project/marker.json new file mode 100644 index 0000000..63f11ca --- /dev/null +++ b/tests/fixtures/persisted/sessions/v0.1.2/project/marker.json @@ -0,0 +1,3 @@ +{ + "id": "36570eff-a2ea-492b-ae5d-427c2ed92f64" +} diff --git a/tests/fixtures/persisted/sessions/v0.1.2/project/syncmill.toml b/tests/fixtures/persisted/sessions/v0.1.2/project/syncmill.toml new file mode 100644 index 0000000..fa1ce50 --- /dev/null +++ b/tests/fixtures/persisted/sessions/v0.1.2/project/syncmill.toml @@ -0,0 +1,10 @@ +config_version = 2 +repo_root = "." +default_strategy = "route" + +[project] +id = "36570eff-a2ea-492b-ae5d-427c2ed92f64" +memory_dir = ".syncmill/memory" + +[gate] +commands = [] diff --git a/tests/fixtures/persisted/sessions/v0.1.2/recovery-required/sessions/335de61e-1fa2-4532-b5ed-57f5f05c0633.json b/tests/fixtures/persisted/sessions/v0.1.2/recovery-required/sessions/335de61e-1fa2-4532-b5ed-57f5f05c0633.json new file mode 100644 index 0000000..d072758 --- /dev/null +++ b/tests/fixtures/persisted/sessions/v0.1.2/recovery-required/sessions/335de61e-1fa2-4532-b5ed-57f5f05c0633.json @@ -0,0 +1,20 @@ +{ + "schema_version": 1, + "session_id": "335de61e-1fa2-4532-b5ed-57f5f05c0633", + "project_id": "frozen-sessions-recovery-required", + "repo_root": "/private/var/folders/4n/mt3km5rs1bn_b0w_d6hvlmhc0000gn/T/tmpmktquzd3/repo", + "agent_id": "codex", + "state": "recovery_required", + "tmux_name": "syncmill-p-frozen-s-codex-335de61e", + "run_root": "/private/var/folders/4n/mt3km5rs1bn_b0w_d6hvlmhc0000gn/T/tmpmktquzd3/state/workspaces/session-335de61e-1fa2-4532-b5ed-57f5f05c0633", + "control_repo": "/private/var/folders/4n/mt3km5rs1bn_b0w_d6hvlmhc0000gn/T/tmpmktquzd3/state/workspaces/session-335de61e-1fa2-4532-b5ed-57f5f05c0633/control", + "worktree_path": "/private/var/folders/4n/mt3km5rs1bn_b0w_d6hvlmhc0000gn/T/tmpmktquzd3/state/workspaces/session-335de61e-1fa2-4532-b5ed-57f5f05c0633/worktrees/codex-session--0", + "worktree_branch": "syncmill/codex/session--0", + "base_commit": "3525ec63111f370df5771448042c531d0d5b708c", + "source_fingerprint": "8868703dabc72e8fe3597cc38fca3b8958e86257d1456e68b1676561c292c954", + "source_head": "705c9f393585a2c8ba487630817039ad43752e66", + "checkpoints": [], + "revision": 3, + "created_at": "2026-11-10T01:17:56.109480+00:00", + "updated_at": "2026-11-10T01:17:56.609454+00:00" +} diff --git a/tests/fixtures/persisted/sessions/v0.2.0/bindings.json b/tests/fixtures/persisted/sessions/v0.2.0/bindings.json new file mode 100644 index 0000000..ce38f66 --- /dev/null +++ b/tests/fixtures/persisted/sessions/v0.2.0/bindings.json @@ -0,0 +1,72 @@ +{ + "release": "0.2.0", + "box_root": "/private/var/folders/4n/mt3km5rs1bn_b0w_d6hvlmhc0000gn/T/tmpfeuw_bsz", + "tree": { + "calc.py": "def add(a, b):\n return a - b\n", + "syncmill.toml": "config_version = 2\nrepo_root = \".\"\ndefault_strategy = \"route\"\n\n[project]\nid = \"bba2632e-b782-4603-93e4-d39a67960f7c\"\nmemory_dir = \".syncmill/memory\"\n\n[gate]\ncommands = []\n" + }, + "marker": { + "id": "bba2632e-b782-4603-93e4-d39a67960f7c", + "mode": "0o600" + }, + "scenarios": { + "open": { + "project_id": "frozen-sessions-open", + "session_id": "23127f27-a1cc-4212-9e14-c672948975a5", + "agent_id": "codex", + "state": "open", + "revision": 1, + "source_fingerprint": "c605ecdc0a3f4df5822c04269e4815c7d3395a26938e4dce93fe3fb0ef1a0d59", + "checkpoints": [] + }, + "checkpointed": { + "project_id": "frozen-sessions-checkpointed", + "session_id": "b77edfa7-844b-4356-82fc-813f62701ab1", + "agent_id": "codex", + "state": "open", + "revision": 2, + "source_fingerprint": "c605ecdc0a3f4df5822c04269e4815c7d3395a26938e4dce93fe3fb0ef1a0d59", + "checkpoints": [ + "fe64c7cf-4267-46f0-84c6-7decaeb8b747" + ] + }, + "closed": { + "project_id": "frozen-sessions-closed", + "session_id": "74f2b6e8-d34b-4223-b0b8-81ba8a984566", + "agent_id": "codex", + "state": "closed", + "revision": 4, + "source_fingerprint": "c605ecdc0a3f4df5822c04269e4815c7d3395a26938e4dce93fe3fb0ef1a0d59", + "checkpoints": [ + "0cee9417-5b05-42bb-8bde-7c63723a586d" + ] + }, + "creating": { + "project_id": "frozen-sessions-creating", + "session_id": "160fca14-4e8b-4881-ae81-6e7b8ce73c67", + "agent_id": "codex", + "state": "creating", + "revision": 0, + "source_fingerprint": "c605ecdc0a3f4df5822c04269e4815c7d3395a26938e4dce93fe3fb0ef1a0d59", + "checkpoints": [] + }, + "closing": { + "project_id": "frozen-sessions-closing", + "session_id": "85004bd7-9141-46ad-9899-8f658b95b3a8", + "agent_id": "codex", + "state": "closing", + "revision": 2, + "source_fingerprint": "c605ecdc0a3f4df5822c04269e4815c7d3395a26938e4dce93fe3fb0ef1a0d59", + "checkpoints": [] + }, + "recovery-required": { + "project_id": "frozen-sessions-recovery-required", + "session_id": "fcc8f978-f4c2-46cd-936e-ecc4398de4ac", + "agent_id": "codex", + "state": "recovery_required", + "revision": 3, + "source_fingerprint": "c605ecdc0a3f4df5822c04269e4815c7d3395a26938e4dce93fe3fb0ef1a0d59", + "checkpoints": [] + } + } +} diff --git a/tests/fixtures/persisted/sessions/v0.2.0/checkpointed/runs/fe64c7cf-4267-46f0-84c6-7decaeb8b747/manifest.json b/tests/fixtures/persisted/sessions/v0.2.0/checkpointed/runs/fe64c7cf-4267-46f0-84c6-7decaeb8b747/manifest.json new file mode 100644 index 0000000..8dfc72b --- /dev/null +++ b/tests/fixtures/persisted/sessions/v0.2.0/checkpointed/runs/fe64c7cf-4267-46f0-84c6-7decaeb8b747/manifest.json @@ -0,0 +1,30 @@ +{ + "schema_version": 1, + "run_id": "fe64c7cf-4267-46f0-84c6-7decaeb8b747", + "project_id": "frozen-sessions-checkpointed", + "parent_run_id": null, + "created_at": "2026-11-10T01:17:58.475747+00:00", + "updated_at": "2026-11-10T01:17:58.476887+00:00", + "state": "pending", + "repo_root": "/private/var/folders/4n/mt3km5rs1bn_b0w_d6hvlmhc0000gn/T/tmpfeuw_bsz/repo", + "source_fingerprint": "c605ecdc0a3f4df5822c04269e4815c7d3395a26938e4dce93fe3fb0ef1a0d59", + "snapshot_commit": "46a594a84f7c4d5a897c7a1574dceb92ecacd9b5", + "task_id": "81d8e41c-62f2-4ff1-a902-bd6d983e9cdd", + "prompt": "Interactive session checkpoint b77edfa7-844b-4356-82fc-813f62701ab1", + "strategy": "interactive", + "agents": [ + "codex" + ], + "plan": { + "origin": "isolated-session", + "session_id": "b77edfa7-844b-4356-82fc-813f62701ab1" + }, + "agent_id": "codex", + "files_changed": [ + "calc.py" + ], + "patch_sha256": "35dc711418e60edc7974186bd853c7ab08db843f16e43066417b07a53d8566ba", + "patch_bytes": 157, + "provider_session_id": null, + "conversation_id": null +} diff --git a/tests/fixtures/persisted/sessions/v0.2.0/checkpointed/runs/fe64c7cf-4267-46f0-84c6-7decaeb8b747/result.json b/tests/fixtures/persisted/sessions/v0.2.0/checkpointed/runs/fe64c7cf-4267-46f0-84c6-7decaeb8b747/result.json new file mode 100644 index 0000000..12c12aa --- /dev/null +++ b/tests/fixtures/persisted/sessions/v0.2.0/checkpointed/runs/fe64c7cf-4267-46f0-84c6-7decaeb8b747/result.json @@ -0,0 +1,7 @@ +{ + "status": "completed", + "output": "Checkpoint from isolated session b77edfa7-844b-4356-82fc-813f62701ab1", + "error": null, + "exit_code": 0, + "duration_seconds": 0.0 +} diff --git a/tests/fixtures/persisted/sessions/v0.2.0/checkpointed/runs/fe64c7cf-4267-46f0-84c6-7decaeb8b747/winner.patch b/tests/fixtures/persisted/sessions/v0.2.0/checkpointed/runs/fe64c7cf-4267-46f0-84c6-7decaeb8b747/winner.patch new file mode 100644 index 0000000..95e886e --- /dev/null +++ b/tests/fixtures/persisted/sessions/v0.2.0/checkpointed/runs/fe64c7cf-4267-46f0-84c6-7decaeb8b747/winner.patch @@ -0,0 +1,8 @@ +diff --git a/calc.py b/calc.py +index 12ee743..4693ad3 100644 +--- a/calc.py ++++ b/calc.py +@@ -1,2 +1,2 @@ + def add(a, b): +- return a - b ++ return a + b diff --git a/tests/fixtures/persisted/sessions/v0.2.0/checkpointed/sessions/b77edfa7-844b-4356-82fc-813f62701ab1.json b/tests/fixtures/persisted/sessions/v0.2.0/checkpointed/sessions/b77edfa7-844b-4356-82fc-813f62701ab1.json new file mode 100644 index 0000000..515a65f --- /dev/null +++ b/tests/fixtures/persisted/sessions/v0.2.0/checkpointed/sessions/b77edfa7-844b-4356-82fc-813f62701ab1.json @@ -0,0 +1,22 @@ +{ + "schema_version": 1, + "session_id": "b77edfa7-844b-4356-82fc-813f62701ab1", + "project_id": "frozen-sessions-checkpointed", + "repo_root": "/private/var/folders/4n/mt3km5rs1bn_b0w_d6hvlmhc0000gn/T/tmpfeuw_bsz/repo", + "agent_id": "codex", + "state": "open", + "tmux_name": "syncmill-p-frozen-s-codex-b77edfa7", + "run_root": "/private/var/folders/4n/mt3km5rs1bn_b0w_d6hvlmhc0000gn/T/tmpfeuw_bsz/state/workspaces/session-b77edfa7-844b-4356-82fc-813f62701ab1", + "control_repo": "/private/var/folders/4n/mt3km5rs1bn_b0w_d6hvlmhc0000gn/T/tmpfeuw_bsz/state/workspaces/session-b77edfa7-844b-4356-82fc-813f62701ab1/control", + "worktree_path": "/private/var/folders/4n/mt3km5rs1bn_b0w_d6hvlmhc0000gn/T/tmpfeuw_bsz/state/workspaces/session-b77edfa7-844b-4356-82fc-813f62701ab1/worktrees/codex-session--0", + "worktree_branch": "syncmill/codex/session--0", + "base_commit": "46a594a84f7c4d5a897c7a1574dceb92ecacd9b5", + "source_fingerprint": "c605ecdc0a3f4df5822c04269e4815c7d3395a26938e4dce93fe3fb0ef1a0d59", + "source_head": "fae6f00bb5ada4b8aa8f1f7b4b6756ac21618caf", + "checkpoints": [ + "fe64c7cf-4267-46f0-84c6-7decaeb8b747" + ], + "revision": 2, + "created_at": "2026-11-10T01:17:58.216179+00:00", + "updated_at": "2026-11-10T01:17:58.478146+00:00" +} diff --git a/tests/fixtures/persisted/sessions/v0.2.0/closed/runs/0cee9417-5b05-42bb-8bde-7c63723a586d/manifest.json b/tests/fixtures/persisted/sessions/v0.2.0/closed/runs/0cee9417-5b05-42bb-8bde-7c63723a586d/manifest.json new file mode 100644 index 0000000..977a78e --- /dev/null +++ b/tests/fixtures/persisted/sessions/v0.2.0/closed/runs/0cee9417-5b05-42bb-8bde-7c63723a586d/manifest.json @@ -0,0 +1,30 @@ +{ + "schema_version": 1, + "run_id": "0cee9417-5b05-42bb-8bde-7c63723a586d", + "project_id": "frozen-sessions-closed", + "parent_run_id": null, + "created_at": "2026-11-10T01:17:59.181256+00:00", + "updated_at": "2026-11-10T01:17:59.182352+00:00", + "state": "pending", + "repo_root": "/private/var/folders/4n/mt3km5rs1bn_b0w_d6hvlmhc0000gn/T/tmpfeuw_bsz/repo", + "source_fingerprint": "c605ecdc0a3f4df5822c04269e4815c7d3395a26938e4dce93fe3fb0ef1a0d59", + "snapshot_commit": "64956ea9ad256d624b9622f28900d3c6eb25c138", + "task_id": "54f7402f-ca50-4eb5-b01a-d7208f3a0892", + "prompt": "Interactive session checkpoint 74f2b6e8-d34b-4223-b0b8-81ba8a984566", + "strategy": "interactive", + "agents": [ + "codex" + ], + "plan": { + "origin": "isolated-session", + "session_id": "74f2b6e8-d34b-4223-b0b8-81ba8a984566" + }, + "agent_id": "codex", + "files_changed": [ + "calc.py" + ], + "patch_sha256": "35dc711418e60edc7974186bd853c7ab08db843f16e43066417b07a53d8566ba", + "patch_bytes": 157, + "provider_session_id": null, + "conversation_id": null +} diff --git a/tests/fixtures/persisted/sessions/v0.2.0/closed/runs/0cee9417-5b05-42bb-8bde-7c63723a586d/result.json b/tests/fixtures/persisted/sessions/v0.2.0/closed/runs/0cee9417-5b05-42bb-8bde-7c63723a586d/result.json new file mode 100644 index 0000000..fa2308d --- /dev/null +++ b/tests/fixtures/persisted/sessions/v0.2.0/closed/runs/0cee9417-5b05-42bb-8bde-7c63723a586d/result.json @@ -0,0 +1,7 @@ +{ + "status": "completed", + "output": "Checkpoint from isolated session 74f2b6e8-d34b-4223-b0b8-81ba8a984566", + "error": null, + "exit_code": 0, + "duration_seconds": 0.0 +} diff --git a/tests/fixtures/persisted/sessions/v0.2.0/closed/runs/0cee9417-5b05-42bb-8bde-7c63723a586d/winner.patch b/tests/fixtures/persisted/sessions/v0.2.0/closed/runs/0cee9417-5b05-42bb-8bde-7c63723a586d/winner.patch new file mode 100644 index 0000000..95e886e --- /dev/null +++ b/tests/fixtures/persisted/sessions/v0.2.0/closed/runs/0cee9417-5b05-42bb-8bde-7c63723a586d/winner.patch @@ -0,0 +1,8 @@ +diff --git a/calc.py b/calc.py +index 12ee743..4693ad3 100644 +--- a/calc.py ++++ b/calc.py +@@ -1,2 +1,2 @@ + def add(a, b): +- return a - b ++ return a + b diff --git a/tests/fixtures/persisted/sessions/v0.2.0/closed/sessions/74f2b6e8-d34b-4223-b0b8-81ba8a984566.json b/tests/fixtures/persisted/sessions/v0.2.0/closed/sessions/74f2b6e8-d34b-4223-b0b8-81ba8a984566.json new file mode 100644 index 0000000..090d6ef --- /dev/null +++ b/tests/fixtures/persisted/sessions/v0.2.0/closed/sessions/74f2b6e8-d34b-4223-b0b8-81ba8a984566.json @@ -0,0 +1,22 @@ +{ + "schema_version": 1, + "session_id": "74f2b6e8-d34b-4223-b0b8-81ba8a984566", + "project_id": "frozen-sessions-closed", + "repo_root": "/private/var/folders/4n/mt3km5rs1bn_b0w_d6hvlmhc0000gn/T/tmpfeuw_bsz/repo", + "agent_id": "codex", + "state": "closed", + "tmux_name": "syncmill-p-frozen-s-codex-74f2b6e8", + "run_root": "/private/var/folders/4n/mt3km5rs1bn_b0w_d6hvlmhc0000gn/T/tmpfeuw_bsz/state/workspaces/session-74f2b6e8-d34b-4223-b0b8-81ba8a984566", + "control_repo": "/private/var/folders/4n/mt3km5rs1bn_b0w_d6hvlmhc0000gn/T/tmpfeuw_bsz/state/workspaces/session-74f2b6e8-d34b-4223-b0b8-81ba8a984566/control", + "worktree_path": "/private/var/folders/4n/mt3km5rs1bn_b0w_d6hvlmhc0000gn/T/tmpfeuw_bsz/state/workspaces/session-74f2b6e8-d34b-4223-b0b8-81ba8a984566/worktrees/codex-session--0", + "worktree_branch": "syncmill/codex/session--0", + "base_commit": "64956ea9ad256d624b9622f28900d3c6eb25c138", + "source_fingerprint": "c605ecdc0a3f4df5822c04269e4815c7d3395a26938e4dce93fe3fb0ef1a0d59", + "source_head": "fae6f00bb5ada4b8aa8f1f7b4b6756ac21618caf", + "checkpoints": [ + "0cee9417-5b05-42bb-8bde-7c63723a586d" + ], + "revision": 4, + "created_at": "2026-11-10T01:17:58.915318+00:00", + "updated_at": "2026-11-10T01:17:59.416920+00:00" +} diff --git a/tests/fixtures/persisted/sessions/v0.2.0/closing/sessions/85004bd7-9141-46ad-9899-8f658b95b3a8.json b/tests/fixtures/persisted/sessions/v0.2.0/closing/sessions/85004bd7-9141-46ad-9899-8f658b95b3a8.json new file mode 100644 index 0000000..040feb1 --- /dev/null +++ b/tests/fixtures/persisted/sessions/v0.2.0/closing/sessions/85004bd7-9141-46ad-9899-8f658b95b3a8.json @@ -0,0 +1,20 @@ +{ + "schema_version": 1, + "session_id": "85004bd7-9141-46ad-9899-8f658b95b3a8", + "project_id": "frozen-sessions-closing", + "repo_root": "/private/var/folders/4n/mt3km5rs1bn_b0w_d6hvlmhc0000gn/T/tmpfeuw_bsz/repo", + "agent_id": "codex", + "state": "closing", + "tmux_name": "syncmill-p-frozen-s-codex-85004bd7", + "run_root": "/private/var/folders/4n/mt3km5rs1bn_b0w_d6hvlmhc0000gn/T/tmpfeuw_bsz/state/workspaces/session-85004bd7-9141-46ad-9899-8f658b95b3a8", + "control_repo": "/private/var/folders/4n/mt3km5rs1bn_b0w_d6hvlmhc0000gn/T/tmpfeuw_bsz/state/workspaces/session-85004bd7-9141-46ad-9899-8f658b95b3a8/control", + "worktree_path": "/private/var/folders/4n/mt3km5rs1bn_b0w_d6hvlmhc0000gn/T/tmpfeuw_bsz/state/workspaces/session-85004bd7-9141-46ad-9899-8f658b95b3a8/worktrees/codex-session--0", + "worktree_branch": "syncmill/codex/session--0", + "base_commit": "a5fb6f5cdcc9c620d2fb54d0ca1ceee22049ba5b", + "source_fingerprint": "c605ecdc0a3f4df5822c04269e4815c7d3395a26938e4dce93fe3fb0ef1a0d59", + "source_head": "fae6f00bb5ada4b8aa8f1f7b4b6756ac21618caf", + "checkpoints": [], + "revision": 2, + "created_at": "2026-11-10T01:18:00.282334+00:00", + "updated_at": "2026-11-10T01:18:00.509210+00:00" +} diff --git a/tests/fixtures/persisted/sessions/v0.2.0/creating/sessions/160fca14-4e8b-4881-ae81-6e7b8ce73c67.json b/tests/fixtures/persisted/sessions/v0.2.0/creating/sessions/160fca14-4e8b-4881-ae81-6e7b8ce73c67.json new file mode 100644 index 0000000..dcf8b3d --- /dev/null +++ b/tests/fixtures/persisted/sessions/v0.2.0/creating/sessions/160fca14-4e8b-4881-ae81-6e7b8ce73c67.json @@ -0,0 +1,20 @@ +{ + "schema_version": 1, + "session_id": "160fca14-4e8b-4881-ae81-6e7b8ce73c67", + "project_id": "frozen-sessions-creating", + "repo_root": "/private/var/folders/4n/mt3km5rs1bn_b0w_d6hvlmhc0000gn/T/tmpfeuw_bsz/repo", + "agent_id": "codex", + "state": "creating", + "tmux_name": "syncmill-p-frozen-s-codex-160fca14", + "run_root": "/private/var/folders/4n/mt3km5rs1bn_b0w_d6hvlmhc0000gn/T/tmpfeuw_bsz/state/workspaces/session-160fca14-4e8b-4881-ae81-6e7b8ce73c67", + "control_repo": "/private/var/folders/4n/mt3km5rs1bn_b0w_d6hvlmhc0000gn/T/tmpfeuw_bsz/state/workspaces/session-160fca14-4e8b-4881-ae81-6e7b8ce73c67/control", + "worktree_path": "/private/var/folders/4n/mt3km5rs1bn_b0w_d6hvlmhc0000gn/T/tmpfeuw_bsz/state/workspaces/session-160fca14-4e8b-4881-ae81-6e7b8ce73c67/worktrees/codex-session--0", + "worktree_branch": "syncmill/codex/session--0", + "base_commit": "f460ea6d5cb46913efc0cc8a16b108aa93f36688", + "source_fingerprint": "c605ecdc0a3f4df5822c04269e4815c7d3395a26938e4dce93fe3fb0ef1a0d59", + "source_head": "fae6f00bb5ada4b8aa8f1f7b4b6756ac21618caf", + "checkpoints": [], + "revision": 0, + "created_at": "2026-11-10T01:17:59.874289+00:00", + "updated_at": "2026-11-10T01:17:59.874767+00:00" +} diff --git a/tests/fixtures/persisted/sessions/v0.2.0/open/sessions/23127f27-a1cc-4212-9e14-c672948975a5.json b/tests/fixtures/persisted/sessions/v0.2.0/open/sessions/23127f27-a1cc-4212-9e14-c672948975a5.json new file mode 100644 index 0000000..6df4665 --- /dev/null +++ b/tests/fixtures/persisted/sessions/v0.2.0/open/sessions/23127f27-a1cc-4212-9e14-c672948975a5.json @@ -0,0 +1,20 @@ +{ + "schema_version": 1, + "session_id": "23127f27-a1cc-4212-9e14-c672948975a5", + "project_id": "frozen-sessions-open", + "repo_root": "/private/var/folders/4n/mt3km5rs1bn_b0w_d6hvlmhc0000gn/T/tmpfeuw_bsz/repo", + "agent_id": "codex", + "state": "open", + "tmux_name": "syncmill-p-frozen-s-codex-23127f27", + "run_root": "/private/var/folders/4n/mt3km5rs1bn_b0w_d6hvlmhc0000gn/T/tmpfeuw_bsz/state/workspaces/session-23127f27-a1cc-4212-9e14-c672948975a5", + "control_repo": "/private/var/folders/4n/mt3km5rs1bn_b0w_d6hvlmhc0000gn/T/tmpfeuw_bsz/state/workspaces/session-23127f27-a1cc-4212-9e14-c672948975a5/control", + "worktree_path": "/private/var/folders/4n/mt3km5rs1bn_b0w_d6hvlmhc0000gn/T/tmpfeuw_bsz/state/workspaces/session-23127f27-a1cc-4212-9e14-c672948975a5/worktrees/codex-session--0", + "worktree_branch": "syncmill/codex/session--0", + "base_commit": "4221c528ed33a256c552a5e7b4d2a2094b173325", + "source_fingerprint": "c605ecdc0a3f4df5822c04269e4815c7d3395a26938e4dce93fe3fb0ef1a0d59", + "source_head": "fae6f00bb5ada4b8aa8f1f7b4b6756ac21618caf", + "checkpoints": [], + "revision": 1, + "created_at": "2026-11-10T01:17:57.405812+00:00", + "updated_at": "2026-11-10T01:17:57.682425+00:00" +} diff --git a/tests/fixtures/persisted/sessions/v0.2.0/project/marker.gitignore b/tests/fixtures/persisted/sessions/v0.2.0/project/marker.gitignore new file mode 100644 index 0000000..6dfa913 --- /dev/null +++ b/tests/fixtures/persisted/sessions/v0.2.0/project/marker.gitignore @@ -0,0 +1,2 @@ +memory/ +project.json diff --git a/tests/fixtures/persisted/sessions/v0.2.0/project/marker.json b/tests/fixtures/persisted/sessions/v0.2.0/project/marker.json new file mode 100644 index 0000000..9cc21ba --- /dev/null +++ b/tests/fixtures/persisted/sessions/v0.2.0/project/marker.json @@ -0,0 +1,3 @@ +{ + "id": "bba2632e-b782-4603-93e4-d39a67960f7c" +} diff --git a/tests/fixtures/persisted/sessions/v0.2.0/project/syncmill.toml b/tests/fixtures/persisted/sessions/v0.2.0/project/syncmill.toml new file mode 100644 index 0000000..243d757 --- /dev/null +++ b/tests/fixtures/persisted/sessions/v0.2.0/project/syncmill.toml @@ -0,0 +1,10 @@ +config_version = 2 +repo_root = "." +default_strategy = "route" + +[project] +id = "bba2632e-b782-4603-93e4-d39a67960f7c" +memory_dir = ".syncmill/memory" + +[gate] +commands = [] diff --git a/tests/fixtures/persisted/sessions/v0.2.0/recovery-required/sessions/fcc8f978-f4c2-46cd-936e-ecc4398de4ac.json b/tests/fixtures/persisted/sessions/v0.2.0/recovery-required/sessions/fcc8f978-f4c2-46cd-936e-ecc4398de4ac.json new file mode 100644 index 0000000..bdb4cdd --- /dev/null +++ b/tests/fixtures/persisted/sessions/v0.2.0/recovery-required/sessions/fcc8f978-f4c2-46cd-936e-ecc4398de4ac.json @@ -0,0 +1,20 @@ +{ + "schema_version": 1, + "session_id": "fcc8f978-f4c2-46cd-936e-ecc4398de4ac", + "project_id": "frozen-sessions-recovery-required", + "repo_root": "/private/var/folders/4n/mt3km5rs1bn_b0w_d6hvlmhc0000gn/T/tmpfeuw_bsz/repo", + "agent_id": "codex", + "state": "recovery_required", + "tmux_name": "syncmill-p-frozen-s-codex-fcc8f978", + "run_root": "/private/var/folders/4n/mt3km5rs1bn_b0w_d6hvlmhc0000gn/T/tmpfeuw_bsz/state/workspaces/session-fcc8f978-f4c2-46cd-936e-ecc4398de4ac", + "control_repo": "/private/var/folders/4n/mt3km5rs1bn_b0w_d6hvlmhc0000gn/T/tmpfeuw_bsz/state/workspaces/session-fcc8f978-f4c2-46cd-936e-ecc4398de4ac/control", + "worktree_path": "/private/var/folders/4n/mt3km5rs1bn_b0w_d6hvlmhc0000gn/T/tmpfeuw_bsz/state/workspaces/session-fcc8f978-f4c2-46cd-936e-ecc4398de4ac/worktrees/codex-session--0", + "worktree_branch": "syncmill/codex/session--0", + "base_commit": "c90194afe36347932ace323f220d38b93cd39c18", + "source_fingerprint": "c605ecdc0a3f4df5822c04269e4815c7d3395a26938e4dce93fe3fb0ef1a0d59", + "source_head": "fae6f00bb5ada4b8aa8f1f7b4b6756ac21618caf", + "checkpoints": [], + "revision": 3, + "created_at": "2026-11-10T01:18:00.902727+00:00", + "updated_at": "2026-11-10T01:18:01.150132+00:00" +} diff --git a/tests/fixtures/persisted/sessions/v0.3.0/bindings.json b/tests/fixtures/persisted/sessions/v0.3.0/bindings.json new file mode 100644 index 0000000..bbec56b --- /dev/null +++ b/tests/fixtures/persisted/sessions/v0.3.0/bindings.json @@ -0,0 +1,72 @@ +{ + "release": "0.3.0", + "box_root": "/private/var/folders/4n/mt3km5rs1bn_b0w_d6hvlmhc0000gn/T/tmp6z2ca2fi", + "tree": { + "calc.py": "def add(a, b):\n return a - b\n", + "syncmill.toml": "config_version = 2\nrepo_root = \".\"\ndefault_strategy = \"route\"\n\n[project]\nid = \"7d66bc69-1bf8-442c-8332-22fddc302ded\"\nmemory_dir = \".syncmill/memory\"\n\n[gate]\ncommands = []\n" + }, + "marker": { + "id": "7d66bc69-1bf8-442c-8332-22fddc302ded", + "mode": "0o600" + }, + "scenarios": { + "open": { + "project_id": "frozen-sessions-open", + "session_id": "dc058eaf-be75-4c09-bdad-c0c1fff90ce1", + "agent_id": "codex", + "state": "open", + "revision": 1, + "source_fingerprint": "53c966b60bfbf5b8ce599a63f3798c468fb862b463df5c15f488a176b1d04c0a", + "checkpoints": [] + }, + "checkpointed": { + "project_id": "frozen-sessions-checkpointed", + "session_id": "2be035d6-c596-4a41-89e9-899f58d83405", + "agent_id": "codex", + "state": "open", + "revision": 2, + "source_fingerprint": "53c966b60bfbf5b8ce599a63f3798c468fb862b463df5c15f488a176b1d04c0a", + "checkpoints": [ + "9981a2fe-6428-4fa0-bc06-cbde114bba0b" + ] + }, + "closed": { + "project_id": "frozen-sessions-closed", + "session_id": "5d175919-72c9-4c03-a994-e8a2e91650aa", + "agent_id": "codex", + "state": "closed", + "revision": 4, + "source_fingerprint": "53c966b60bfbf5b8ce599a63f3798c468fb862b463df5c15f488a176b1d04c0a", + "checkpoints": [ + "070b81f0-44cf-414e-9d06-1b98a5a9bf68" + ] + }, + "creating": { + "project_id": "frozen-sessions-creating", + "session_id": "3f0564ec-577d-4af7-8377-2c0588ded520", + "agent_id": "codex", + "state": "creating", + "revision": 0, + "source_fingerprint": "53c966b60bfbf5b8ce599a63f3798c468fb862b463df5c15f488a176b1d04c0a", + "checkpoints": [] + }, + "closing": { + "project_id": "frozen-sessions-closing", + "session_id": "0928a925-a3d4-4707-98f7-1d5b1214234c", + "agent_id": "codex", + "state": "closing", + "revision": 2, + "source_fingerprint": "53c966b60bfbf5b8ce599a63f3798c468fb862b463df5c15f488a176b1d04c0a", + "checkpoints": [] + }, + "recovery-required": { + "project_id": "frozen-sessions-recovery-required", + "session_id": "e4a9c0f5-5819-4afb-a208-a2d8e1cf7475", + "agent_id": "codex", + "state": "recovery_required", + "revision": 3, + "source_fingerprint": "53c966b60bfbf5b8ce599a63f3798c468fb862b463df5c15f488a176b1d04c0a", + "checkpoints": [] + } + } +} diff --git a/tests/fixtures/persisted/sessions/v0.3.0/checkpointed/runs/9981a2fe-6428-4fa0-bc06-cbde114bba0b/manifest.json b/tests/fixtures/persisted/sessions/v0.3.0/checkpointed/runs/9981a2fe-6428-4fa0-bc06-cbde114bba0b/manifest.json new file mode 100644 index 0000000..d003407 --- /dev/null +++ b/tests/fixtures/persisted/sessions/v0.3.0/checkpointed/runs/9981a2fe-6428-4fa0-bc06-cbde114bba0b/manifest.json @@ -0,0 +1,30 @@ +{ + "schema_version": 1, + "run_id": "9981a2fe-6428-4fa0-bc06-cbde114bba0b", + "project_id": "frozen-sessions-checkpointed", + "parent_run_id": null, + "created_at": "2026-11-10T01:18:04.484323+00:00", + "updated_at": "2026-11-10T01:18:04.485342+00:00", + "state": "pending", + "repo_root": "/private/var/folders/4n/mt3km5rs1bn_b0w_d6hvlmhc0000gn/T/tmp6z2ca2fi/repo", + "source_fingerprint": "53c966b60bfbf5b8ce599a63f3798c468fb862b463df5c15f488a176b1d04c0a", + "snapshot_commit": "3f1a86cac8bcf53bfcc38fccd122a6db96200505", + "task_id": "71c6bb26-9ff1-43c5-adbb-11627c91a95b", + "prompt": "Interactive session checkpoint 2be035d6-c596-4a41-89e9-899f58d83405", + "strategy": "interactive", + "agents": [ + "codex" + ], + "plan": { + "origin": "isolated-session", + "session_id": "2be035d6-c596-4a41-89e9-899f58d83405" + }, + "agent_id": "codex", + "files_changed": [ + "calc.py" + ], + "patch_sha256": "35dc711418e60edc7974186bd853c7ab08db843f16e43066417b07a53d8566ba", + "patch_bytes": 157, + "provider_session_id": null, + "conversation_id": null +} diff --git a/tests/fixtures/persisted/sessions/v0.3.0/checkpointed/runs/9981a2fe-6428-4fa0-bc06-cbde114bba0b/result.json b/tests/fixtures/persisted/sessions/v0.3.0/checkpointed/runs/9981a2fe-6428-4fa0-bc06-cbde114bba0b/result.json new file mode 100644 index 0000000..39ad6f4 --- /dev/null +++ b/tests/fixtures/persisted/sessions/v0.3.0/checkpointed/runs/9981a2fe-6428-4fa0-bc06-cbde114bba0b/result.json @@ -0,0 +1,7 @@ +{ + "status": "completed", + "output": "Checkpoint from isolated session 2be035d6-c596-4a41-89e9-899f58d83405", + "error": null, + "exit_code": 0, + "duration_seconds": 0.0 +} diff --git a/tests/fixtures/persisted/sessions/v0.3.0/checkpointed/runs/9981a2fe-6428-4fa0-bc06-cbde114bba0b/winner.patch b/tests/fixtures/persisted/sessions/v0.3.0/checkpointed/runs/9981a2fe-6428-4fa0-bc06-cbde114bba0b/winner.patch new file mode 100644 index 0000000..95e886e --- /dev/null +++ b/tests/fixtures/persisted/sessions/v0.3.0/checkpointed/runs/9981a2fe-6428-4fa0-bc06-cbde114bba0b/winner.patch @@ -0,0 +1,8 @@ +diff --git a/calc.py b/calc.py +index 12ee743..4693ad3 100644 +--- a/calc.py ++++ b/calc.py +@@ -1,2 +1,2 @@ + def add(a, b): +- return a - b ++ return a + b diff --git a/tests/fixtures/persisted/sessions/v0.3.0/checkpointed/sessions/2be035d6-c596-4a41-89e9-899f58d83405.json b/tests/fixtures/persisted/sessions/v0.3.0/checkpointed/sessions/2be035d6-c596-4a41-89e9-899f58d83405.json new file mode 100644 index 0000000..20c85c1 --- /dev/null +++ b/tests/fixtures/persisted/sessions/v0.3.0/checkpointed/sessions/2be035d6-c596-4a41-89e9-899f58d83405.json @@ -0,0 +1,22 @@ +{ + "schema_version": 1, + "session_id": "2be035d6-c596-4a41-89e9-899f58d83405", + "project_id": "frozen-sessions-checkpointed", + "repo_root": "/private/var/folders/4n/mt3km5rs1bn_b0w_d6hvlmhc0000gn/T/tmp6z2ca2fi/repo", + "agent_id": "codex", + "state": "open", + "tmux_name": "syncmill-p-frozen-s-codex-2be035d6", + "run_root": "/private/var/folders/4n/mt3km5rs1bn_b0w_d6hvlmhc0000gn/T/tmp6z2ca2fi/state/workspaces/session-2be035d6-c596-4a41-89e9-899f58d83405", + "control_repo": "/private/var/folders/4n/mt3km5rs1bn_b0w_d6hvlmhc0000gn/T/tmp6z2ca2fi/state/workspaces/session-2be035d6-c596-4a41-89e9-899f58d83405/control", + "worktree_path": "/private/var/folders/4n/mt3km5rs1bn_b0w_d6hvlmhc0000gn/T/tmp6z2ca2fi/state/workspaces/session-2be035d6-c596-4a41-89e9-899f58d83405/worktrees/codex-session--0", + "worktree_branch": "syncmill/codex/session--0", + "base_commit": "3f1a86cac8bcf53bfcc38fccd122a6db96200505", + "source_fingerprint": "53c966b60bfbf5b8ce599a63f3798c468fb862b463df5c15f488a176b1d04c0a", + "source_head": "14038757ec6e6a6a192990e4a9d05ccdd002e46e", + "checkpoints": [ + "9981a2fe-6428-4fa0-bc06-cbde114bba0b" + ], + "revision": 2, + "created_at": "2026-11-10T01:18:03.872463+00:00", + "updated_at": "2026-11-10T01:18:04.486533+00:00" +} diff --git a/tests/fixtures/persisted/sessions/v0.3.0/closed/runs/070b81f0-44cf-414e-9d06-1b98a5a9bf68/manifest.json b/tests/fixtures/persisted/sessions/v0.3.0/closed/runs/070b81f0-44cf-414e-9d06-1b98a5a9bf68/manifest.json new file mode 100644 index 0000000..a2a1f97 --- /dev/null +++ b/tests/fixtures/persisted/sessions/v0.3.0/closed/runs/070b81f0-44cf-414e-9d06-1b98a5a9bf68/manifest.json @@ -0,0 +1,30 @@ +{ + "schema_version": 1, + "run_id": "070b81f0-44cf-414e-9d06-1b98a5a9bf68", + "project_id": "frozen-sessions-closed", + "parent_run_id": null, + "created_at": "2026-11-10T01:18:05.917929+00:00", + "updated_at": "2026-11-10T01:18:05.919023+00:00", + "state": "pending", + "repo_root": "/private/var/folders/4n/mt3km5rs1bn_b0w_d6hvlmhc0000gn/T/tmp6z2ca2fi/repo", + "source_fingerprint": "53c966b60bfbf5b8ce599a63f3798c468fb862b463df5c15f488a176b1d04c0a", + "snapshot_commit": "e59ad52086cf1bd7f2d3f7ebf03e78dde9b78337", + "task_id": "61a6168b-1845-4db7-94c0-a83b4234cf74", + "prompt": "Interactive session checkpoint 5d175919-72c9-4c03-a994-e8a2e91650aa", + "strategy": "interactive", + "agents": [ + "codex" + ], + "plan": { + "origin": "isolated-session", + "session_id": "5d175919-72c9-4c03-a994-e8a2e91650aa" + }, + "agent_id": "codex", + "files_changed": [ + "calc.py" + ], + "patch_sha256": "35dc711418e60edc7974186bd853c7ab08db843f16e43066417b07a53d8566ba", + "patch_bytes": 157, + "provider_session_id": null, + "conversation_id": null +} diff --git a/tests/fixtures/persisted/sessions/v0.3.0/closed/runs/070b81f0-44cf-414e-9d06-1b98a5a9bf68/result.json b/tests/fixtures/persisted/sessions/v0.3.0/closed/runs/070b81f0-44cf-414e-9d06-1b98a5a9bf68/result.json new file mode 100644 index 0000000..b6ce085 --- /dev/null +++ b/tests/fixtures/persisted/sessions/v0.3.0/closed/runs/070b81f0-44cf-414e-9d06-1b98a5a9bf68/result.json @@ -0,0 +1,7 @@ +{ + "status": "completed", + "output": "Checkpoint from isolated session 5d175919-72c9-4c03-a994-e8a2e91650aa", + "error": null, + "exit_code": 0, + "duration_seconds": 0.0 +} diff --git a/tests/fixtures/persisted/sessions/v0.3.0/closed/runs/070b81f0-44cf-414e-9d06-1b98a5a9bf68/winner.patch b/tests/fixtures/persisted/sessions/v0.3.0/closed/runs/070b81f0-44cf-414e-9d06-1b98a5a9bf68/winner.patch new file mode 100644 index 0000000..95e886e --- /dev/null +++ b/tests/fixtures/persisted/sessions/v0.3.0/closed/runs/070b81f0-44cf-414e-9d06-1b98a5a9bf68/winner.patch @@ -0,0 +1,8 @@ +diff --git a/calc.py b/calc.py +index 12ee743..4693ad3 100644 +--- a/calc.py ++++ b/calc.py +@@ -1,2 +1,2 @@ + def add(a, b): +- return a - b ++ return a + b diff --git a/tests/fixtures/persisted/sessions/v0.3.0/closed/sessions/5d175919-72c9-4c03-a994-e8a2e91650aa.json b/tests/fixtures/persisted/sessions/v0.3.0/closed/sessions/5d175919-72c9-4c03-a994-e8a2e91650aa.json new file mode 100644 index 0000000..6a0d713 --- /dev/null +++ b/tests/fixtures/persisted/sessions/v0.3.0/closed/sessions/5d175919-72c9-4c03-a994-e8a2e91650aa.json @@ -0,0 +1,22 @@ +{ + "schema_version": 1, + "session_id": "5d175919-72c9-4c03-a994-e8a2e91650aa", + "project_id": "frozen-sessions-closed", + "repo_root": "/private/var/folders/4n/mt3km5rs1bn_b0w_d6hvlmhc0000gn/T/tmp6z2ca2fi/repo", + "agent_id": "codex", + "state": "closed", + "tmux_name": "syncmill-p-frozen-s-codex-5d175919", + "run_root": "/private/var/folders/4n/mt3km5rs1bn_b0w_d6hvlmhc0000gn/T/tmp6z2ca2fi/state/workspaces/session-5d175919-72c9-4c03-a994-e8a2e91650aa", + "control_repo": "/private/var/folders/4n/mt3km5rs1bn_b0w_d6hvlmhc0000gn/T/tmp6z2ca2fi/state/workspaces/session-5d175919-72c9-4c03-a994-e8a2e91650aa/control", + "worktree_path": "/private/var/folders/4n/mt3km5rs1bn_b0w_d6hvlmhc0000gn/T/tmp6z2ca2fi/state/workspaces/session-5d175919-72c9-4c03-a994-e8a2e91650aa/worktrees/codex-session--0", + "worktree_branch": "syncmill/codex/session--0", + "base_commit": "e59ad52086cf1bd7f2d3f7ebf03e78dde9b78337", + "source_fingerprint": "53c966b60bfbf5b8ce599a63f3798c468fb862b463df5c15f488a176b1d04c0a", + "source_head": "14038757ec6e6a6a192990e4a9d05ccdd002e46e", + "checkpoints": [ + "070b81f0-44cf-414e-9d06-1b98a5a9bf68" + ], + "revision": 4, + "created_at": "2026-11-10T01:18:05.279316+00:00", + "updated_at": "2026-11-10T01:18:06.525657+00:00" +} diff --git a/tests/fixtures/persisted/sessions/v0.3.0/closing/sessions/0928a925-a3d4-4707-98f7-1d5b1214234c.json b/tests/fixtures/persisted/sessions/v0.3.0/closing/sessions/0928a925-a3d4-4707-98f7-1d5b1214234c.json new file mode 100644 index 0000000..ab22631 --- /dev/null +++ b/tests/fixtures/persisted/sessions/v0.3.0/closing/sessions/0928a925-a3d4-4707-98f7-1d5b1214234c.json @@ -0,0 +1,20 @@ +{ + "schema_version": 1, + "session_id": "0928a925-a3d4-4707-98f7-1d5b1214234c", + "project_id": "frozen-sessions-closing", + "repo_root": "/private/var/folders/4n/mt3km5rs1bn_b0w_d6hvlmhc0000gn/T/tmp6z2ca2fi/repo", + "agent_id": "codex", + "state": "closing", + "tmux_name": "syncmill-p-frozen-s-codex-0928a925", + "run_root": "/private/var/folders/4n/mt3km5rs1bn_b0w_d6hvlmhc0000gn/T/tmp6z2ca2fi/state/workspaces/session-0928a925-a3d4-4707-98f7-1d5b1214234c", + "control_repo": "/private/var/folders/4n/mt3km5rs1bn_b0w_d6hvlmhc0000gn/T/tmp6z2ca2fi/state/workspaces/session-0928a925-a3d4-4707-98f7-1d5b1214234c/control", + "worktree_path": "/private/var/folders/4n/mt3km5rs1bn_b0w_d6hvlmhc0000gn/T/tmp6z2ca2fi/state/workspaces/session-0928a925-a3d4-4707-98f7-1d5b1214234c/worktrees/codex-session--0", + "worktree_branch": "syncmill/codex/session--0", + "base_commit": "ce63d788c4a2ecd224b129604b3a10ec96c48145", + "source_fingerprint": "53c966b60bfbf5b8ce599a63f3798c468fb862b463df5c15f488a176b1d04c0a", + "source_head": "14038757ec6e6a6a192990e4a9d05ccdd002e46e", + "checkpoints": [], + "revision": 2, + "created_at": "2026-11-10T01:18:08.119617+00:00", + "updated_at": "2026-11-10T01:18:08.703417+00:00" +} diff --git a/tests/fixtures/persisted/sessions/v0.3.0/creating/sessions/3f0564ec-577d-4af7-8377-2c0588ded520.json b/tests/fixtures/persisted/sessions/v0.3.0/creating/sessions/3f0564ec-577d-4af7-8377-2c0588ded520.json new file mode 100644 index 0000000..0ab6c60 --- /dev/null +++ b/tests/fixtures/persisted/sessions/v0.3.0/creating/sessions/3f0564ec-577d-4af7-8377-2c0588ded520.json @@ -0,0 +1,20 @@ +{ + "schema_version": 1, + "session_id": "3f0564ec-577d-4af7-8377-2c0588ded520", + "project_id": "frozen-sessions-creating", + "repo_root": "/private/var/folders/4n/mt3km5rs1bn_b0w_d6hvlmhc0000gn/T/tmp6z2ca2fi/repo", + "agent_id": "codex", + "state": "creating", + "tmux_name": "syncmill-p-frozen-s-codex-3f0564ec", + "run_root": "/private/var/folders/4n/mt3km5rs1bn_b0w_d6hvlmhc0000gn/T/tmp6z2ca2fi/state/workspaces/session-3f0564ec-577d-4af7-8377-2c0588ded520", + "control_repo": "/private/var/folders/4n/mt3km5rs1bn_b0w_d6hvlmhc0000gn/T/tmp6z2ca2fi/state/workspaces/session-3f0564ec-577d-4af7-8377-2c0588ded520/control", + "worktree_path": "/private/var/folders/4n/mt3km5rs1bn_b0w_d6hvlmhc0000gn/T/tmp6z2ca2fi/state/workspaces/session-3f0564ec-577d-4af7-8377-2c0588ded520/worktrees/codex-session--0", + "worktree_branch": "syncmill/codex/session--0", + "base_commit": "407a3bff9baa00b3a181e9c8912049e01a8938cf", + "source_fingerprint": "53c966b60bfbf5b8ce599a63f3798c468fb862b463df5c15f488a176b1d04c0a", + "source_head": "14038757ec6e6a6a192990e4a9d05ccdd002e46e", + "checkpoints": [], + "revision": 0, + "created_at": "2026-11-10T01:18:07.336656+00:00", + "updated_at": "2026-11-10T01:18:07.337162+00:00" +} diff --git a/tests/fixtures/persisted/sessions/v0.3.0/open/sessions/dc058eaf-be75-4c09-bdad-c0c1fff90ce1.json b/tests/fixtures/persisted/sessions/v0.3.0/open/sessions/dc058eaf-be75-4c09-bdad-c0c1fff90ce1.json new file mode 100644 index 0000000..0b08c7f --- /dev/null +++ b/tests/fixtures/persisted/sessions/v0.3.0/open/sessions/dc058eaf-be75-4c09-bdad-c0c1fff90ce1.json @@ -0,0 +1,20 @@ +{ + "schema_version": 1, + "session_id": "dc058eaf-be75-4c09-bdad-c0c1fff90ce1", + "project_id": "frozen-sessions-open", + "repo_root": "/private/var/folders/4n/mt3km5rs1bn_b0w_d6hvlmhc0000gn/T/tmp6z2ca2fi/repo", + "agent_id": "codex", + "state": "open", + "tmux_name": "syncmill-p-frozen-s-codex-dc058eaf", + "run_root": "/private/var/folders/4n/mt3km5rs1bn_b0w_d6hvlmhc0000gn/T/tmp6z2ca2fi/state/workspaces/session-dc058eaf-be75-4c09-bdad-c0c1fff90ce1", + "control_repo": "/private/var/folders/4n/mt3km5rs1bn_b0w_d6hvlmhc0000gn/T/tmp6z2ca2fi/state/workspaces/session-dc058eaf-be75-4c09-bdad-c0c1fff90ce1/control", + "worktree_path": "/private/var/folders/4n/mt3km5rs1bn_b0w_d6hvlmhc0000gn/T/tmp6z2ca2fi/state/workspaces/session-dc058eaf-be75-4c09-bdad-c0c1fff90ce1/worktrees/codex-session--0", + "worktree_branch": "syncmill/codex/session--0", + "base_commit": "2e225657bea9677372e64df712e82422f2152e53", + "source_fingerprint": "53c966b60bfbf5b8ce599a63f3798c468fb862b463df5c15f488a176b1d04c0a", + "source_head": "14038757ec6e6a6a192990e4a9d05ccdd002e46e", + "checkpoints": [], + "revision": 1, + "created_at": "2026-11-10T01:18:02.692161+00:00", + "updated_at": "2026-11-10T01:18:02.978852+00:00" +} diff --git a/tests/fixtures/persisted/sessions/v0.3.0/project/marker.gitignore b/tests/fixtures/persisted/sessions/v0.3.0/project/marker.gitignore new file mode 100644 index 0000000..6dfa913 --- /dev/null +++ b/tests/fixtures/persisted/sessions/v0.3.0/project/marker.gitignore @@ -0,0 +1,2 @@ +memory/ +project.json diff --git a/tests/fixtures/persisted/sessions/v0.3.0/project/marker.json b/tests/fixtures/persisted/sessions/v0.3.0/project/marker.json new file mode 100644 index 0000000..4de69f1 --- /dev/null +++ b/tests/fixtures/persisted/sessions/v0.3.0/project/marker.json @@ -0,0 +1,3 @@ +{ + "id": "7d66bc69-1bf8-442c-8332-22fddc302ded" +} diff --git a/tests/fixtures/persisted/sessions/v0.3.0/project/syncmill.toml b/tests/fixtures/persisted/sessions/v0.3.0/project/syncmill.toml new file mode 100644 index 0000000..92b30bd --- /dev/null +++ b/tests/fixtures/persisted/sessions/v0.3.0/project/syncmill.toml @@ -0,0 +1,10 @@ +config_version = 2 +repo_root = "." +default_strategy = "route" + +[project] +id = "7d66bc69-1bf8-442c-8332-22fddc302ded" +memory_dir = ".syncmill/memory" + +[gate] +commands = [] diff --git a/tests/fixtures/persisted/sessions/v0.3.0/recovery-required/sessions/e4a9c0f5-5819-4afb-a208-a2d8e1cf7475.json b/tests/fixtures/persisted/sessions/v0.3.0/recovery-required/sessions/e4a9c0f5-5819-4afb-a208-a2d8e1cf7475.json new file mode 100644 index 0000000..f930351 --- /dev/null +++ b/tests/fixtures/persisted/sessions/v0.3.0/recovery-required/sessions/e4a9c0f5-5819-4afb-a208-a2d8e1cf7475.json @@ -0,0 +1,20 @@ +{ + "schema_version": 1, + "session_id": "e4a9c0f5-5819-4afb-a208-a2d8e1cf7475", + "project_id": "frozen-sessions-recovery-required", + "repo_root": "/private/var/folders/4n/mt3km5rs1bn_b0w_d6hvlmhc0000gn/T/tmp6z2ca2fi/repo", + "agent_id": "codex", + "state": "recovery_required", + "tmux_name": "syncmill-p-frozen-s-codex-e4a9c0f5", + "run_root": "/private/var/folders/4n/mt3km5rs1bn_b0w_d6hvlmhc0000gn/T/tmp6z2ca2fi/state/workspaces/session-e4a9c0f5-5819-4afb-a208-a2d8e1cf7475", + "control_repo": "/private/var/folders/4n/mt3km5rs1bn_b0w_d6hvlmhc0000gn/T/tmp6z2ca2fi/state/workspaces/session-e4a9c0f5-5819-4afb-a208-a2d8e1cf7475/control", + "worktree_path": "/private/var/folders/4n/mt3km5rs1bn_b0w_d6hvlmhc0000gn/T/tmp6z2ca2fi/state/workspaces/session-e4a9c0f5-5819-4afb-a208-a2d8e1cf7475/worktrees/codex-session--0", + "worktree_branch": "syncmill/codex/session--0", + "base_commit": "86ca80d99c70edd6ddc07ee769f6c2b4d8a72cbf", + "source_fingerprint": "53c966b60bfbf5b8ce599a63f3798c468fb862b463df5c15f488a176b1d04c0a", + "source_head": "14038757ec6e6a6a192990e4a9d05ccdd002e46e", + "checkpoints": [], + "revision": 3, + "created_at": "2026-11-10T01:18:09.472679+00:00", + "updated_at": "2026-11-10T01:18:10.071579+00:00" +} diff --git a/tests/test_frozen_sessions.py b/tests/test_frozen_sessions.py new file mode 100644 index 0000000..2ae1c05 --- /dev/null +++ b/tests/test_frozen_sessions.py @@ -0,0 +1,570 @@ +"""Released isolated sessions and project markers, read by the current readers +(#271, persisted contracts D). + +The documents under ``fixtures/persisted/sessions//`` were written +by that release's own ``syncmill init`` and ``syncmill session`` commands +(see ``freeze.py`` there) and are never regenerated by the current writer. +``project/`` holds what ``init`` wrote: ``syncmill.toml`` and the marker +directory's ``project.json`` and ``.gitignore``, stored as ``marker.json`` and +``marker.gitignore``. Every other scenario is one project: its session +document and any checkpoint run artifact. Workspaces were not frozen. + +Only machine-bound values are rebound, each in a named place: a session +document's four paths (repository, workspace run root, control clone, +worktree) and its source fingerprint (recomputed in the stored value's own +version), and a checkpoint artifact's repository root and fingerprint. The +tmux name is derived from the project, agent and session id, so it is never +rebound. ``Frozen`` asserts every other byte is unchanged. + +tmux is never launched: the store's probe and command seams and the CLI's +gates are patched. +""" + +from __future__ import annotations + +import json +import logging +import os +import subprocess +from pathlib import Path +from types import SimpleNamespace + +import pytest +from click.testing import CliRunner + +from syncmill.cli import cli +from syncmill.worktree import fingerprint_version, repository_fingerprint + +FROZEN = Path(__file__).parent / "fixtures" / "persisted" / "sessions" +RELEASES = sorted(p.parent.name for p in FROZEN.glob("v*/bindings.json")) + +#: scenario -> (state, revision) the release left its session in. +SCENARIOS = { + "open": ("open", 1), + "checkpointed": ("open", 2), + "closed": ("closed", 4), + "creating": ("creating", 0), + "closing": ("closing", 2), + "recovery-required": ("recovery_required", 3), +} +#: The four paths a session document binds to the generating machine. +PATH_FIELDS = ("repo_root", "run_root", "control_repo", "worktree_path") +FIXED = "def add(a, b):\n return a + b\n" + + +def _bindings(release: str) -> dict: + return json.loads((FROZEN / release / "bindings.json").read_text()) + + +@pytest.fixture(params=RELEASES) +def release(request) -> str: + return request.param + + +def _repo(root: Path, tree: dict) -> Path: + """The generating repository's shape: the tree and ``syncmill.toml`` + committed, ``.syncmill/`` ignored.""" + repo = root / "repo" + repo.mkdir() + for name, text in tree.items(): + (repo / name).write_text(text, encoding="utf-8") + for argv in ( + ["init", "-q"], + ["add", "-A"], + ["-c", "user.name=t", "-c", "user.email=t@t.test", "-c", + "commit.gpgsign=false", "commit", "-q", "-m", "frozen tree"], + ): + subprocess.run(["git", *argv], cwd=repo, check=True, capture_output=True) + (repo / ".git" / "info" / "exclude").write_text(".syncmill/\n", encoding="utf-8") + return repo + + +class Frozen: + """One release session scenario installed as a project bound to ``repo``.""" + + def __init__(self, release: str, scenario: str, tmp_path: Path, monkeypatch): + self.release = release + self.scenario = scenario + self.bindings = _bindings(release) + self.facts = self.bindings["scenarios"][scenario] + self.dir = FROZEN / release / scenario + self.root = tmp_path.resolve() + self.repo = _repo(self.root, self.bindings["tree"]) + self.session_id = self.facts["session_id"] + self.runs_root = self.root / "state" / "runs" + self.workspace_root = self.root / "state" / "workspaces" + self.project_dir = self.runs_root / self.facts["project_id"] + monkeypatch.setenv("SYNCMILL_RUNS__ROOT", str(self.runs_root)) + monkeypatch.setenv("SYNCMILL_WORKSPACE__ROOT", str(self.workspace_root)) + monkeypatch.setenv("SYNCMILL_PROJECT__ID", self.facts["project_id"]) + stored = self.facts["source_fingerprint"] + self.fingerprint = repository_fingerprint(self.repo, like=stored) + # Recomputed in the stored value's own version, never upgraded. + assert fingerprint_version(self.fingerprint) == fingerprint_version(stored) == 1 + self._install() + + def _rebound(self, data: bytes, *, paths: int) -> bytes: + stored = self.facts["source_fingerprint"].encode() + box = self.bindings["box_root"].encode() + assert data.count(stored) == 1 + assert data.count(box) == paths + return data.replace(stored, self.fingerprint.encode()).replace( + box, str(self.root).encode() + ) + + def _install(self) -> None: + sessions = self.project_dir / "sessions" + sessions.mkdir(parents=True) + [document] = (self.dir / "sessions").glob("*.json") + assert document.stem == self.session_id + installed = sessions / document.name + installed.write_bytes(self._rebound(document.read_bytes(), paths=len(PATH_FIELDS))) + # Only the bound fields moved. + released, now = json.loads(document.read_text()), json.loads(installed.read_text()) + bound = {*PATH_FIELDS, "source_fingerprint"} + assert {k: v for k, v in now.items() if k not in bound} == { + k: v for k, v in released.items() if k not in bound + } + if (self.dir / "runs").is_dir(): + import shutil + + shutil.copytree(self.dir / "runs", self.project_dir / "runs") + for manifest in self.project_dir.glob("runs/*/manifest.json"): + manifest.write_bytes(self._rebound(manifest.read_bytes(), paths=1)) + + def document(self) -> dict: + return json.loads( + (self.project_dir / "sessions" / f"{self.session_id}.json").read_text() + ) + + def released(self) -> dict: + [document] = (self.dir / "sessions").glob("*.json") + return json.loads(document.read_text()) + + def store(self): + from syncmill.isolated_session import IsolatedSessionStore + from syncmill.project import load_project_config + + _, config = load_project_config(self.repo) + return IsolatedSessionStore(config, self.repo) + + def cli(self, *argv: str): + return CliRunner().invoke(cli, [*argv, "--repo", str(self.repo)]) + + +@pytest.fixture +def no_tmux(monkeypatch): + """No real tmux anywhere: the store's probe and commands, the CLI's gates + and attachment. ``live`` says what the probe answers.""" + state = SimpleNamespace(live=False, attached=[], commands=[]) + + def tmux(*args, **kwargs): + state.commands.append(args) + return subprocess.CompletedProcess(args, 0, stdout="", stderr="") + + monkeypatch.setattr("syncmill.isolated_session.session_exists", lambda name: state.live) + monkeypatch.setattr("syncmill.isolated_session._tmux", tmux) + monkeypatch.setattr( + "syncmill.isolated_session._attach_and_report", + lambda name, agent: state.attached.append((name, agent)) or 0, + ) + monkeypatch.setattr("syncmill.cli.tmux_available", lambda: True) + monkeypatch.setattr("syncmill.cli._stdout_isatty", lambda: True) + return state + + +# ── inventory ───────────────────────────────────────────────────────────── + + +def test_every_release_freezes_every_scenario(): + assert RELEASES == ["v0.1.2", "v0.2.0", "v0.3.0"] + for release in RELEASES: + bindings = _bindings(release) + assert bindings["release"] == release.removeprefix("v") + assert sorted(bindings["scenarios"]) == sorted(SCENARIOS) + assert bindings["marker"]["mode"] == "0o600" + for name, facts in bindings["scenarios"].items(): + assert (facts["state"], facts["revision"]) == SCENARIOS[name], (release, name) + assert fingerprint_version(facts["source_fingerprint"]) == 1 + document = json.loads(next((FROZEN / release / name / "sessions").glob("*.json")) + .read_text()) + assert document["schema_version"] == 1 + assert (document["state"], document["revision"]) == SCENARIOS[name] + assert document["checkpoints"] == facts["checkpoints"] + runs = sorted(p.name for p in (FROZEN / release / name / "runs").glob("*")) \ + if (FROZEN / release / name / "runs").is_dir() else [] + assert runs == sorted(facts["checkpoints"]) + project = FROZEN / release / "project" + assert sorted(p.name for p in project.iterdir()) == [ + "marker.gitignore", "marker.json", "syncmill.toml" + ] + + +@pytest.mark.parametrize("scenario", list(SCENARIOS)) +def test_the_tmux_name_is_derived_not_bound(release, scenario): + from syncmill.isolated_session import session_tmux_name + + facts = _bindings(release)["scenarios"][scenario] + document = json.loads(next((FROZEN / release / scenario / "sessions").glob("*.json")) + .read_text()) + assert document["tmux_name"] == session_tmux_name( + facts["project_id"], facts["agent_id"], facts["session_id"] + ) + + +# ── listing and loading every released session ──────────────────────────── + + +@pytest.mark.parametrize("scenario", list(SCENARIOS)) +def test_isolated_list_reads_a_released_session(release, scenario, tmp_path, monkeypatch, no_tmux): + frozen = Frozen(release, scenario, tmp_path, monkeypatch) + state, _ = SCENARIOS[scenario] + listed = frozen.cli("session", "isolated-list") + assert listed.exit_code == 0, listed.output + [line] = listed.output.splitlines() + assert line.split() == [frozen.session_id, "codex", state, "stopped"] + + # Live is the probe's answer; staleness is the stored fingerprint + # recomputed in its own version, and only an open session is judged. + no_tmux.live = True + (frozen.repo / "calc.py").write_text(FIXED, encoding="utf-8") + [value] = frozen.store().list() + assert value["live"] is True + assert value["source_stale"] is (state == "open") + for field in PATH_FIELDS: + assert value[field].startswith(str(frozen.root) + "/") + + +@pytest.mark.parametrize("scenario", list(SCENARIOS)) +def test_render_session_shows_a_released_session(release, scenario, tmp_path, monkeypatch, no_tmux): + from syncmill.tui_collaboration import render_session + + frozen = Frozen(release, scenario, tmp_path, monkeypatch) + [value] = frozen.store().list() + text = render_session(value).splitlines() + assert text[0] == f"Session: {frozen.session_id}" + assert text[2] == f"State: {SCENARIOS[scenario][0]} | live: False" + assert text[3] == "Source: clean snapshot" + assert text[4] == f"Worktree: {frozen.document()['worktree_path']}" + assert text[5] == f"Checkpoints: {', '.join(frozen.facts['checkpoints']) or '-'}" + + +def _edit(frozen: Frozen, **fields) -> None: + path = frozen.project_dir / "sessions" / f"{frozen.session_id}.json" + value = json.loads(path.read_text()) + value.update(fields) + path.write_text(json.dumps(value, indent=2), encoding="utf-8") + + +@pytest.mark.parametrize("unsafe", ["workspace-root", "tmux-name", "worktree", "repository"]) +def test_a_released_session_with_unsafe_paths_is_refused( + release, unsafe, tmp_path, monkeypatch, no_tmux +): + frozen = Frozen(release, "open", tmp_path, monkeypatch) + # Cleanup and attachment are bound to paths and a name derived from the + # session id; a document that disagrees is never acted on. + if unsafe == "workspace-root": + monkeypatch.setenv("SYNCMILL_WORKSPACE__ROOT", str(frozen.root / "elsewhere")) + elif unsafe == "tmux-name": + _edit(frozen, tmux_name="syncmill-p-someone-else") + elif unsafe == "worktree": + _edit(frozen, worktree_path=str(frozen.root / "elsewhere")) + else: + (frozen.root / "other").mkdir() + other = _repo(frozen.root / "other", frozen.bindings["tree"]) + _edit(frozen, repo_root=str(other)) + before = frozen.document() + assert frozen.store().list() == [] + result = frozen.cli("session", "checkpoint", frozen.session_id) + error = ( + "session belongs to a different repository" if unsafe == "repository" + else "session metadata contains unsafe paths" + ) + assert (result.exit_code, result.output.splitlines()[-1]) == (1, f"Error: {error}") + assert frozen.document() == before + + +# ── the session commands ────────────────────────────────────────────────── + + +def test_attach_a_released_open_session(release, tmp_path, monkeypatch, no_tmux): + frozen = Frozen(release, "open", tmp_path, monkeypatch) + refused = frozen.cli("session", "attach-isolated", frozen.session_id) + assert (refused.exit_code, refused.output.splitlines()[-1]) == ( + 1, "Error: session process is no longer running" + ) + assert no_tmux.attached == [] + + # Running: its one pane must sit in the rebound worktree. + no_tmux.live = True + worktree = frozen.document()["worktree_path"] + Path(worktree).mkdir(parents=True) + + def panes(*args, **kwargs): + return subprocess.CompletedProcess(args, 0, stdout=worktree + "\n", stderr="") + + monkeypatch.setattr("syncmill.isolated_session._tmux", panes) + attached = frozen.cli("session", "attach-isolated", frozen.session_id) + assert attached.exit_code == 0, attached.output + assert no_tmux.attached == [(frozen.document()["tmux_name"], "codex")] + + # A pane anywhere else is never attached to. + monkeypatch.setattr("syncmill.isolated_session._PANE_CWD_SETTLE_SECONDS", 0) + + def elsewhere(*args, **kwargs): + return subprocess.CompletedProcess(args, 0, stdout=str(frozen.root) + "\n", stderr="") + + monkeypatch.setattr("syncmill.isolated_session._tmux", elsewhere) + refused = frozen.cli("session", "attach-isolated", frozen.session_id) + assert refused.exit_code == 1, refused.output + assert refused.output.splitlines()[-1] == ( + "Error: session cwd does not match its isolated workspace" + ) + assert len(no_tmux.attached) == 1 + + +@pytest.mark.parametrize("scenario", ["closed", "creating", "closing", "recovery-required"]) +def test_attach_refuses_a_released_session_that_is_not_open( + release, scenario, tmp_path, monkeypatch, no_tmux +): + frozen = Frozen(release, scenario, tmp_path, monkeypatch) + refused = frozen.cli("session", "attach-isolated", frozen.session_id) + assert (refused.exit_code, refused.output.splitlines()[-1]) == ( + 1, f"Error: session is {SCENARIOS[scenario][0]}, not open" + ) + + +@pytest.mark.parametrize("scenario", ["open", "checkpointed"]) +def test_checkpoint_refuses_a_released_session_without_its_workspace( + release, scenario, tmp_path, monkeypatch, no_tmux +): + frozen = Frozen(release, scenario, tmp_path, monkeypatch) + before = frozen.document() + result = frozen.cli("session", "checkpoint", frozen.session_id) + assert (result.exit_code, result.output.splitlines()[-1]) == ( + 1, "Error: isolated worktree is missing" + ) + assert frozen.document() == before + + +#: scenario -> the revision HEAD's close leaves, the workspace being absent. +CLOSED_AT = { + "open": 3, "checkpointed": 4, "closed": 4, "creating": 1, "closing": 3, + "recovery-required": 4, +} + + +@pytest.mark.parametrize("scenario", list(SCENARIOS)) +def test_close_a_released_session(release, scenario, tmp_path, monkeypatch, no_tmux): + frozen = Frozen(release, scenario, tmp_path, monkeypatch) + before = frozen.document() + result = frozen.cli("session", "close", frozen.session_id) + assert result.exit_code == 0, result.output + assert result.stdout.splitlines() == [f"Closed isolated session {frozen.session_id}"] + after = frozen.document() + assert (after["state"], after["revision"]) == ("closed", CLOSED_AT[scenario]) + if scenario == "closed": + assert after == before + else: + # The lifecycle fields move; the checkpoints and bindings never do. + moving = {"state", "revision", "updated_at"} + assert {k: v for k, v in after.items() if k not in moving} == { + k: v for k, v in before.items() if k not in moving + } + assert no_tmux.commands == [] # no process to kill: nothing was sent to tmux + + +# ── the checkpoint run artifact ─────────────────────────────────────────── + + +def _checkpoint(frozen: Frozen) -> tuple[str, Path]: + [run_id] = frozen.facts["checkpoints"] + return run_id, frozen.project_dir / "runs" / run_id + + +@pytest.mark.parametrize("scenario", ["checkpointed", "closed"]) +def test_a_released_checkpoint_reads_as_written(release, scenario, tmp_path, monkeypatch, no_tmux): + frozen = Frozen(release, scenario, tmp_path, monkeypatch) + run_id, run_dir = _checkpoint(frozen) + manifest = json.loads((run_dir / "manifest.json").read_text()) + assert manifest["state"] == "pending" + assert manifest["plan"] == {"origin": "isolated-session", "session_id": frozen.session_id} + listed = frozen.cli("runs", "list") + assert listed.exit_code == 0, listed.output + assert any(line.split()[:1] == [run_id] for line in listed.output.splitlines()) + shown = frozen.cli("runs", "show", run_id) + assert shown.exit_code == 0, shown.output + assert json.loads(shown.stdout) == manifest + diff = frozen.cli("diff", run_id) + assert diff.exit_code == 0, diff.output + assert diff.stdout == (run_dir / "winner.patch").read_text() + assert "+ return a + b" in diff.stdout + conversation = frozen.cli("conversations", "show", run_id) + assert conversation.exit_code == 0, conversation.output + assert json.loads(conversation.stdout) == { + "manifest": manifest, "result": json.loads((frozen.dir / "runs" / run_id / "result.json") + .read_text()), + } + # No provider session: not a conversation, and not continuable. + conversations = frozen.cli("conversations", "list") + assert conversations.exit_code == 0, conversations.output + assert run_id not in conversations.output + continued = frozen.cli("continue", run_id, "and more") + assert (continued.exit_code, continued.output.splitlines()[-1]) == ( + 1, "Error: run has no resumable provider session" + ) + recovered = frozen.cli("recover", run_id) + assert recovered.exit_code == 0, recovered.output + assert recovered.output.splitlines() == [ + f"Run {run_id}: skipped \N{EM DASH} run is 'pending', not applying" + ] + assert frozen.document()["checkpoints"] == [run_id] + + +def test_the_tui_renders_a_released_checkpoint(release, tmp_path, monkeypatch, no_tmux): + from syncmill.artifacts import ArtifactStore + from syncmill.tui import _read_artifact_result, render_artifact + + frozen = Frozen(release, "checkpointed", tmp_path, monkeypatch) + run_id, run_dir = _checkpoint(frozen) + store = ArtifactStore(frozen.runs_root, frozen.facts["project_id"]) + result = _read_artifact_result(run_id, store=store) + assert result == json.loads((run_dir / "result.json").read_text()) + text = render_artifact(json.loads((run_dir / "manifest.json").read_text()), result) + assert run_id in text and result["output"] in text + + +def test_apply_a_released_checkpoint(release, tmp_path, monkeypatch, no_tmux): + from syncmill.artifacts import ArtifactStore + + frozen = Frozen(release, "checkpointed", tmp_path, monkeypatch) + run_id, _ = _checkpoint(frozen) + store = ArtifactStore(frozen.runs_root, frozen.facts["project_id"]) + # The detached apply's preflight accepts it, without launching anything. + assert store.validate_applicable(run_id).run_id == run_id + applied = frozen.cli("apply", run_id) + assert applied.exit_code == 0, applied.output + assert (frozen.repo / "calc.py").read_text() == FIXED + manifest = store.load(run_id).manifest + assert manifest["state"] == "applied" + [receipt] = (store.root / run_id / "applications").glob("*.json") + assert json.loads(receipt.read_text())["run_id"] == run_id + assert json.loads(receipt.read_text())["apply_id"] == manifest["apply_id"] + assert frozen.document()["checkpoints"] == [run_id] + + +def test_prune_keeps_a_released_pending_checkpoint(release, tmp_path, monkeypatch, no_tmux): + frozen = Frozen(release, "closed", tmp_path, monkeypatch) + run_id, run_dir = _checkpoint(frozen) + # Pending: never pruned, even with nothing kept, although its session is closed. + kept = frozen.cli("runs", "prune", "--keep", "0") + assert kept.exit_code == 0, kept.output + assert run_dir.is_dir() + discarded = frozen.cli("discard", run_id) + assert discarded.exit_code == 0, discarded.output + assert json.loads((run_dir / "manifest.json").read_text())["state"] == "discarded" + pruned = frozen.cli("runs", "prune", "--keep", "0") + assert pruned.exit_code == 0, pruned.output + assert not run_dir.exists() + assert frozen.document()["checkpoints"] == [run_id] + + +def test_close_the_conversation_of_a_released_checkpoint(release, tmp_path, monkeypatch, no_tmux): + frozen = Frozen(release, "checkpointed", tmp_path, monkeypatch) + run_id, run_dir = _checkpoint(frozen) + before = json.loads((run_dir / "manifest.json").read_text()) + stamp = "2031-01-01T00:00:00+00:00" + monkeypatch.setattr("syncmill.artifacts._now", lambda: stamp) + closed = frozen.cli("conversations", "close", run_id) + assert closed.exit_code == 0, closed.output + after = json.loads((run_dir / "manifest.json").read_text()) + # Closing needs no provider session: only the flag and the stamp move. + changed = {k for k in {*before, *after} if before.get(k) != after.get(k)} + assert changed == {"conversation_closed", "updated_at"} + assert (after["conversation_closed"], after["state"], after["updated_at"]) == ( + True, "pending", stamp + ) + assert frozen.document()["checkpoints"] == [run_id] + + +# ── the project marker and config ───────────────────────────────────────── + + +def _project(release: str, tmp_path: Path, monkeypatch) -> tuple[Path, dict]: + """A repository holding the released marker directory and config, with + no configured project id anywhere but the released ``syncmill.toml``.""" + monkeypatch.delenv("SYNCMILL_PROJECT__ID", raising=False) + bindings = _bindings(release) + repo = _repo(tmp_path.resolve(), bindings["tree"]) + stored = FROZEN / release / "project" + assert (repo / "syncmill.toml").read_bytes() == (stored / "syncmill.toml").read_bytes() + marker_dir = repo / ".syncmill" + marker_dir.mkdir() + (marker_dir / "project.json").write_bytes((stored / "marker.json").read_bytes()) + os.chmod(marker_dir / "project.json", int(bindings["marker"]["mode"], 8)) + (marker_dir / ".gitignore").write_bytes((stored / "marker.gitignore").read_bytes()) + return repo, bindings + + +def _path_hash(repo: Path) -> str: + import hashlib + + return hashlib.sha256(str(repo.resolve()).encode()).hexdigest()[:24] + + +def test_the_released_marker_names_the_project(release, tmp_path, monkeypatch): + from syncmill.project import project_id + + repo, bindings = _project(release, tmp_path, monkeypatch) + assert project_id(repo) == bindings["marker"]["id"] + + +def test_a_tracked_released_marker_is_not_trusted(release, tmp_path, monkeypatch, caplog): + from syncmill.project import project_id + + repo, bindings = _project(release, tmp_path, monkeypatch) + subprocess.run(["git", "add", "-f", ".syncmill/project.json"], cwd=repo, check=True) + with caplog.at_level(logging.WARNING): + assert project_id(repo) == _path_hash(repo) + assert any("project.json" in record.getMessage() for record in caplog.records) + + +def test_a_released_marker_through_a_symlink_is_not_trusted(release, tmp_path, monkeypatch): + from syncmill.project import project_id + + repo, _ = _project(release, tmp_path, monkeypatch) + elsewhere = tmp_path / "elsewhere" + (repo / ".syncmill").rename(elsewhere) + (repo / ".syncmill").symlink_to(elsewhere) + assert project_id(repo) == _path_hash(repo) + + +def test_the_released_config_loads_as_written(release, tmp_path, monkeypatch): + import tomllib + + from syncmill.project import load_project_config + + from syncmill.config import SyncmillConfig + + repo, bindings = _project(release, tmp_path, monkeypatch) + # The suite neutralizes cwd-relative discovery; this test is about it. + monkeypatch.setitem(SyncmillConfig.model_config, "toml_file", "syncmill.toml") + written = tomllib.loads((repo / "syncmill.toml").read_text()) + _, config = load_project_config(repo) + assert config.project.id == written["project"]["id"] == bindings["marker"]["id"] + assert config.config_version == written["config_version"] + assert config.project.memory_dir == written["project"]["memory_dir"] + assert config.default_strategy == written["default_strategy"] + assert config.gate.commands == written["gate"]["commands"] + assert Path(config.repo_root).resolve() == repo.resolve() + + +def test_reinit_keeps_the_released_marker_id(release, tmp_path, monkeypatch): + from syncmill.project import initialize_project + + repo, bindings = _project(release, tmp_path, monkeypatch) + # No configured id left: the released marker is the only identity. + (repo / "syncmill.toml").unlink() + _, pid = initialize_project(repo) + assert pid == bindings["marker"]["id"] + assert json.loads((repo / ".syncmill" / "project.json").read_text())["id"] == pid