"""Tests for muse/plugins/todo/plugin.py — the Episode 06 reference domain. Built live in Build With Muse, Episode 06 ("Build a Domain"). Covers the six required protocol methods plus two real regressions hit while building it on camera: 1. Object IDs must use the canonical `blob_id()` scheme (Episode 03's `blob \\0` format), not a bare sha256 of raw bytes. 2. `diff()`'s top-level ops must be keyed by the *file* address (so `muse checkout` can restore/delete the right object), with task-level detail nested as `child_ops` -- not task-level addresses at the top, which `muse checkout` silently fails to act on. """ from __future__ import annotations import pathlib from muse.core.object_store import read_object, write_object from muse.core.types import blob_id from muse.plugins.todo.plugin import TodoPlugin, _TASK_FILE def _repo(tmp_path: pathlib.Path) -> pathlib.Path: (tmp_path / ".muse" / "objects").mkdir(parents=True) return tmp_path def _commit_tasks(plugin: TodoPlugin, repo_root: pathlib.Path, tasks: list[str]) -> dict: body = "\n".join(tasks) + ("\n" if tasks else "") (repo_root / _TASK_FILE).write_text(body) snap = plugin.snapshot(repo_root) if tasks: raw = body.encode("utf-8") write_object(repo_root, blob_id(raw), raw) return snap class TestSchema: def test_top_level_is_content_addressed_set(self) -> None: schema = TodoPlugin().schema() assert schema["top_level"]["kind"] == "set" assert schema["top_level"]["identity"] == "by_content" def test_merge_mode_is_three_way(self) -> None: assert TodoPlugin().schema()["merge_mode"] == "three_way" class TestSnapshot: def test_empty_repo_has_no_files(self, tmp_path: pathlib.Path) -> None: root = _repo(tmp_path) snap = TodoPlugin().snapshot(root) assert snap["files"] == {} def test_object_id_uses_canonical_blob_format(self, tmp_path: pathlib.Path) -> None: """Regression: object IDs must be blob_id(), not bare sha256(raw). A bare sha256 of raw bytes doesn't match what write_object_from_path (and the rest of the store) expects -- confirmed live, this made `muse commit` fail outright with a content-integrity error. """ root = _repo(tmp_path) (root / _TASK_FILE).write_text("Buy milk\n") snap = TodoPlugin().snapshot(root) assert snap["files"][_TASK_FILE] == blob_id(b"Buy milk\n") class TestDiff: def test_no_change_when_hashes_match(self, tmp_path: pathlib.Path) -> None: root = _repo(tmp_path) snap = _commit_tasks(TodoPlugin(), root, ["Buy milk"]) delta = TodoPlugin().diff(snap, snap, repo_root=root) assert delta["ops"] == [] def test_top_level_op_is_keyed_by_file_not_task(self, tmp_path: pathlib.Path) -> None: """Regression: `muse checkout` restores/deletes files by matching `op["address"]` against the snapshot manifest's file paths. A task-level address (e.g. "todo.txt::") never matches, so checkout silently restored nothing -- confirmed live, switching branches left stale content from the previously-checked-out branch sitting in the working tree with no error at all. """ plugin = TodoPlugin() base = _commit_tasks(plugin, root := tmp_path, []) target = _commit_tasks(plugin, root, ["Buy milk"]) delta = plugin.diff(base, target, repo_root=root) assert len(delta["ops"]) == 1 assert delta["ops"][0]["op"] == "patch" assert delta["ops"][0]["address"] == _TASK_FILE def test_child_ops_carry_task_level_detail(self, tmp_path: pathlib.Path) -> None: plugin = TodoPlugin() root = tmp_path base = _commit_tasks(plugin, root, ["Buy milk"]) target = _commit_tasks(plugin, root, ["Buy milk", "Fix the bug"]) delta = plugin.diff(base, target, repo_root=root) child_ops = delta["ops"][0]["child_ops"] assert len(child_ops) == 1 assert child_ops[0]["op"] == "insert" assert "Fix the bug" in child_ops[0]["content_summary"] def test_added_and_removed_tasks_both_reported(self, tmp_path: pathlib.Path) -> None: plugin = TodoPlugin() root = tmp_path base = _commit_tasks(plugin, root, ["Buy milk"]) target = _commit_tasks(plugin, root, ["Fix the bug"]) delta = plugin.diff(base, target, repo_root=root) assert delta["summary"] == "1 task(s) added, 1 task(s) removed" class TestMerge: def test_two_independent_additions_never_conflict(self, tmp_path: pathlib.Path) -> None: plugin = TodoPlugin() root = tmp_path base = _commit_tasks(plugin, root, ["Write episode 06 script"]) left = _commit_tasks(plugin, root, ["Write episode 06 script", "Buy milk"]) right = _commit_tasks(plugin, root, ["Write episode 06 script", "Fix the bug"]) result = plugin.merge(base, left, right, repo_root=root) assert result.conflicts == [] merged_raw = read_object(root, result.merged["files"][_TASK_FILE]) merged_tasks = set(merged_raw.decode("utf-8").splitlines()) assert merged_tasks == {"Write episode 06 script", "Buy milk", "Fix the bug"} def test_task_removed_on_one_side_stays_removed(self, tmp_path: pathlib.Path) -> None: plugin = TodoPlugin() root = tmp_path base = _commit_tasks(plugin, root, ["Buy milk", "Fix the bug"]) left = _commit_tasks(plugin, root, ["Fix the bug"]) # removed "Buy milk" right = _commit_tasks(plugin, root, ["Buy milk", "Fix the bug"]) # unchanged result = plugin.merge(base, left, right, repo_root=root) merged_raw = read_object(root, result.merged["files"][_TASK_FILE]) merged_tasks = set(merged_raw.decode("utf-8").splitlines()) assert merged_tasks == {"Fix the bug"} class TestDrift: def test_uncommitted_change_is_detected(self, tmp_path: pathlib.Path) -> None: """Regression: drift must see live, uncommitted file content even though snapshot() never writes the live file's blob to the object store -- only `commit` does that. Reading back via read_object alone returned None for anything not yet committed, so drift saw "0 added, N removed" for any dirty working tree, regardless of what actually changed. """ plugin = TodoPlugin() root = tmp_path committed = _commit_tasks(plugin, root, ["Buy milk"]) (root / _TASK_FILE).write_text("Buy milk\nShip it\n") # uncommitted report = plugin.drift(committed, root) assert report.has_drift is True assert report.summary == "1 task(s) added, 0 task(s) removed" def test_clean_tree_has_no_drift(self, tmp_path: pathlib.Path) -> None: plugin = TodoPlugin() root = tmp_path committed = _commit_tasks(plugin, root, ["Buy milk"]) report = plugin.drift(committed, root) assert report.has_drift is False