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