test_symbol_anchor_parser.py
python
sha256:3ff9c9863a9891bdcde71b4a43228f66d0493e38b7cc1d09fe9eb7de774046b2
feat: add repair-commit wire endpoint (API parity with repa…
Opus 4.8
minor
⚠ breaking
1 day ago
| 1 | """TDD: symbol anchor parser — same-repo, cross-repo, and ref-pinned anchors. |
| 2 | |
| 3 | Covers musehub.services.symbol_anchor: |
| 4 | parse_symbol_anchor(raw: str) -> SymbolAnchorRef |
| 5 | |
| 6 | Anchor grammar |
| 7 | -------------- |
| 8 | anchor := cross_repo_anchor | same_repo_anchor |
| 9 | cross_anchor := owner '/' repo '::' file_path ('::' symbol)? ('@' ref)? |
| 10 | same_anchor := file_path ('::' symbol)? ('@' ref)? |
| 11 | |
| 12 | owner/repo — exactly one '/', no dots, no sub-slashes |
| 13 | file_path — any path (may contain '/', must include extension or multiple |
| 14 | path components distinguishing it from an owner/repo slug) |
| 15 | symbol — function/class name, may contain dots (e.g. 'Cls.method') |
| 16 | ref — branch name OR 'sha256:<64hex>' |
| 17 | |
| 18 | blob_url() contract |
| 19 | ------------------- |
| 20 | same-repo anchor → /{default_owner}/{default_repo}/blob/{ref}/{file}#{S:sym} |
| 21 | cross-repo anchor → /{owner}/{repo}/blob/{ref}/{file}#{S:sym} |
| 22 | ref defaults to 'main' when not specified |
| 23 | |
| 24 | Unit tests (pure function, no DB): |
| 25 | test_same_repo_file_only |
| 26 | test_same_repo_file_and_symbol |
| 27 | test_same_repo_with_branch_ref |
| 28 | test_same_repo_with_commit_ref |
| 29 | test_cross_repo_file_only |
| 30 | test_cross_repo_file_and_symbol |
| 31 | test_cross_repo_with_branch_ref |
| 32 | test_cross_repo_with_commit_ref |
| 33 | test_cross_repo_deeply_nested_file |
| 34 | test_symbol_with_dots_preserved |
| 35 | test_file_with_extension_not_mistaken_for_owner_repo |
| 36 | test_deeply_nested_path_not_mistaken_for_owner_repo |
| 37 | test_is_cross_repo_flag |
| 38 | blob_url tests: |
| 39 | test_blob_url_same_repo_file_only |
| 40 | test_blob_url_same_repo_with_symbol |
| 41 | test_blob_url_cross_repo_with_symbol |
| 42 | test_blob_url_uses_explicit_ref |
| 43 | test_blob_url_cross_repo_uses_own_ref |
| 44 | test_blob_url_default_ref_is_main |
| 45 | display_label tests: |
| 46 | test_display_label_same_repo |
| 47 | test_display_label_cross_repo_shows_owner_repo_prefix |
| 48 | test_display_label_cross_repo_with_ref |
| 49 | """ |
| 50 | from __future__ import annotations |
| 51 | |
| 52 | import pytest |
| 53 | |
| 54 | from musehub.services.symbol_anchor import SymbolAnchorRef, parse_symbol_anchor |
| 55 | |
| 56 | |
| 57 | # --------------------------------------------------------------------------- |
| 58 | # Helpers |
| 59 | # --------------------------------------------------------------------------- |
| 60 | |
| 61 | BASE = "https://staging.musehub.ai" |
| 62 | OWN_OWNER = "gabriel" |
| 63 | OWN_REPO = "musehub" |
| 64 | |
| 65 | |
| 66 | def _url(raw: str, ref: str = "main") -> str: |
| 67 | return parse_symbol_anchor(raw).blob_url(BASE, OWN_OWNER, OWN_REPO, default_ref=ref) |
| 68 | |
| 69 | |
| 70 | # --------------------------------------------------------------------------- |
| 71 | # Parsing — same-repo anchors |
| 72 | # --------------------------------------------------------------------------- |
| 73 | |
| 74 | |
| 75 | def test_same_repo_file_only() -> None: |
| 76 | a = parse_symbol_anchor("musehub/services/billing.py") |
| 77 | assert a.owner is None |
| 78 | assert a.repo is None |
| 79 | assert a.file_path == "musehub/services/billing.py" |
| 80 | assert a.symbol_name is None |
| 81 | assert a.ref is None |
| 82 | |
| 83 | |
| 84 | def test_same_repo_file_and_symbol() -> None: |
| 85 | a = parse_symbol_anchor("musehub/services/billing.py::compute_total") |
| 86 | assert a.owner is None |
| 87 | assert a.file_path == "musehub/services/billing.py" |
| 88 | assert a.symbol_name == "compute_total" |
| 89 | assert a.ref is None |
| 90 | |
| 91 | |
| 92 | def test_same_repo_with_branch_ref() -> None: |
| 93 | a = parse_symbol_anchor("musehub/services/billing.py::compute_total@dev") |
| 94 | assert a.owner is None |
| 95 | assert a.file_path == "musehub/services/billing.py" |
| 96 | assert a.symbol_name == "compute_total" |
| 97 | assert a.ref == "dev" |
| 98 | |
| 99 | |
| 100 | def test_same_repo_with_commit_ref() -> None: |
| 101 | ref = "sha256:fa0025329d55487de25035b1122b17517bf88e234f5a8f697111087fc785e81c" |
| 102 | a = parse_symbol_anchor(f"muse/cli/commands/bridge.py::GitExporter._has_shebang@{ref}") |
| 103 | assert a.owner is None |
| 104 | assert a.file_path == "muse/cli/commands/bridge.py" |
| 105 | assert a.symbol_name == "GitExporter._has_shebang" |
| 106 | assert a.ref == ref |
| 107 | |
| 108 | |
| 109 | def test_same_repo_file_only_with_ref() -> None: |
| 110 | a = parse_symbol_anchor("src/main.py@feat/my-branch") |
| 111 | assert a.file_path == "src/main.py" |
| 112 | assert a.symbol_name is None |
| 113 | assert a.ref == "feat/my-branch" |
| 114 | |
| 115 | |
| 116 | # --------------------------------------------------------------------------- |
| 117 | # Parsing — cross-repo anchors |
| 118 | # --------------------------------------------------------------------------- |
| 119 | |
| 120 | |
| 121 | def test_cross_repo_file_only() -> None: |
| 122 | a = parse_symbol_anchor("gabriel/muse::muse/core/snapshot.py") |
| 123 | assert a.owner == "gabriel" |
| 124 | assert a.repo == "muse" |
| 125 | assert a.file_path == "muse/core/snapshot.py" |
| 126 | assert a.symbol_name is None |
| 127 | assert a.ref is None |
| 128 | |
| 129 | |
| 130 | def test_cross_repo_file_and_symbol() -> None: |
| 131 | a = parse_symbol_anchor("gabriel/muse::muse/cli/commands/bridge.py::GitExporter.fix_file_modes") |
| 132 | assert a.owner == "gabriel" |
| 133 | assert a.repo == "muse" |
| 134 | assert a.file_path == "muse/cli/commands/bridge.py" |
| 135 | assert a.symbol_name == "GitExporter.fix_file_modes" |
| 136 | assert a.ref is None |
| 137 | |
| 138 | |
| 139 | def test_cross_repo_with_branch_ref() -> None: |
| 140 | a = parse_symbol_anchor("gabriel/muse::muse/cli/commands/bridge.py::GitExporter._has_shebang@dev") |
| 141 | assert a.owner == "gabriel" |
| 142 | assert a.repo == "muse" |
| 143 | assert a.file_path == "muse/cli/commands/bridge.py" |
| 144 | assert a.symbol_name == "GitExporter._has_shebang" |
| 145 | assert a.ref == "dev" |
| 146 | |
| 147 | |
| 148 | def test_cross_repo_with_commit_ref() -> None: |
| 149 | sha = "sha256:fa0025329d55487de25035b1122b17517bf88e234f5a8f697111087fc785e81c" |
| 150 | a = parse_symbol_anchor(f"gabriel/muse::muse/cli/commands/bridge.py::GitExporter.fix_file_modes@{sha}") |
| 151 | assert a.owner == "gabriel" |
| 152 | assert a.ref == sha |
| 153 | |
| 154 | |
| 155 | def test_cross_repo_deeply_nested_file() -> None: |
| 156 | a = parse_symbol_anchor("acme/infra::deploy/paperclip/scripts/install.sh::main") |
| 157 | assert a.owner == "acme" |
| 158 | assert a.repo == "infra" |
| 159 | assert a.file_path == "deploy/paperclip/scripts/install.sh" |
| 160 | assert a.symbol_name == "main" |
| 161 | |
| 162 | |
| 163 | # --------------------------------------------------------------------------- |
| 164 | # Disambiguation — file paths must not be confused with owner/repo slugs |
| 165 | # --------------------------------------------------------------------------- |
| 166 | |
| 167 | |
| 168 | def test_file_with_extension_not_mistaken_for_owner_repo() -> None: |
| 169 | """'src/billing.py' has a dot in the second segment → same-repo file.""" |
| 170 | a = parse_symbol_anchor("src/billing.py::compute_total") |
| 171 | assert a.owner is None |
| 172 | assert a.file_path == "src/billing.py" |
| 173 | |
| 174 | |
| 175 | def test_deeply_nested_path_not_mistaken_for_owner_repo() -> None: |
| 176 | """'muse/cli/commands/bridge.py' has multiple slashes → same-repo file.""" |
| 177 | a = parse_symbol_anchor("muse/cli/commands/bridge.py::GitExporter.fix_file_modes") |
| 178 | assert a.owner is None |
| 179 | assert a.file_path == "muse/cli/commands/bridge.py" |
| 180 | |
| 181 | |
| 182 | def test_single_segment_file_is_same_repo() -> None: |
| 183 | a = parse_symbol_anchor("README.md") |
| 184 | assert a.owner is None |
| 185 | assert a.file_path == "README.md" |
| 186 | |
| 187 | |
| 188 | def test_symbol_with_dots_preserved() -> None: |
| 189 | """Method names like 'GitExporter.fix_file_modes' survive the split.""" |
| 190 | a = parse_symbol_anchor("gabriel/muse::muse/cli/commands/bridge.py::GitExporter.fix_file_modes") |
| 191 | assert a.symbol_name == "GitExporter.fix_file_modes" |
| 192 | |
| 193 | |
| 194 | # --------------------------------------------------------------------------- |
| 195 | # is_cross_repo flag |
| 196 | # --------------------------------------------------------------------------- |
| 197 | |
| 198 | |
| 199 | def test_is_cross_repo_true_for_cross_repo_anchor() -> None: |
| 200 | a = parse_symbol_anchor("gabriel/muse::muse/cli/commands/bridge.py::GitExporter.fix_file_modes") |
| 201 | assert a.is_cross_repo is True |
| 202 | |
| 203 | |
| 204 | def test_is_cross_repo_false_for_same_repo_anchor() -> None: |
| 205 | a = parse_symbol_anchor("musehub/services/billing.py::compute_total") |
| 206 | assert a.is_cross_repo is False |
| 207 | |
| 208 | |
| 209 | # --------------------------------------------------------------------------- |
| 210 | # blob_url() |
| 211 | # --------------------------------------------------------------------------- |
| 212 | |
| 213 | |
| 214 | def test_blob_url_same_repo_file_only() -> None: |
| 215 | url = _url("src/main.py") |
| 216 | assert url == f"{BASE}/{OWN_OWNER}/{OWN_REPO}/blob/main/src/main.py" |
| 217 | |
| 218 | |
| 219 | def test_blob_url_same_repo_with_symbol() -> None: |
| 220 | url = _url("musehub/services/billing.py::compute_total") |
| 221 | assert url == f"{BASE}/{OWN_OWNER}/{OWN_REPO}/blob/main/musehub/services/billing.py#S:compute_total" |
| 222 | |
| 223 | |
| 224 | def test_blob_url_cross_repo_with_symbol() -> None: |
| 225 | url = _url("gabriel/muse::muse/cli/commands/bridge.py::GitExporter.fix_file_modes") |
| 226 | assert url == f"{BASE}/gabriel/muse/blob/main/muse/cli/commands/bridge.py#S:GitExporter.fix_file_modes" |
| 227 | |
| 228 | |
| 229 | def test_blob_url_uses_explicit_ref_same_repo() -> None: |
| 230 | url = _url("src/main.py::Fn@dev") |
| 231 | assert url == f"{BASE}/{OWN_OWNER}/{OWN_REPO}/blob/dev/src/main.py#S:Fn" |
| 232 | |
| 233 | |
| 234 | def test_blob_url_cross_repo_uses_own_ref() -> None: |
| 235 | url = _url("gabriel/muse::muse/cli/commands/bridge.py::GitExporter._has_shebang@dev") |
| 236 | assert url == f"{BASE}/gabriel/muse/blob/dev/muse/cli/commands/bridge.py#S:GitExporter._has_shebang" |
| 237 | |
| 238 | |
| 239 | def test_blob_url_default_ref_is_main() -> None: |
| 240 | url = parse_symbol_anchor("src/x.py::Fn").blob_url(BASE, OWN_OWNER, OWN_REPO) |
| 241 | assert "/blob/main/" in url |
| 242 | |
| 243 | |
| 244 | def test_blob_url_commit_ref_in_url() -> None: |
| 245 | sha = "sha256:fa0025329d55487de25035b1122b17517bf88e234f5a8f697111087fc785e81c" |
| 246 | url = _url(f"gabriel/muse::muse/cli/commands/bridge.py::GitExporter.fix_file_modes@{sha}") |
| 247 | assert f"/blob/{sha}/" in url |
| 248 | |
| 249 | |
| 250 | # --------------------------------------------------------------------------- |
| 251 | # display_label() |
| 252 | # --------------------------------------------------------------------------- |
| 253 | |
| 254 | |
| 255 | def test_display_label_same_repo_file_and_symbol() -> None: |
| 256 | a = parse_symbol_anchor("musehub/services/billing.py::compute_total") |
| 257 | assert a.display_label() == "musehub/services/billing.py::compute_total" |
| 258 | |
| 259 | |
| 260 | def test_display_label_same_repo_file_only() -> None: |
| 261 | a = parse_symbol_anchor("src/main.py") |
| 262 | assert a.display_label() == "src/main.py" |
| 263 | |
| 264 | |
| 265 | def test_display_label_cross_repo_shows_prefix() -> None: |
| 266 | a = parse_symbol_anchor("gabriel/muse::muse/cli/commands/bridge.py::GitExporter.fix_file_modes") |
| 267 | label = a.display_label() |
| 268 | assert label.startswith("gabriel/muse::") |
| 269 | assert "GitExporter.fix_file_modes" in label |
| 270 | |
| 271 | |
| 272 | def test_display_label_cross_repo_with_ref() -> None: |
| 273 | a = parse_symbol_anchor("gabriel/muse::muse/cli/commands/bridge.py::GitExporter._has_shebang@dev") |
| 274 | label = a.display_label() |
| 275 | assert "@dev" in label |
File History
1 commit
sha256:3ff9c9863a9891bdcde71b4a43228f66d0493e38b7cc1d09fe9eb7de774046b2
feat: add repair-commit wire endpoint (API parity with repa…
Opus 4.8
minor
⚠
1 day ago