gabriel / muse public
test_code_manifest.py python
201 lines 8.2 KB
Raw
sha256:c5131d76c6eada02939111fda4aa8e51b0c1456b9983727cfd6be101916de14e merge: pull local/dev — resolve trivial _EXT_MAP symbol con… Sonnet 4.6 patch 13 days ago
1 """Tests for the hierarchical code manifest in muse/plugins/code/manifest.py."""
2
3 import pathlib
4 import tempfile
5
6 import pytest
7
8 from muse.core.types import blob_id
9 from muse.core.object_store import object_path
10 from muse.core.paths import muse_dir
11 from muse.plugins.code.manifest import (
12 CodeManifest,
13 ManifestFileDiff,
14 build_code_manifest,
15 diff_manifests,
16 read_code_manifest,
17 write_code_manifest,
18 )
19
20
21 # ---------------------------------------------------------------------------
22 # Helpers
23 # ---------------------------------------------------------------------------
24
25
26 def _make_repo(tmp_path: pathlib.Path) -> pathlib.Path:
27 dot_muse = muse_dir(tmp_path)
28 dot_muse.mkdir()
29 (dot_muse / "objects").mkdir()
30 return tmp_path
31
32
33 def _write_object(root: pathlib.Path, content: bytes) -> str:
34 from muse.core.object_store import write_object
35 oid = blob_id(content)
36 write_object(root, oid, content)
37 return oid
38
39
40 # ---------------------------------------------------------------------------
41 # build_code_manifest
42 # ---------------------------------------------------------------------------
43
44
45 class TestBuildCodeManifest:
46 def test_empty_snapshot(self) -> None:
47 with tempfile.TemporaryDirectory() as tmp:
48 root = _make_repo(pathlib.Path(tmp))
49 manifest = build_code_manifest("s" * 64, {}, root)
50 assert manifest["snapshot_id"] == "s" * 64
51 assert manifest["total_files"] == 0
52 assert manifest["packages"] == []
53 assert manifest["total_symbols"] == 0
54
55 def test_single_python_file(self) -> None:
56 with tempfile.TemporaryDirectory() as tmp:
57 root = _make_repo(pathlib.Path(tmp))
58 src = b"def foo():\n return 1\n"
59 h = _write_object(root, src)
60 manifest = build_code_manifest("s" * 64, {"src/utils.py": h}, root)
61 assert manifest["total_files"] == 1
62 assert manifest["semantic_files"] >= 1
63 assert len(manifest["packages"]) == 1
64 pkg = manifest["packages"][0]
65 assert pkg["package"] == "src"
66 assert len(pkg["modules"]) == 1
67 mod = pkg["modules"][0]
68 assert mod["module_path"] == "src/utils.py"
69 assert mod["language"] == "Python"
70
71 def test_groups_by_package(self) -> None:
72 with tempfile.TemporaryDirectory() as tmp:
73 root = _make_repo(pathlib.Path(tmp))
74 h1 = _write_object(root, b"x = 1\n")
75 h2 = _write_object(root, b"y = 2\n")
76 h3 = _write_object(root, b"z = 3\n")
77 flat = {
78 "src/a.py": h1,
79 "src/b.py": h2,
80 "tests/c.py": h3,
81 }
82 manifest = build_code_manifest("s" * 64, flat, root)
83 assert manifest["total_files"] == 3
84 packages = {pkg["package"] for pkg in manifest["packages"]}
85 assert "src" in packages
86 assert "tests" in packages
87
88 def test_manifest_hash_stable(self) -> None:
89 with tempfile.TemporaryDirectory() as tmp:
90 root = _make_repo(pathlib.Path(tmp))
91 src = b"x = 1\n"
92 h = _write_object(root, src)
93 m1 = build_code_manifest("s" * 64, {"a.py": h}, root)
94 m2 = build_code_manifest("s" * 64, {"a.py": h}, root)
95 assert m1["manifest_hash"] == m2["manifest_hash"]
96
97 def test_non_semantic_file_has_empty_ast_hash(self) -> None:
98 with tempfile.TemporaryDirectory() as tmp:
99 root = _make_repo(pathlib.Path(tmp))
100 h = _write_object(root, b"some binary or text content")
101 manifest = build_code_manifest("s" * 64, {"README.md": h}, root)
102 mod = manifest["packages"][0]["modules"][0]
103 assert mod["ast_hash"] == ""
104 assert mod["symbol_count"] == 0
105
106
107 # ---------------------------------------------------------------------------
108 # diff_manifests
109 # ---------------------------------------------------------------------------
110
111
112 class TestDiffManifests:
113 def _build_simple(self, root: pathlib.Path, files: _FileStore) -> CodeManifest:
114 flat: Manifest = {}
115 for path, content in files.items():
116 flat[path] = _write_object(root, content)
117 return build_code_manifest("snap", flat, root)
118
119 def test_identical_manifests_no_diff(self) -> None:
120 with tempfile.TemporaryDirectory() as tmp:
121 root = _make_repo(pathlib.Path(tmp))
122 base = self._build_simple(root, {"a.py": b"x = 1\n"})
123 diffs = diff_manifests(base, base)
124 assert diffs == []
125
126 def test_added_file_detected(self) -> None:
127 with tempfile.TemporaryDirectory() as tmp:
128 root = _make_repo(pathlib.Path(tmp))
129 base = self._build_simple(root, {"a.py": b"x = 1\n"})
130 target = self._build_simple(root, {"a.py": b"x = 1\n", "b.py": b"y = 2\n"})
131 diffs = diff_manifests(base, target)
132 added = [d for d in diffs if d["change"] == "added"]
133 assert any(d["path"] == "b.py" for d in added)
134
135 def test_removed_file_detected(self) -> None:
136 with tempfile.TemporaryDirectory() as tmp:
137 root = _make_repo(pathlib.Path(tmp))
138 base = self._build_simple(root, {"a.py": b"x = 1\n", "b.py": b"y = 2\n"})
139 target = self._build_simple(root, {"a.py": b"x = 1\n"})
140 diffs = diff_manifests(base, target)
141 removed = [d for d in diffs if d["change"] == "removed"]
142 assert any(d["path"] == "b.py" for d in removed)
143
144 def test_semantic_change_detected(self) -> None:
145 with tempfile.TemporaryDirectory() as tmp:
146 root = _make_repo(pathlib.Path(tmp))
147 base = self._build_simple(root, {"a.py": b"def foo():\n return 1\n"})
148 target = self._build_simple(root, {"a.py": b"def foo():\n return 2\n"})
149 diffs = diff_manifests(base, target)
150 assert len(diffs) == 1
151 assert diffs[0]["semantic_change"] is True
152
153 def test_whitespace_only_change_non_semantic(self) -> None:
154 # Whitespace-only changes: content_hash differs but ast_hash should be the same.
155 with tempfile.TemporaryDirectory() as tmp:
156 root = _make_repo(pathlib.Path(tmp))
157 base = self._build_simple(root, {"a.py": b"def foo():\n return 1\n"})
158 target = self._build_simple(root, {"a.py": b"def foo():\n return 1\n\n\n"})
159 diffs = diff_manifests(base, target)
160 # Whitespace diff may or may not change AST hash depending on parser.
161 # Just assert we get a diff record with a path.
162 if diffs:
163 assert diffs[0]["path"] == "a.py"
164
165
166 # ---------------------------------------------------------------------------
167 # Persistence
168 # ---------------------------------------------------------------------------
169
170
171 class TestManifestPersistence:
172 def test_write_and_read_roundtrip(self) -> None:
173 with tempfile.TemporaryDirectory() as tmp:
174 root = _make_repo(pathlib.Path(tmp))
175 src = b"def my_fn():\n pass\n"
176 h = _write_object(root, src)
177 original = build_code_manifest("s" * 64, {"src/a.py": h}, root)
178
179 write_code_manifest(root, original)
180 loaded = read_code_manifest(root, original["manifest_hash"])
181
182 assert loaded is not None
183 assert loaded["snapshot_id"] == "s" * 64
184 assert loaded["manifest_hash"] == original["manifest_hash"]
185 assert len(loaded["packages"]) == len(original["packages"])
186
187 def test_read_nonexistent_returns_none(self) -> None:
188 with tempfile.TemporaryDirectory() as tmp:
189 root = _make_repo(pathlib.Path(tmp))
190 result = read_code_manifest(root, "nonexistent_hash")
191 assert result is None
192
193 def test_write_idempotent(self) -> None:
194 with tempfile.TemporaryDirectory() as tmp:
195 root = _make_repo(pathlib.Path(tmp))
196 h = _write_object(root, b"x = 1\n")
197 manifest = build_code_manifest("s" * 64, {"a.py": h}, root)
198 write_code_manifest(root, manifest)
199 write_code_manifest(root, manifest) # second write should not error
200 loaded = read_code_manifest(root, manifest["manifest_hash"])
201 assert loaded is not None
File History 5 commits
sha256:c5131d76c6eada02939111fda4aa8e51b0c1456b9983727cfd6be101916de14e merge: pull local/dev — resolve trivial _EXT_MAP symbol con… Sonnet 4.6 patch 13 days ago
sha256:9c33d61749fff814c5226d5386aa2af7064c2c02788594a25fdd709358132eea fix: _PROPOSAL_PREFIX_RESOLVE_LIMIT 200 → 100 to match hub … Sonnet 4.6 19 days ago
sha256:36c3cb3e76619d4c30a6d9bf81b5ec4ff148e30dcfed913e3114ca7b43b81c7e fix: rename objects→blobs in push client and all stale test… Sonnet 4.6 patch 22 days ago
sha256:c06a9b9b9fee26c68ea725b44d54b2c0a171301ce9de746d5b656617b4463a9a fix: repair four test failures from post-migration audit Sonnet 4.6 patch 28 days ago
sha256:1900655993c83c4107067375548a7be823e471d2515830842f1a12cba4bd3cdf fix: unified object store migration — idempotent writes, JS… Sonnet 4.6 minor 29 days ago