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