gabriel / musehub public
test_musehub_issues_input_limits.py python
350 lines 13.2 KB
Raw
sha256:3ff9c9863a9891bdcde71b4a43228f66d0493e38b7cc1d09fe9eb7de774046b2 feat: add repair-commit wire endpoint (API parity with repa… Opus 4.8 minor ⚠ breaking 1 day ago
1 """Tests for input size limits on issue and comment write endpoints.
2
3 Verifies that Pydantic validation enforces the goldilocks limits designed for
4 agent-swarm use while preventing abuse:
5
6 body: max 50,000 chars (bumped from 10k for agent-written issues)
7 title: max 500 chars
8 labels: max 20 items, each max 100 chars
9 symbol_anchors: max 50 items, each max 500 chars
10 commit_anchors: max 50 items, each max 71 chars (sha256:<64-hex> canonical form)
11 comment body: max 50,000 chars
12
13 All limits are enforced at the Pydantic layer (422 before the DB is touched).
14 """
15 from __future__ import annotations
16
17 import pytest
18 from httpx import AsyncClient
19
20 from muse.core.types import long_id
21 from musehub.types.json_types import StrDict
22
23
24 # ---------------------------------------------------------------------------
25 # Helpers
26 # ---------------------------------------------------------------------------
27
28
29 async def _create_repo(client: AsyncClient, headers: StrDict, name: str) -> str:
30 r = await client.post("/api/repos", json={"name": name, "owner": "testuser"}, headers=headers)
31 assert r.status_code == 201
32 return r.json()["repoId"]
33
34
35 async def _create_issue(client: AsyncClient, headers: StrDict, repo_id: str, **kwargs: str | int | bool | None) -> int:
36 payload = {"title": "baseline", "body": "", **kwargs}
37 r = await client.post(f"/api/repos/{repo_id}/issues", json=payload, headers=headers)
38 assert r.status_code == 201
39 return r.json()["number"]
40
41
42 # ---------------------------------------------------------------------------
43 # IssueCreate — body
44 # ---------------------------------------------------------------------------
45
46
47 async def test_issue_body_at_limit_accepted(client: AsyncClient, auth_headers: StrDict) -> None:
48 repo_id = await _create_repo(client, auth_headers, "il-body-ok")
49 r = await client.post(
50 f"/api/repos/{repo_id}/issues",
51 json={"title": "t", "body": "x" * 50_000},
52 headers=auth_headers,
53 )
54 assert r.status_code == 201
55
56
57 async def test_issue_body_over_limit_rejected(client: AsyncClient, auth_headers: StrDict) -> None:
58 repo_id = await _create_repo(client, auth_headers, "il-body-over")
59 r = await client.post(
60 f"/api/repos/{repo_id}/issues",
61 json={"title": "t", "body": "x" * 50_001},
62 headers=auth_headers,
63 )
64 assert r.status_code == 422
65
66
67 # ---------------------------------------------------------------------------
68 # IssueCreate — title
69 # ---------------------------------------------------------------------------
70
71
72 async def test_issue_title_at_limit_accepted(client: AsyncClient, auth_headers: StrDict) -> None:
73 repo_id = await _create_repo(client, auth_headers, "il-title-ok")
74 r = await client.post(
75 f"/api/repos/{repo_id}/issues",
76 json={"title": "t" * 500},
77 headers=auth_headers,
78 )
79 assert r.status_code == 201
80
81
82 async def test_issue_title_over_limit_rejected(client: AsyncClient, auth_headers: StrDict) -> None:
83 repo_id = await _create_repo(client, auth_headers, "il-title-over")
84 r = await client.post(
85 f"/api/repos/{repo_id}/issues",
86 json={"title": "t" * 501},
87 headers=auth_headers,
88 )
89 assert r.status_code == 422
90
91
92 # ---------------------------------------------------------------------------
93 # IssueCreate — labels
94 # ---------------------------------------------------------------------------
95
96
97 async def test_issue_labels_at_limit_accepted(client: AsyncClient, auth_headers: StrDict) -> None:
98 repo_id = await _create_repo(client, auth_headers, "il-labels-ok")
99 r = await client.post(
100 f"/api/repos/{repo_id}/issues",
101 json={"title": "t", "labels": [f"label-{i}" for i in range(20)]},
102 headers=auth_headers,
103 )
104 assert r.status_code == 201
105
106
107 async def test_issue_labels_too_many_rejected(client: AsyncClient, auth_headers: StrDict) -> None:
108 repo_id = await _create_repo(client, auth_headers, "il-labels-over")
109 r = await client.post(
110 f"/api/repos/{repo_id}/issues",
111 json={"title": "t", "labels": [f"label-{i}" for i in range(21)]},
112 headers=auth_headers,
113 )
114 assert r.status_code == 422
115
116
117 async def test_issue_label_item_too_long_rejected(client: AsyncClient, auth_headers: StrDict) -> None:
118 repo_id = await _create_repo(client, auth_headers, "il-label-item")
119 r = await client.post(
120 f"/api/repos/{repo_id}/issues",
121 json={"title": "t", "labels": ["x" * 101]},
122 headers=auth_headers,
123 )
124 assert r.status_code == 422
125
126
127 async def test_issue_label_item_at_limit_accepted(client: AsyncClient, auth_headers: StrDict) -> None:
128 repo_id = await _create_repo(client, auth_headers, "il-label-item-ok")
129 r = await client.post(
130 f"/api/repos/{repo_id}/issues",
131 json={"title": "t", "labels": ["x" * 100]},
132 headers=auth_headers,
133 )
134 assert r.status_code == 201
135
136
137 # ---------------------------------------------------------------------------
138 # IssueCreate — symbol_anchors
139 # ---------------------------------------------------------------------------
140
141
142 async def test_issue_symbol_anchors_at_limit_accepted(client: AsyncClient, auth_headers: StrDict) -> None:
143 repo_id = await _create_repo(client, auth_headers, "il-sym-ok")
144 anchors = [f"path/to/file.py::Symbol{i}" for i in range(50)]
145 r = await client.post(
146 f"/api/repos/{repo_id}/issues",
147 json={"title": "t", "symbolAnchors": anchors},
148 headers=auth_headers,
149 )
150 assert r.status_code == 201
151
152
153 async def test_issue_symbol_anchors_too_many_rejected(client: AsyncClient, auth_headers: StrDict) -> None:
154 repo_id = await _create_repo(client, auth_headers, "il-sym-over")
155 anchors = [f"path/to/file.py::Symbol{i}" for i in range(51)]
156 r = await client.post(
157 f"/api/repos/{repo_id}/issues",
158 json={"title": "t", "symbolAnchors": anchors},
159 headers=auth_headers,
160 )
161 assert r.status_code == 422
162
163
164 async def test_issue_symbol_anchor_item_too_long_rejected(client: AsyncClient, auth_headers: StrDict) -> None:
165 repo_id = await _create_repo(client, auth_headers, "il-sym-item")
166 r = await client.post(
167 f"/api/repos/{repo_id}/issues",
168 json={"title": "t", "symbolAnchors": ["x" * 501]},
169 headers=auth_headers,
170 )
171 assert r.status_code == 422
172
173
174 # ---------------------------------------------------------------------------
175 # IssueCreate — commit_anchors
176 # ---------------------------------------------------------------------------
177
178
179 async def test_issue_commit_anchor_canonical_prefix_accepted(client: AsyncClient, auth_headers: StrDict) -> None:
180 """sha256:<64-hex> is the canonical commit ID format (71 chars) and must be accepted."""
181 repo_id = await _create_repo(client, auth_headers, "il-commit-canonical")
182 import secrets
183 hex64 = secrets.token_hex(32)
184 canonical = long_id(hex64)
185 assert len(canonical) == 71
186 r = await client.post(
187 f"/api/repos/{repo_id}/issues",
188 json={"title": "t", "commitAnchors": [canonical]},
189 headers=auth_headers,
190 )
191 assert r.status_code == 201, (
192 f"Canonical sha256:<64-hex> commit anchor (71 chars) was rejected with {r.status_code}: {r.text}. "
193 "The max_length for commit anchors must be 71 to fit the 'sha256:' prefix."
194 )
195
196
197 async def test_issue_commit_anchors_at_limit_accepted(client: AsyncClient, auth_headers: StrDict) -> None:
198 repo_id = await _create_repo(client, auth_headers, "il-commit-ok")
199 import secrets
200 anchors = [secrets.token_hex(32) for _ in range(50)]
201 r = await client.post(
202 f"/api/repos/{repo_id}/issues",
203 json={"title": "t", "commitAnchors": anchors},
204 headers=auth_headers,
205 )
206 assert r.status_code == 201
207
208
209 async def test_issue_commit_anchors_too_many_rejected(client: AsyncClient, auth_headers: StrDict) -> None:
210 repo_id = await _create_repo(client, auth_headers, "il-commit-over")
211 import secrets
212 anchors = [secrets.token_hex(32) for _ in range(51)]
213 r = await client.post(
214 f"/api/repos/{repo_id}/issues",
215 json={"title": "t", "commitAnchors": anchors},
216 headers=auth_headers,
217 )
218 assert r.status_code == 422
219
220
221 async def test_issue_commit_anchor_item_too_long_rejected(client: AsyncClient, auth_headers: StrDict) -> None:
222 repo_id = await _create_repo(client, auth_headers, "il-commit-item")
223 r = await client.post(
224 f"/api/repos/{repo_id}/issues",
225 json={"title": "t", "commitAnchors": ["a" * 72]},
226 headers=auth_headers,
227 )
228 assert r.status_code == 422
229
230
231 # ---------------------------------------------------------------------------
232 # IssueCommentCreate — body
233 # ---------------------------------------------------------------------------
234
235
236 async def test_comment_body_at_limit_accepted(client: AsyncClient, auth_headers: StrDict) -> None:
237 repo_id = await _create_repo(client, auth_headers, "il-cmt-ok")
238 number = await _create_issue(client, auth_headers, repo_id)
239 r = await client.post(
240 f"/api/repos/{repo_id}/issues/{number}/comments",
241 json={"body": "x" * 50_000},
242 headers=auth_headers,
243 )
244 assert r.status_code == 201
245
246
247 async def test_create_comment_returns_single_resource(client: AsyncClient, auth_headers: StrDict) -> None:
248 """POST .../comments must return the created comment as a flat single resource, not a list."""
249 repo_id = await _create_repo(client, auth_headers, "il-cmt-shape")
250 number = await _create_issue(client, auth_headers, repo_id)
251 r = await client.post(
252 f"/api/repos/{repo_id}/issues/{number}/comments",
253 json={"body": "hello world"},
254 headers=auth_headers,
255 )
256 assert r.status_code == 201
257 data = r.json()
258 # Must be a flat resource — not a list envelope.
259 assert "commentId" in data, f"expected 'commentId' key, got: {list(data.keys())}"
260 assert "comments" not in data, "response must not wrap in list envelope"
261 assert data["body"] == "hello world"
262 assert "author" in data
263 assert "createdAt" in data
264
265
266 async def test_comment_body_over_limit_rejected(client: AsyncClient, auth_headers: StrDict) -> None:
267 repo_id = await _create_repo(client, auth_headers, "il-cmt-over")
268 number = await _create_issue(client, auth_headers, repo_id)
269 r = await client.post(
270 f"/api/repos/{repo_id}/issues/{number}/comments",
271 json={"body": "x" * 50_001},
272 headers=auth_headers,
273 )
274 assert r.status_code == 422
275
276
277 async def test_comment_body_empty_rejected(client: AsyncClient, auth_headers: StrDict) -> None:
278 repo_id = await _create_repo(client, auth_headers, "il-cmt-empty")
279 number = await _create_issue(client, auth_headers, repo_id)
280 r = await client.post(
281 f"/api/repos/{repo_id}/issues/{number}/comments",
282 json={"body": ""},
283 headers=auth_headers,
284 )
285 assert r.status_code == 422
286
287
288 # ---------------------------------------------------------------------------
289 # IssueLabelAssignRequest
290 # ---------------------------------------------------------------------------
291
292
293 async def test_label_assign_too_many_rejected(client: AsyncClient, auth_headers: StrDict) -> None:
294 repo_id = await _create_repo(client, auth_headers, "il-lbl-assign")
295 number = await _create_issue(client, auth_headers, repo_id)
296 r = await client.post(
297 f"/api/repos/{repo_id}/issues/{number}/labels",
298 json={"labels": [f"lbl-{i}" for i in range(21)]},
299 headers=auth_headers,
300 )
301 assert r.status_code == 422
302
303
304 async def test_label_assign_item_too_long_rejected(client: AsyncClient, auth_headers: StrDict) -> None:
305 repo_id = await _create_repo(client, auth_headers, "il-lbl-item")
306 number = await _create_issue(client, auth_headers, repo_id)
307 r = await client.post(
308 f"/api/repos/{repo_id}/issues/{number}/labels",
309 json={"labels": ["x" * 101]},
310 headers=auth_headers,
311 )
312 assert r.status_code == 422
313
314
315 # ---------------------------------------------------------------------------
316 # IssueUpdate — same limits apply on PATCH
317 # ---------------------------------------------------------------------------
318
319
320 async def test_update_body_over_limit_rejected(client: AsyncClient, auth_headers: StrDict) -> None:
321 repo_id = await _create_repo(client, auth_headers, "il-upd-body")
322 number = await _create_issue(client, auth_headers, repo_id)
323 r = await client.patch(
324 f"/api/repos/{repo_id}/issues/{number}",
325 json={"body": "x" * 50_001},
326 headers=auth_headers,
327 )
328 assert r.status_code == 422
329
330
331 async def test_update_labels_too_many_rejected(client: AsyncClient, auth_headers: StrDict) -> None:
332 repo_id = await _create_repo(client, auth_headers, "il-upd-labels")
333 number = await _create_issue(client, auth_headers, repo_id)
334 r = await client.patch(
335 f"/api/repos/{repo_id}/issues/{number}",
336 json={"labels": [f"l{i}" for i in range(21)]},
337 headers=auth_headers,
338 )
339 assert r.status_code == 422
340
341
342 async def test_update_symbol_anchors_too_many_rejected(client: AsyncClient, auth_headers: StrDict) -> None:
343 repo_id = await _create_repo(client, auth_headers, "il-upd-sym")
344 number = await _create_issue(client, auth_headers, repo_id)
345 r = await client.patch(
346 f"/api/repos/{repo_id}/issues/{number}",
347 json={"symbolAnchors": [f"f.py::S{i}" for i in range(51)]},
348 headers=auth_headers,
349 )
350 assert r.status_code == 422
File History 1 commit
sha256:3ff9c9863a9891bdcde71b4a43228f66d0493e38b7cc1d09fe9eb7de774046b2 feat: add repair-commit wire endpoint (API parity with repa… Opus 4.8 minor 1 day ago