gabriel / musehub public
test_musehub_ui_blob_deep_links.py python
228 lines 7.7 KB
Raw
sha256:bb2baaabdd19320bde50cb69d447fd1c1e571467df729be1a23b5e93064b046a feat(intel): standardize headers, gauge icon, velocity card… Sonnet 4.6 minor ⚠ breaking 143 days ago
1 """Tests for blob deep-link infrastructure: symbol→lineno enrichment and #S: fragments.
2
3 Covers:
4 _enrich_with_linenos (unit)
5 - test_enrich_adds_lineno_for_known_function
6 - test_enrich_adds_lineno_for_class
7 - test_enrich_skips_non_python_files
8 - test_enrich_does_not_overwrite_existing_lineno
9 - test_enrich_tolerates_syntax_error
10 - test_enrich_ignores_symbol_not_in_ast
11
12 _symbol_line_map (unit)
13 - test_symbol_line_map_returns_display_name_to_lineno
14 - test_symbol_line_map_excludes_symbols_without_lineno
15 - test_symbol_line_map_excludes_falsy_lineno
16
17 Issue detail SSR — #S: deep links
18 - test_issue_detail_symbol_anchor_link_contains_hash_fragment
19 - test_issue_detail_symbol_anchor_plain_file_no_fragment
20 - test_issue_detail_multiple_symbol_anchors_all_linked
21
22 Blob page SSR — symbolLines in page_json
23 - test_blob_page_json_contains_symbol_lines_key
24 """
25 from __future__ import annotations
26
27 import pytest
28 from httpx import AsyncClient
29 from sqlalchemy.ext.asyncio import AsyncSession
30
31 from datetime import datetime, timezone
32
33 from musehub.api.routes.musehub.ui_blob import _enrich_with_linenos, _symbol_line_map
34 from musehub.core.genesis import compute_identity_id, compute_issue_id, compute_repo_id
35 from musehub.db.musehub_models import MusehubIssue, MusehubRepo
36 from musehub.types.json_types import JSONObject, StrDict
37
38
39 # ---------------------------------------------------------------------------
40 # Helpers
41 # ---------------------------------------------------------------------------
42
43 _PY_SRC = """\
44 class MyService:
45 def run(self) -> None:
46 pass
47
48 async def compute(x: int) -> int:
49 return x * 2
50 """
51
52
53 async def _make_repo(db: AsyncSession, owner: str = "blober", slug: str = "blobby") -> str:
54 owner_id = compute_identity_id(owner.encode())
55 created_at = datetime.now(tz=timezone.utc)
56 repo = MusehubRepo(
57 repo_id=compute_repo_id(owner_id, slug, "code", created_at.isoformat()),
58 name=slug,
59 owner=owner,
60 slug=slug,
61 visibility="public",
62 owner_user_id=owner_id,
63 created_at=created_at,
64 updated_at=created_at,
65 )
66 db.add(repo)
67 await db.commit()
68 await db.refresh(repo)
69 return str(repo.repo_id)
70
71
72 async def _make_issue(
73 db: AsyncSession,
74 repo_id: str,
75 *,
76 number: int = 1,
77 title: str = "Deep link test issue",
78 symbol_anchors: list[str] | None = None,
79 ) -> MusehubIssue:
80 author_id = compute_identity_id(b"blober")
81 issue = MusehubIssue(
82 issue_id=compute_issue_id(repo_id, author_id, datetime.now(tz=timezone.utc).isoformat()),
83 repo_id=repo_id,
84 number=number,
85 title=title,
86 body="",
87 state="open",
88 labels=[],
89 author="blober",
90 symbol_anchors=symbol_anchors or [],
91 )
92 db.add(issue)
93 await db.commit()
94 await db.refresh(issue)
95 return issue
96
97
98 # ---------------------------------------------------------------------------
99 # Unit: _enrich_with_linenos
100 # ---------------------------------------------------------------------------
101
102
103 def test_enrich_adds_lineno_for_known_function() -> None:
104 syms: list[JSONObject] = [{"display_name": "compute"}]
105 _enrich_with_linenos(syms, "service.py", _PY_SRC)
106 assert syms[0]["lineno"] == 5
107
108
109 def test_enrich_adds_lineno_for_class() -> None:
110 syms: list[JSONObject] = [{"display_name": "MyService"}]
111 _enrich_with_linenos(syms, "service.py", _PY_SRC)
112 assert syms[0]["lineno"] == 1
113
114
115 def test_enrich_skips_non_python_files() -> None:
116 syms: list[JSONObject] = [{"display_name": "compute"}]
117 _enrich_with_linenos(syms, "service.ts", _PY_SRC)
118 assert "lineno" not in syms[0]
119
120
121 def test_enrich_does_not_overwrite_existing_lineno() -> None:
122 syms: list[JSONObject] = [{"display_name": "compute", "lineno": 99}]
123 _enrich_with_linenos(syms, "service.py", _PY_SRC)
124 assert syms[0]["lineno"] == 99
125
126
127 def test_enrich_tolerates_syntax_error() -> None:
128 syms: list[JSONObject] = [{"display_name": "compute"}]
129 _enrich_with_linenos(syms, "bad.py", "def compute(: pass")
130 assert "lineno" not in syms[0]
131
132
133 def test_enrich_ignores_symbol_not_in_ast() -> None:
134 syms: list[JSONObject] = [{"display_name": "ghost_fn"}]
135 _enrich_with_linenos(syms, "service.py", _PY_SRC)
136 assert "lineno" not in syms[0]
137
138
139 # ---------------------------------------------------------------------------
140 # Unit: _symbol_line_map
141 # ---------------------------------------------------------------------------
142
143
144 def test_symbol_line_map_returns_display_name_to_lineno() -> None:
145 syms: list[JSONObject] = [
146 {"display_name": "foo", "lineno": 3, "end_lineno": 8},
147 {"display_name": "bar", "lineno": 10, "end_lineno": 20},
148 ]
149 result = _symbol_line_map(syms)
150 assert result == {"foo": [3, 8], "bar": [10, 20]}
151
152
153 def test_symbol_line_map_excludes_symbols_without_lineno() -> None:
154 syms: list[JSONObject] = [
155 {"display_name": "foo", "lineno": 3, "end_lineno": 5},
156 {"display_name": "no_line"},
157 ]
158 result = _symbol_line_map(syms)
159 assert "no_line" not in result
160 assert result["foo"] == [3, 5]
161
162
163 def test_symbol_line_map_excludes_falsy_lineno() -> None:
164 syms: list[JSONObject] = [{"display_name": "zero", "lineno": 0}]
165 result = _symbol_line_map(syms)
166 assert "zero" not in result
167
168
169 # ---------------------------------------------------------------------------
170 # Issue detail SSR — #S: deep links
171 # ---------------------------------------------------------------------------
172
173
174 async def test_issue_detail_symbol_anchor_link_contains_hash_fragment(
175 client: AsyncClient, db_session: AsyncSession, auth_headers: StrDict
176 ) -> None:
177 repo_id = await _make_repo(db_session)
178 await _make_issue(db_session, repo_id, symbol_anchors=["musehub/services/foo.py::compute"])
179 r = await client.get("/blober/blobby/issues/1")
180 assert r.status_code == 200
181 assert "#S:compute" in r.text
182
183
184 async def test_issue_detail_symbol_anchor_plain_file_no_fragment(
185 client: AsyncClient, db_session: AsyncSession, auth_headers: StrDict
186 ) -> None:
187 repo_id = await _make_repo(db_session, slug="blobby2")
188 await _make_issue(db_session, repo_id, number=1, symbol_anchors=["musehub/services/bar.py"])
189 r = await client.get("/blober/blobby2/issues/1")
190 assert r.status_code == 200
191 # plain file anchor — no #S: fragment, just the blob URL
192 assert "blob/main/musehub/services/bar.py" in r.text
193 assert "#S:" not in r.text
194
195
196 async def test_issue_detail_multiple_symbol_anchors_all_linked(
197 client: AsyncClient, db_session: AsyncSession, auth_headers: StrDict
198 ) -> None:
199 repo_id = await _make_repo(db_session, slug="blobby3")
200 await _make_issue(
201 db_session,
202 repo_id,
203 symbol_anchors=[
204 "musehub/services/a.py::Alpha",
205 "musehub/services/b.py::Beta",
206 ],
207 )
208 r = await client.get("/blober/blobby3/issues/1")
209 assert r.status_code == 200
210 assert "#S:Alpha" in r.text
211 assert "#S:Beta" in r.text
212
213
214 # ---------------------------------------------------------------------------
215 # Blob page SSR — symbolLines key present in page_json
216 # ---------------------------------------------------------------------------
217
218
219 async def test_blob_page_json_contains_symbol_lines_key(
220 client: AsyncClient, db_session: AsyncSession
221 ) -> None:
222 """Even when no file is found the blob template must emit symbolLines in page_json."""
223 await _make_repo(db_session, owner="blober", slug="blobby4")
224 r = await client.get("/blober/blobby4/blob/main/some/file.py")
225 # May 200 or 404 depending on whether the file exists, but the template always renders.
226 # We only care that the response contains "symbolLines" in the page_json script block.
227 assert r.status_code in (200, 404)
228 assert '"symbolLines"' in r.text
File History 1 commit
sha256:bb2baaabdd19320bde50cb69d447fd1c1e571467df729be1a23b5e93064b046a feat(intel): standardize headers, gauge icon, velocity card… Sonnet 4.6 minor 143 days ago