"""TDD — directory-level insight dimension (issue #3). Phase 1: helpers in _query.py flat_directory_ops — yields RenameOp / insert/delete directory ops touched_directories — set of directories affected by an op list dir_of — extract parent directory from a file path Phase 2: muse diff -- directories key in JSON output rename, added dir, deleted dir surfaced as distinct dimension Phase 3: muse code hotspots --granularity directory churn counted at directory level instead of symbol level Phase 4: muse code entangle --granularity directory co-change pairs at directory granularity Phase 5: muse code impact --roll-up-to directory blast radius rolled up to directory level """ from __future__ import annotations import json import pathlib import textwrap from typing import cast import pytest from muse.domain import RenameOp, DomainOp, InsertOp, DeleteOp, PatchOp, ReplaceOp # --------------------------------------------------------------------------- # Helpers used across phases # --------------------------------------------------------------------------- def _insert(address: str, content_summary: str = "") -> InsertOp: return InsertOp(op="insert", address=address, position=None, content_id="", content_summary=content_summary) def _delete(address: str, content_summary: str = "") -> DeleteOp: return DeleteOp(op="delete", address=address, position=None, content_id="", content_summary=content_summary) def _rename(from_addr: str, to_addr: str, file_count: int = 2) -> RenameOp: return RenameOp(op="rename", address=to_addr, from_address=from_addr) def _patch(address: str, children: list[DomainOp] | None = None) -> PatchOp: return PatchOp( op="patch", address=address, child_ops=children or [], child_domain="code", child_summary="", ) def _sym_insert(address: str) -> InsertOp: return InsertOp(op="insert", address=address, position=None, content_id="", content_summary="added function") # --------------------------------------------------------------------------- # Phase 1A: flat_directory_ops # --------------------------------------------------------------------------- class TestFlatDirectoryOps: """flat_directory_ops yields directory-level ops and ignores symbol/file ops.""" def test_yields_directory_rename(self) -> None: from muse.plugins.code._query import flat_directory_ops ops: list[DomainOp] = [_rename("src/old", "src/new")] result = list(flat_directory_ops(ops)) assert len(result) == 1 assert result[0]["op"] == "rename" def test_yields_directory_insert(self) -> None: from muse.plugins.code._query import flat_directory_ops ops: list[DomainOp] = [_insert("src/newdir/", "directory: src/newdir/")] result = list(flat_directory_ops(ops)) assert len(result) == 1 def test_yields_directory_delete(self) -> None: from muse.plugins.code._query import flat_directory_ops ops: list[DomainOp] = [_delete("src/olddir/", "directory: src/olddir/")] result = list(flat_directory_ops(ops)) assert len(result) == 1 def test_skips_symbol_level_patch_children(self) -> None: from muse.plugins.code._query import flat_directory_ops ops: list[DomainOp] = [ _patch("src/billing.py", [_sym_insert("src/billing.py::compute_total")]), ] result = list(flat_directory_ops(ops)) assert result == [] def test_skips_plain_file_insert(self) -> None: from muse.plugins.code._query import flat_directory_ops ops: list[DomainOp] = [_insert("src/billing.py", "added file")] result = list(flat_directory_ops(ops)) assert result == [] def test_mixed_ops_only_dir(self) -> None: from muse.plugins.code._query import flat_directory_ops ops: list[DomainOp] = [ _rename("api/v1", "api/v2"), _patch("src/billing.py", [_sym_insert("src/billing.py::fn")]), _insert("src/utils.py", "added file"), _insert("tests/", "directory: tests/"), ] result = list(flat_directory_ops(ops)) assert len(result) == 2 # rename + tests/ insert op_types = {r["op"] for r in result} assert "rename" in op_types def test_empty_ops(self) -> None: from muse.plugins.code._query import flat_directory_ops assert list(flat_directory_ops([])) == [] def test_rename_carries_from_address(self) -> None: from muse.plugins.code._query import flat_directory_ops ops: list[DomainOp] = [_rename("src/auth_old", "src/auth")] result = list(flat_directory_ops(ops)) assert result[0].get("from_address") == "src/auth_old" assert result[0]["address"] == "src/auth" # --------------------------------------------------------------------------- # Phase 1B: touched_directories # --------------------------------------------------------------------------- class TestTouchedDirectories: """touched_directories returns the set of directories whose files changed.""" def test_single_file_returns_its_parent(self) -> None: from muse.plugins.code._query import touched_directories ops: list[DomainOp] = [ _patch("src/billing.py", [_sym_insert("src/billing.py::fn")]), ] dirs = touched_directories(ops) assert "src" in dirs def test_root_level_file_returns_dot_or_empty(self) -> None: from muse.plugins.code._query import touched_directories ops: list[DomainOp] = [ _patch("main.py", [_sym_insert("main.py::fn")]), ] dirs = touched_directories(ops) # root-level files belong to "." (POSIX convention) assert "." in dirs def test_multiple_files_same_dir_counted_once(self) -> None: from muse.plugins.code._query import touched_directories ops: list[DomainOp] = [ _patch("src/billing.py", [_sym_insert("src/billing.py::fn")]), _patch("src/auth.py", [_sym_insert("src/auth.py::validate")]), ] dirs = touched_directories(ops) assert dirs.count("src") == 1 if isinstance(dirs, list) else len([d for d in dirs if d == "src"]) == 1 def test_returns_frozenset(self) -> None: from muse.plugins.code._query import touched_directories ops: list[DomainOp] = [ _patch("src/a.py", [_sym_insert("src/a.py::fn")]), ] result = touched_directories(ops) assert isinstance(result, frozenset) def test_nested_path_returns_immediate_parent(self) -> None: from muse.plugins.code._query import touched_directories ops: list[DomainOp] = [ _patch("muse/cli/commands/cat.py", [_sym_insert("muse/cli/commands/cat.py::run")]), ] dirs = touched_directories(ops) assert "muse/cli/commands" in dirs def test_directory_rename_op_adds_both_dirs(self) -> None: from muse.plugins.code._query import touched_directories ops: list[DomainOp] = [_rename("api/v1", "api/v2")] dirs = touched_directories(ops) assert "api/v1" in dirs assert "api/v2" in dirs def test_empty_ops_returns_empty(self) -> None: from muse.plugins.code._query import touched_directories assert touched_directories([]) == frozenset() def test_file_without_symbol_children_not_counted(self) -> None: from muse.plugins.code._query import touched_directories ops: list[DomainOp] = [ _patch("src/billing.py", []), # PatchOp with no children — non-semantic ] dirs = touched_directories(ops) assert "src" not in dirs # --------------------------------------------------------------------------- # Phase 1C: dir_of # --------------------------------------------------------------------------- class TestDirOf: """dir_of extracts the immediate parent directory from a file path.""" def test_nested_path(self) -> None: from muse.plugins.code._query import dir_of assert dir_of("src/billing.py") == "src" def test_deeply_nested(self) -> None: from muse.plugins.code._query import dir_of assert dir_of("muse/cli/commands/cat.py") == "muse/cli/commands" def test_root_level_file(self) -> None: from muse.plugins.code._query import dir_of assert dir_of("main.py") == "." def test_directory_address_trailing_slash(self) -> None: from muse.plugins.code._query import dir_of assert dir_of("src/") == "src" def test_no_extension_file(self) -> None: from muse.plugins.code._query import dir_of assert dir_of("src/Makefile") == "src" # --------------------------------------------------------------------------- # Phase 2: muse diff -- directory dimension in JSON output # --------------------------------------------------------------------------- from tests.cli_test_helper import CliRunner cli = None runner = CliRunner() def _make_diff_repo(tmp_path: pathlib.Path) -> pathlib.Path: """Init a repo and make two commits with a directory rename.""" from muse.core.object_store import write_object from muse.core.ids import hash_commit as compute_commit_id, hash_snapshot as compute_snapshot_id from muse.core.commits import ( CommitRecord, write_commit, ) from muse.core.snapshots import ( SnapshotRecord, write_snapshot, ) from muse.core.types import long_id, blob_id from muse.core.paths import muse_dir, ref_path import datetime dot = muse_dir(tmp_path) for d in ("commits", "snapshots", "objects", "refs/heads"): (dot / d).mkdir(parents=True, exist_ok=True) (dot / "HEAD").write_text("ref: refs/heads/main", encoding="utf-8") (dot / "repo.json").write_text(json.dumps({"repo_id": "dir-dim-test", "domain": "code"}), encoding="utf-8") return tmp_path # --------------------------------------------------------------------------- # Phase 3: muse code hotspots --granularity directory # --------------------------------------------------------------------------- class TestHotspotsDirectoryGranularity: """muse code hotspots --granularity directory counts churn at dir level.""" def test_granularity_flag_accepted(self, tmp_path: pathlib.Path) -> None: import argparse from muse.cli.commands.hotspots import register p = argparse.ArgumentParser() subs = p.add_subparsers(dest="cmd") register(subs) args = p.parse_args(["hotspots", "--granularity", "directory"]) assert args.granularity == "directory" def test_granularity_default_is_symbol(self, tmp_path: pathlib.Path) -> None: import argparse from muse.cli.commands.hotspots import register p = argparse.ArgumentParser() subs = p.add_subparsers(dest="cmd") register(subs) args = p.parse_args(["hotspots"]) assert args.granularity == "symbol" def test_directory_granularity_json_addresses_have_no_colons( self, tmp_path: pathlib.Path ) -> None: """Directory addresses are plain paths, never contain '::'.""" from tests.cli_test_helper import CliRunner as CR r = CR() from muse.core.object_store import write_object from muse.core.ids import hash_commit as compute_commit_id, hash_snapshot as compute_snapshot_id from muse.core.commits import ( CommitRecord, write_commit, ) from muse.core.snapshots import ( SnapshotRecord, write_snapshot, ) from muse.core.types import long_id, blob_id from muse.core.paths import muse_dir, ref_path import datetime dot = muse_dir(tmp_path) for d in ("commits", "snapshots", "objects", "refs/heads"): (dot / d).mkdir(parents=True, exist_ok=True) (dot / "HEAD").write_text("ref: refs/heads/main", encoding="utf-8") (dot / "repo.json").write_text(json.dumps({"repo_id": "hs-dir-test", "domain": "code"}), encoding="utf-8") result = r.invoke( None, ["code", "hotspots", "--granularity", "directory", "--json"], env={"MUSE_REPO_ROOT": str(tmp_path)}, ) assert result.exit_code == 0 data = json.loads(result.output) assert "hotspots" in data for entry in data["hotspots"]: assert "::" not in entry["address"], f"directory address contains '::': {entry['address']}" def test_directory_granularity_json_has_granularity_field( self, tmp_path: pathlib.Path ) -> None: from tests.cli_test_helper import CliRunner as CR from muse.core.paths import muse_dir dot = muse_dir(tmp_path) for d in ("commits", "snapshots", "objects", "refs/heads"): (dot / d).mkdir(parents=True, exist_ok=True) (dot / "HEAD").write_text("ref: refs/heads/main", encoding="utf-8") (dot / "repo.json").write_text(json.dumps({"repo_id": "hs-dir-test2", "domain": "code"}), encoding="utf-8") r = CR() result = r.invoke( None, ["code", "hotspots", "--granularity", "directory", "--json"], env={"MUSE_REPO_ROOT": str(tmp_path)}, ) assert result.exit_code == 0 data = json.loads(result.output) assert data.get("granularity") == "directory" def test_symbol_granularity_json_addresses_contain_colons( self, tmp_path: pathlib.Path ) -> None: """Default (symbol) granularity addresses are 'file.py::symbol'.""" from tests.cli_test_helper import CliRunner as CR from muse.core.paths import muse_dir dot = muse_dir(tmp_path) for d in ("commits", "snapshots", "objects", "refs/heads"): (dot / d).mkdir(parents=True, exist_ok=True) (dot / "HEAD").write_text("ref: refs/heads/main", encoding="utf-8") (dot / "repo.json").write_text(json.dumps({"repo_id": "hs-sym-test", "domain": "code"}), encoding="utf-8") r = CR() result = r.invoke( None, ["code", "hotspots", "--json"], env={"MUSE_REPO_ROOT": str(tmp_path)}, ) assert result.exit_code == 0 data = json.loads(result.output) assert data.get("granularity") == "symbol" # --------------------------------------------------------------------------- # Phase 4: muse code entangle --granularity directory # --------------------------------------------------------------------------- class TestEntangleDirectoryGranularity: """muse code entangle --granularity directory finds directories that always change together.""" def test_granularity_flag_accepted(self) -> None: import argparse from muse.cli.commands.entangle import register p = argparse.ArgumentParser() subs = p.add_subparsers(dest="cmd") register(subs) args = p.parse_args(["entangle", "--granularity", "directory"]) assert args.granularity == "directory" def test_granularity_default_is_symbol(self) -> None: import argparse from muse.cli.commands.entangle import register p = argparse.ArgumentParser() subs = p.add_subparsers(dest="cmd") register(subs) args = p.parse_args(["entangle"]) assert args.granularity == "symbol" def test_directory_granularity_json_pairs_have_no_colons( self, tmp_path: pathlib.Path ) -> None: from tests.cli_test_helper import CliRunner as CR from muse.core.paths import muse_dir dot = muse_dir(tmp_path) for d in ("commits", "snapshots", "objects", "refs/heads"): (dot / d).mkdir(parents=True, exist_ok=True) (dot / "HEAD").write_text("ref: refs/heads/main", encoding="utf-8") (dot / "repo.json").write_text(json.dumps({"repo_id": "ent-dir-test", "domain": "code"}), encoding="utf-8") r = CR() result = r.invoke( None, ["code", "entangle", "--granularity", "directory", "--json"], env={"MUSE_REPO_ROOT": str(tmp_path)}, ) assert result.exit_code == 0 data = json.loads(result.output) assert "pairs" in data for pair in data["pairs"]: assert "::" not in pair["dir_a"] assert "::" not in pair["dir_b"] def test_directory_granularity_json_has_granularity_field( self, tmp_path: pathlib.Path ) -> None: from tests.cli_test_helper import CliRunner as CR from muse.core.paths import muse_dir dot = muse_dir(tmp_path) for d in ("commits", "snapshots", "objects", "refs/heads"): (dot / d).mkdir(parents=True, exist_ok=True) (dot / "HEAD").write_text("ref: refs/heads/main", encoding="utf-8") (dot / "repo.json").write_text(json.dumps({"repo_id": "ent-dir-test2", "domain": "code"}), encoding="utf-8") r = CR() result = r.invoke( None, ["code", "entangle", "--granularity", "directory", "--json"], env={"MUSE_REPO_ROOT": str(tmp_path)}, ) assert result.exit_code == 0 data = json.loads(result.output) assert data.get("granularity") == "directory" # --------------------------------------------------------------------------- # Phase 5: muse code impact --roll-up-to directory # --------------------------------------------------------------------------- class TestImpactDirectoryRollup: """muse code impact --roll-up-to directory aggregates blast radius by dir.""" def test_roll_up_to_flag_accepted(self) -> None: import argparse from muse.cli.commands.impact import register p = argparse.ArgumentParser() subs = p.add_subparsers(dest="cmd") register(subs) args = p.parse_args(["impact", "src/billing.py::compute_total", "--roll-up-to", "directory"]) assert args.roll_up_to == "directory" def test_roll_up_to_default_is_none(self) -> None: import argparse from muse.cli.commands.impact import register p = argparse.ArgumentParser() subs = p.add_subparsers(dest="cmd") register(subs) args = p.parse_args(["impact", "src/billing.py::compute_total"]) assert args.roll_up_to is None def test_directory_rollup_json_has_directory_blast_radius( self, tmp_path: pathlib.Path ) -> None: from tests.cli_test_helper import CliRunner as CR from muse.core.paths import muse_dir dot = muse_dir(tmp_path) for d in ("commits", "snapshots", "objects", "refs/heads"): (dot / d).mkdir(parents=True, exist_ok=True) (dot / "HEAD").write_text("ref: refs/heads/main", encoding="utf-8") (dot / "repo.json").write_text(json.dumps({"repo_id": "imp-dir-test", "domain": "code"}), encoding="utf-8") r = CR() result = r.invoke( None, ["code", "impact", "src/billing.py::compute_total", "--roll-up-to", "directory", "--json"], env={"MUSE_REPO_ROOT": str(tmp_path)}, ) # May exit 1 (symbol not found in empty repo) — just verify schema when successful if result.exit_code == 0: data = json.loads(result.output) assert "directory_blast_radius" in data def test_directory_rollup_addresses_have_no_colons( self, tmp_path: pathlib.Path ) -> None: """directory_blast_radius keys are plain directory paths, never 'file::sym'.""" from tests.cli_test_helper import CliRunner as CR from muse.core.paths import muse_dir dot = muse_dir(tmp_path) for d in ("commits", "snapshots", "objects", "refs/heads"): (dot / d).mkdir(parents=True, exist_ok=True) (dot / "HEAD").write_text("ref: refs/heads/main", encoding="utf-8") (dot / "repo.json").write_text(json.dumps({"repo_id": "imp-dir-test2", "domain": "code"}), encoding="utf-8") r = CR() result = r.invoke( None, ["code", "impact", "src/billing.py::compute_total", "--roll-up-to", "directory", "--json"], env={"MUSE_REPO_ROOT": str(tmp_path)}, ) if result.exit_code == 0: data = json.loads(result.output) for dir_path in data.get("directory_blast_radius", {}).keys(): assert "::" not in dir_path