gabriel / muse public
test_read_color.py python
171 lines 7.1 KB
Raw
sha256:a154bc65916614c833d5a40a10d81ba3eae0d0495b0afddd34dc34f18d5e91b8 fix: test suite alignment and typing audit — zero violations Sonnet 4.6 minor ⚠ breaking 21 days ago
1 """TDD — ANSI color in muse read text output.
2
3 RC-1 _format_op insert returns green-prefixed line when is_tty=True
4 RC-2 _format_op delete returns red-prefixed line when is_tty=True
5 RC-3 _format_op patch/replace returns yellow-prefixed line when is_tty=True
6 RC-4 _format_op rename returns cyan-prefixed line when is_tty=True
7 RC-5 Non-TTY integration: no ANSI codes anywhere in muse read output
8 RC-6 Fallback path A/D/M lines carry color when is_tty=True via _color_op_line
9 RC-7 Summary labels (Directories/Files/Symbols) are bold in tty mode
10 """
11 from __future__ import annotations
12
13 import datetime
14 import json
15 import pathlib
16 from collections.abc import Mapping
17
18 import pytest
19
20 from tests.cli_test_helper import CliRunner
21 from muse.core.paths import muse_dir, ref_path
22 from muse.core.object_store import write_object
23 from muse.core.ids import hash_commit, hash_snapshot
24 from muse.core.commits import CommitRecord, write_commit
25 from muse.core.snapshots import SnapshotRecord, write_snapshot
26 from muse.core.types import blob_id
27
28 runner = CliRunner()
29
30 _GREEN = "\033[32m"
31 _RED = "\033[31m"
32 _YELLOW = "\033[33m"
33 _CYAN = "\033[36m"
34 _BOLD = "\033[1m"
35 _RESET = "\033[0m"
36
37
38 # ---------------------------------------------------------------------------
39 # Helpers
40 # ---------------------------------------------------------------------------
41
42 def _init_repo(path: pathlib.Path) -> pathlib.Path:
43 dot = muse_dir(path)
44 for d in ("commits", "snapshots", "objects", "refs/heads", "code"):
45 (dot / d).mkdir(parents=True, exist_ok=True)
46 (dot / "HEAD").write_text("ref: refs/heads/main", encoding="utf-8")
47 (dot / "repo.json").write_text(
48 json.dumps({"repo_id": "color-test", "domain": "code"}),
49 encoding="utf-8",
50 )
51 return path
52
53
54 def _make_commit(
55 root: pathlib.Path,
56 files: Mapping[str, bytes],
57 parent: str | None = None,
58 directories: list[str] | None = None,
59 ) -> str:
60 manifest: dict[str, str] = {}
61 for rel, content in files.items():
62 oid = blob_id(content)
63 write_object(root, oid, content)
64 manifest[rel] = oid
65 snap_id = hash_snapshot(manifest, directories or [])
66 write_snapshot(root, SnapshotRecord(
67 snapshot_id=snap_id, manifest=manifest, directories=directories or []
68 ))
69 committed_at = datetime.datetime.now(datetime.timezone.utc)
70 commit_id = hash_commit(
71 parent_ids=[parent] if parent else [],
72 snapshot_id=snap_id,
73 message="test",
74 committed_at_iso=committed_at.isoformat(),
75 )
76 write_commit(root, CommitRecord(
77 commit_id=commit_id, branch="main", snapshot_id=snap_id,
78 message="test", committed_at=committed_at, parent_commit_id=parent,
79 ))
80 ref_path(root, "main").write_text(commit_id, encoding="utf-8")
81 return commit_id
82
83
84 def _env(root: pathlib.Path) -> Mapping[str, str]:
85 return {"MUSE_REPO_ROOT": str(root)}
86
87
88 # ---------------------------------------------------------------------------
89 # RC-1/2/3/4 _format_op color by op type
90 # ---------------------------------------------------------------------------
91
92 class TestFormatOpColor:
93 def test_insert_is_green(self) -> None:
94 from muse.cli.commands.read import _format_op
95 lines = _format_op({"op": "insert", "address": "new.py", "content_id": "", "content_summary": ""}, is_tty=True)
96 assert any(_GREEN in l for l in lines), f"insert must produce green line: {lines}"
97
98 def test_delete_is_red(self) -> None:
99 from muse.cli.commands.read import _format_op
100 lines = _format_op({"op": "delete", "address": "old.py", "content_id": "", "content_summary": ""}, is_tty=True)
101 assert any(_RED in l for l in lines), f"delete must produce red line: {lines}"
102
103 def test_replace_is_yellow(self) -> None:
104 from muse.cli.commands.read import _format_op
105 lines = _format_op({"op": "replace", "address": "f.py", "old_content_id": "", "new_content_id": ""}, is_tty=True)
106 assert any(_YELLOW in l for l in lines), f"replace must produce yellow line: {lines}"
107
108 def test_rename_is_cyan(self) -> None:
109 from muse.cli.commands.read import _format_op
110 lines = _format_op({"op": "rename", "address": "new.py", "from_address": "old.py"}, is_tty=True)
111 assert any(_CYAN in l for l in lines), f"rename must produce cyan line: {lines}"
112
113 def test_patch_is_yellow(self) -> None:
114 from muse.cli.commands.read import _format_op
115 lines = _format_op({"op": "patch", "address": "f.py", "child_ops": [], "child_summary": ""}, is_tty=True)
116 assert any(_YELLOW in l for l in lines), f"patch must produce yellow line: {lines}"
117
118 def test_no_color_when_not_tty(self) -> None:
119 from muse.cli.commands.read import _format_op
120 lines = _format_op({"op": "insert", "address": "new.py", "content_id": "", "content_summary": ""}, is_tty=False)
121 assert not any("\033[" in l for l in lines), f"no ANSI without tty: {lines}"
122
123
124 # ---------------------------------------------------------------------------
125 # RC-6 Fallback path color helper
126 # ---------------------------------------------------------------------------
127
128 class TestColorOpLine:
129 def test_color_op_line_a_is_green(self) -> None:
130 from muse.cli.commands.read import _color_op_line
131 line = _color_op_line("A", "test.py", is_tty=True)
132 assert _GREEN in line, f"A line must be green: {line!r}"
133
134 def test_color_op_line_d_is_red(self) -> None:
135 from muse.cli.commands.read import _color_op_line
136 line = _color_op_line("D", "test.py", is_tty=True)
137 assert _RED in line, f"D line must be red: {line!r}"
138
139 def test_color_op_line_m_is_yellow(self) -> None:
140 from muse.cli.commands.read import _color_op_line
141 line = _color_op_line("M", "test.py", is_tty=True)
142 assert _YELLOW in line, f"M line must be yellow: {line!r}"
143
144 def test_color_op_line_no_color_non_tty(self) -> None:
145 from muse.cli.commands.read import _color_op_line
146 line = _color_op_line("A", "test.py", is_tty=False)
147 assert "\033[" not in line, f"no ANSI without tty: {line!r}"
148
149
150 # ---------------------------------------------------------------------------
151 # RC-5 Non-TTY integration: no ANSI in muse read output
152 # ---------------------------------------------------------------------------
153
154 class TestNoColorIntegration:
155 def test_no_ansi_in_plain_output(self, tmp_path: pathlib.Path) -> None:
156 root = _init_repo(tmp_path)
157 c1 = _make_commit(root, {"readme.md": b"# hi\n"})
158 _make_commit(root, {"readme.md": b"# hi\n", "new.py": b"x=1\n"}, parent=c1)
159
160 out = runner.invoke(None, ["read"], env=_env(root)).output
161
162 assert "\033[" not in out, f"ANSI codes must not appear in non-TTY output:\n{repr(out)}"
163
164 def test_no_ansi_in_plain_fallback_path(self, tmp_path: pathlib.Path) -> None:
165 root = _init_repo(tmp_path)
166 c1 = _make_commit(root, {"readme.md": b"# hi\n"})
167 _make_commit(root, {"readme.md": b"# hi\n"}, parent=c1, directories=["mydir"])
168
169 out = runner.invoke(None, ["read"], env=_env(root)).output
170
171 assert "\033[" not in out, f"ANSI codes must not appear in non-TTY fallback output:\n{repr(out)}"
File History 2 commits
sha256:a154bc65916614c833d5a40a10d81ba3eae0d0495b0afddd34dc34f18d5e91b8 fix: test suite alignment and typing audit — zero violations Sonnet 4.6 minor 21 days ago
sha256:3767afb72520f9b56053bb98fd83d323f738ee4cad16e306e8cf6862608380e4 feat: first-class directory tracking across status, diff, r… Sonnet 4.6 minor 21 days ago