gabriel / musehub public
test_musehub_labels.py python
670 lines 23.8 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 MuseHub label management endpoints.
2
3 Covers all acceptance criteria:
4 - GET /repos/{repo_id}/labels — list labels (public)
5 - POST /repos/{repo_id}/labels — create label (auth required)
6 - PATCH /repos/{repo_id}/labels/{label_id} — update label (auth required)
7 - DELETE /repos/{repo_id}/labels/{label_id} — delete label (auth required)
8 - POST .../issues/{number}/labels — assign labels to issue (auth required)
9 - DELETE .../issues/{number}/labels/{label_id} — remove label from issue (auth required)
10 - POST .../proposals/{proposal_id}/labels — assign labels to proposal (auth required)
11 - DELETE .../proposals/{proposal_id}/labels/{label_id} — remove label from proposal (auth required)
12
13 All tests use the shared ``client``, ``auth_headers``, and ``db_session``
14 fixtures from conftest.py.
15 """
16 from __future__ import annotations
17
18 from datetime import datetime, timezone
19
20 import pytest
21 from httpx import AsyncClient
22 from sqlalchemy.ext.asyncio import AsyncSession
23
24 from muse.core.types import fake_id
25 from musehub.core.genesis import compute_branch_id
26 from musehub.db.musehub_repo_models import MusehubBranch, MusehubCommit, MusehubCommitRef
27 from musehub.types.json_types import JSONObject, StrDict
28
29
30 # ---------------------------------------------------------------------------
31 # Helpers
32 # ---------------------------------------------------------------------------
33
34
35 async def _create_repo(client: AsyncClient, auth_headers: StrDict, name: str = "label-test-repo") -> str:
36 """Create a repo and return its repo_id."""
37 response = await client.post(
38 "/api/repos",
39 json={"name": name, "owner": "testuser", "initialize": False},
40 headers=auth_headers,
41 )
42 assert response.status_code == 201
43 repo_id: str = response.json()["repoId"]
44 return repo_id
45
46
47 async def _create_label(
48 client: AsyncClient,
49 auth_headers: StrDict,
50 repo_id: str,
51 name: str = "test-label",
52 color: str = "#112233",
53 description: str | None = "A test label",
54 ) -> JSONObject:
55 """Create a label and return the response body."""
56 payload = {"name": name, "color": color}
57 if description is not None:
58 payload["description"] = description
59 response = await client.post(
60 f"/api/repos/{repo_id}/labels",
61 json=payload,
62 headers=auth_headers,
63 )
64 assert response.status_code == 201
65 label: JSONObject = response.json()
66 return label
67
68
69 async def _create_issue(
70 client: AsyncClient,
71 auth_headers: StrDict,
72 repo_id: str,
73 title: str = "Test issue",
74 ) -> JSONObject:
75 """Create an issue and return the response body."""
76 response = await client.post(
77 f"/api/repos/{repo_id}/issues",
78 json={"title": title, "body": "", "labels": []},
79 headers=auth_headers,
80 )
81 assert response.status_code == 201
82 issue: JSONObject = response.json()
83 return issue
84
85
86 async def _push_branch(db: AsyncSession, repo_id: str, branch_name: str) -> str:
87 """Insert a branch with one commit so the branch exists (required before creating a proposal)."""
88 commit_id = fake_id(f"{repo_id}{branch_name}")
89 commit = MusehubCommit(
90 commit_id=commit_id,
91 branch=branch_name,
92 parent_ids=[],
93 message=f"Initial commit on {branch_name}",
94 author="testuser",
95 timestamp=datetime.now(tz=timezone.utc),
96 )
97 branch = MusehubBranch(
98 branch_id=compute_branch_id(repo_id, branch_name),
99 repo_id=repo_id,
100 name=branch_name,
101 head_commit_id=commit_id,
102 )
103 db.add(commit)
104 db.add(MusehubCommitRef(repo_id=repo_id, commit_id=commit_id))
105 db.add(branch)
106 await db.commit()
107 return commit_id
108
109
110 async def _create_proposal(
111 client: AsyncClient,
112 auth_headers: StrDict,
113 repo_id: str,
114 title: str = "Test Proposal",
115 ) -> JSONObject:
116 """Create a proposal and return the response body."""
117 response = await client.post(
118 f"/api/repos/{repo_id}/proposals",
119 json={"title": title, "body": "", "fromBranch": "feature", "toBranch": "main"},
120 headers=auth_headers,
121 )
122 assert response.status_code == 201, response.text
123 proposal: JSONObject = response.json()
124 return proposal
125
126
127 # ---------------------------------------------------------------------------
128 # POST /repos/{repo_id}/labels
129 # ---------------------------------------------------------------------------
130
131
132 async def test_create_label_returns_201(
133 client: AsyncClient,
134 auth_headers: StrDict,
135 ) -> None:
136 """POST /labels creates a label and returns 201 with the label data."""
137 repo_id = await _create_repo(client, auth_headers, "create-label-repo")
138 label = await _create_label(client, auth_headers, repo_id)
139
140 assert label["name"] == "test-label"
141 assert label["color"] == "#112233"
142 assert label["description"] == "A test label"
143 assert "labelId" in label or "label_id" in label
144 assert label.get("repoId") == repo_id or label.get("repo_id") == repo_id
145
146
147 async def test_create_label_requires_auth(
148 client: AsyncClient,
149 ) -> None:
150 """POST /labels without auth returns 401."""
151 response = await client.post(
152 "/api/repos/nonexistent/labels",
153 json={"name": "bug", "color": "#d73a4a"},
154 )
155 assert response.status_code == 401
156
157
158 async def test_create_label_unknown_repo_returns_404(
159 client: AsyncClient,
160 auth_headers: StrDict,
161 ) -> None:
162 """POST /labels for a non-existent repo returns 404."""
163 response = await client.post(
164 "/api/repos/does-not-exist/labels",
165 json={"name": "bug", "color": "#d73a4a"},
166 headers=auth_headers,
167 )
168 assert response.status_code == 404
169
170
171 async def test_create_label_duplicate_name_returns_409(
172 client: AsyncClient,
173 auth_headers: StrDict,
174 ) -> None:
175 """POST /labels with a duplicate name returns 409 Conflict."""
176 repo_id = await _create_repo(client, auth_headers, "dupe-label-repo")
177 # "bug" is seeded by default on repo creation — creating it again yields 409.
178 response = await client.post(
179 f"/api/repos/{repo_id}/labels",
180 json={"name": "bug", "color": "#aabbcc"},
181 headers=auth_headers,
182 )
183 assert response.status_code == 409
184
185
186 async def test_create_label_invalid_color_returns_422(
187 client: AsyncClient,
188 auth_headers: StrDict,
189 ) -> None:
190 """POST /labels with an invalid colour format returns 422."""
191 repo_id = await _create_repo(client, auth_headers, "color-invalid-repo")
192 response = await client.post(
193 f"/api/repos/{repo_id}/labels",
194 json={"name": "bug", "color": "red"},
195 headers=auth_headers,
196 )
197 assert response.status_code == 422
198
199
200 # ---------------------------------------------------------------------------
201 # GET /repos/{repo_id}/labels
202 # ---------------------------------------------------------------------------
203
204
205 async def test_list_labels_public_access(
206 client: AsyncClient,
207 auth_headers: StrDict,
208 ) -> None:
209 """GET /labels is publicly accessible and returns all repo labels."""
210 repo_id = await _create_repo(client, auth_headers, "list-labels-repo")
211 # Seeded defaults already include "bug" and "enhancement"; just add one extra.
212 await _create_label(client, auth_headers, repo_id, name="custom-label", color="#123456")
213
214 # No auth headers — public endpoint.
215 response = await client.get(f"/api/repos/{repo_id}/labels")
216 assert response.status_code == 200
217 body = response.json()
218 assert "items" in body
219 assert body["total"] > 0
220 names = [item["name"] for item in body["items"]]
221 # Default-seeded labels must be present.
222 assert "bug" in names
223 assert "enhancement" in names
224 # The extra label we created must also be there.
225 assert "custom-label" in names
226
227
228 async def test_list_labels_unknown_repo_returns_404(
229 client: AsyncClient,
230 ) -> None:
231 """GET /labels for a non-existent repo returns 404."""
232 response = await client.get("/api/repos/no-such-repo/labels")
233 assert response.status_code == 404
234
235
236 async def test_list_labels_empty_repo(
237 client: AsyncClient,
238 auth_headers: StrDict,
239 ) -> None:
240 """GET /labels for a new repo returns the seeded default labels."""
241 repo_id = await _create_repo(client, auth_headers, "empty-labels-repo")
242 response = await client.get(f"/api/repos/{repo_id}/labels")
243 assert response.status_code == 200
244 body = response.json()
245 # Repos are seeded with default labels on creation — the list is never truly empty.
246 assert isinstance(body["items"], list)
247 assert body["total"] > 0
248 names = {lbl["name"] for lbl in body["items"]}
249 assert "bug" in names
250
251
252 # ---------------------------------------------------------------------------
253 # PATCH /repos/{repo_id}/labels/{label_id}
254 # ---------------------------------------------------------------------------
255
256
257 async def test_update_label_name(
258 client: AsyncClient,
259 auth_headers: StrDict,
260 ) -> None:
261 """PATCH /labels/{id} updates the label name."""
262 repo_id = await _create_repo(client, auth_headers, "update-label-repo")
263 label = await _create_label(client, auth_headers, repo_id, name="old-name", color="#aabbcc")
264 label_id = label.get("label_id") or label.get("labelId")
265
266 response = await client.patch(
267 f"/api/repos/{repo_id}/labels/{label_id}",
268 json={"name": "new-name"},
269 headers=auth_headers,
270 )
271 assert response.status_code == 200
272 assert response.json()["name"] == "new-name"
273 assert response.json()["color"] == "#aabbcc"
274
275
276 async def test_update_label_requires_auth(
277 client: AsyncClient,
278 auth_headers: StrDict,
279 ) -> None:
280 """PATCH /labels/{id} without auth returns 401."""
281 from musehub.auth.request_signing import optional_signed_request, require_signed_request
282 from musehub.main import app as _app
283
284 repo_id = await _create_repo(client, auth_headers, "update-auth-label-repo")
285 label = await _create_label(client, auth_headers, repo_id)
286 label_id = label.get("label_id") or label.get("labelId")
287
288 _app.dependency_overrides.pop(require_signed_request, None)
289 _app.dependency_overrides.pop(optional_signed_request, None)
290 response = await client.patch(
291 f"/api/repos/{repo_id}/labels/{label_id}",
292 json={"name": "hacked"},
293 )
294 assert response.status_code == 401
295
296
297 async def test_update_label_not_found_returns_404(
298 client: AsyncClient,
299 auth_headers: StrDict,
300 ) -> None:
301 """PATCH /labels/{id} with an unknown label_id returns 404."""
302 repo_id = await _create_repo(client, auth_headers, "update-404-repo")
303 response = await client.patch(
304 f"/api/repos/{repo_id}/labels/00000000-0000-0000-0000-000000000000",
305 json={"name": "ghost"},
306 headers=auth_headers,
307 )
308 assert response.status_code == 404
309
310
311 # ---------------------------------------------------------------------------
312 # DELETE /repos/{repo_id}/labels/{label_id}
313 # ---------------------------------------------------------------------------
314
315
316 async def test_delete_label_returns_204(
317 client: AsyncClient,
318 auth_headers: StrDict,
319 ) -> None:
320 """DELETE /labels/{id} removes the label and returns 204."""
321 repo_id = await _create_repo(client, auth_headers, "delete-label-repo")
322 label = await _create_label(client, auth_headers, repo_id)
323 label_id = label.get("label_id") or label.get("labelId")
324
325 response = await client.delete(
326 f"/api/repos/{repo_id}/labels/{label_id}",
327 headers=auth_headers,
328 )
329 assert response.status_code == 204
330
331 # Confirm the specific label is gone (seeded defaults remain).
332 list_resp = await client.get(f"/api/repos/{repo_id}/labels")
333 remaining_ids = {lbl.get("label_id") or lbl.get("labelId") for lbl in list_resp.json()["items"]}
334 assert label_id not in remaining_ids
335
336
337 async def test_delete_label_requires_auth(
338 client: AsyncClient,
339 auth_headers: StrDict,
340 ) -> None:
341 """DELETE /labels/{id} without auth returns 401."""
342 from musehub.auth.request_signing import optional_signed_request, require_signed_request
343 from musehub.main import app as _app
344
345 repo_id = await _create_repo(client, auth_headers, "delete-auth-repo")
346 label = await _create_label(client, auth_headers, repo_id)
347 label_id = label.get("label_id") or label.get("labelId")
348
349 _app.dependency_overrides.pop(require_signed_request, None)
350 _app.dependency_overrides.pop(optional_signed_request, None)
351 response = await client.delete(
352 f"/api/repos/{repo_id}/labels/{label_id}",
353 )
354 assert response.status_code == 401
355
356
357 # ---------------------------------------------------------------------------
358 # Issue label assignments
359 # ---------------------------------------------------------------------------
360
361
362 async def test_assign_labels_to_issue(
363 client: AsyncClient,
364 auth_headers: StrDict,
365 ) -> None:
366 """POST .../issues/{number}/labels assigns labels and returns the updated issue."""
367 repo_id = await _create_repo(client, auth_headers, "issue-label-assign-repo")
368 # "bug" is seeded by default — no need to create it separately.
369 issue = await _create_issue(client, auth_headers, repo_id)
370 issue_number = issue["number"]
371
372 response = await client.post(
373 f"/api/repos/{repo_id}/issues/{issue_number}/labels",
374 json={"labels": ["bug"]},
375 headers=auth_headers,
376 )
377 assert response.status_code == 200
378 updated_issue = response.json()
379 assert "bug" in updated_issue.get("labels", [])
380
381
382 async def test_assign_labels_to_issue_idempotent(
383 client: AsyncClient,
384 auth_headers: StrDict,
385 ) -> None:
386 """Assigning the same label twice does not raise an error."""
387 repo_id = await _create_repo(client, auth_headers, "issue-label-idem-repo")
388 # "bug" is seeded by default — no need to create it separately.
389 issue = await _create_issue(client, auth_headers, repo_id)
390 issue_number = issue["number"]
391
392 for _ in range(2):
393 response = await client.post(
394 f"/api/repos/{repo_id}/issues/{issue_number}/labels",
395 json={"labels": ["bug"]},
396 headers=auth_headers,
397 )
398 assert response.status_code == 200
399
400
401 async def test_remove_label_from_issue(
402 client: AsyncClient,
403 auth_headers: StrDict,
404 ) -> None:
405 """DELETE .../issues/{number}/labels/{label_name} removes the association."""
406 repo_id = await _create_repo(client, auth_headers, "issue-label-remove-repo")
407 # "bug" is seeded by default — no need to create it separately.
408 issue = await _create_issue(client, auth_headers, repo_id)
409 issue_number = issue["number"]
410
411 # Assign first.
412 await client.post(
413 f"/api/repos/{repo_id}/issues/{issue_number}/labels",
414 json={"labels": ["bug"]},
415 headers=auth_headers,
416 )
417
418 # Then remove (by label name, returns updated issue with 200).
419 response = await client.delete(
420 f"/api/repos/{repo_id}/issues/{issue_number}/labels/bug",
421 headers=auth_headers,
422 )
423 assert response.status_code == 200
424 assert "bug" not in response.json().get("labels", [])
425
426
427 async def test_remove_label_from_issue_unknown_issue_returns_404(
428 client: AsyncClient,
429 auth_headers: StrDict,
430 ) -> None:
431 """DELETE .../issues/{number}/labels/{label_id} for an unknown issue returns 404."""
432 repo_id = await _create_repo(client, auth_headers, "issue-label-404-repo")
433 label = await _create_label(client, auth_headers, repo_id)
434 label_id = label.get("label_id") or label.get("labelId")
435
436 response = await client.delete(
437 f"/api/repos/{repo_id}/issues/9999/labels/{label_id}",
438 headers=auth_headers,
439 )
440 assert response.status_code == 404
441
442
443 # ---------------------------------------------------------------------------
444 # Proposal label assignments
445 # ---------------------------------------------------------------------------
446
447
448 async def test_assign_labels_to_proposal(
449 client: AsyncClient,
450 auth_headers: StrDict,
451 db_session: AsyncSession,
452 ) -> None:
453 """POST .../proposals/{proposal_id}/labels assigns labels and returns them."""
454 repo_id = await _create_repo(client, auth_headers, "proposal-label-assign-repo")
455 await _push_branch(db_session, repo_id, "main")
456 await _push_branch(db_session, repo_id, "feature")
457 # "enhancement" is seeded by default — look it up from the repo's label list.
458 labels_resp = await client.get(f"/api/repos/{repo_id}/labels")
459 enhancement = next(lbl for lbl in labels_resp.json()["items"] if lbl["name"] == "enhancement")
460 label_id = enhancement.get("label_id") or enhancement.get("labelId")
461 proposal = await _create_proposal(client, auth_headers, repo_id)
462 proposal_id = proposal.get("proposalId") or proposal.get("proposal_id")
463
464 response = await client.post(
465 f"/api/repos/{repo_id}/proposals/{proposal_id}/labels",
466 json={"label_ids": [label_id]},
467 headers=auth_headers,
468 )
469 assert response.status_code == 200
470 assigned = response.json()
471 assert len(assigned) == 1
472 assert assigned[0]["name"] == "enhancement"
473
474
475 async def test_remove_label_from_proposal(
476 client: AsyncClient,
477 auth_headers: StrDict,
478 db_session: AsyncSession,
479 ) -> None:
480 """DELETE .../proposals/{proposal_id}/labels/{label_id} removes the association."""
481 repo_id = await _create_repo(client, auth_headers, "proposal-label-remove-repo")
482 await _push_branch(db_session, repo_id, "main")
483 await _push_branch(db_session, repo_id, "feature")
484 label = await _create_label(client, auth_headers, repo_id)
485 label_id = label.get("label_id") or label.get("labelId")
486 proposal = await _create_proposal(client, auth_headers, repo_id)
487 proposal_id = proposal.get("proposalId") or proposal.get("proposal_id")
488
489 # Assign first.
490 await client.post(
491 f"/api/repos/{repo_id}/proposals/{proposal_id}/labels",
492 json={"label_ids": [label_id]},
493 headers=auth_headers,
494 )
495
496 # Then remove — should be idempotent too.
497 response = await client.delete(
498 f"/api/repos/{repo_id}/proposals/{proposal_id}/labels/{label_id}",
499 headers=auth_headers,
500 )
501 assert response.status_code == 204
502
503
504 async def test_remove_label_from_proposal_unknown_returns_404(
505 client: AsyncClient,
506 auth_headers: StrDict,
507 ) -> None:
508 """DELETE .../proposals/{proposal_id}/labels/{label_id} for an unknown proposal returns 404."""
509 repo_id = await _create_repo(client, auth_headers, "proposal-label-404-repo")
510 label = await _create_label(client, auth_headers, repo_id)
511 label_id = label.get("label_id") or label.get("labelId")
512
513 response = await client.delete(
514 f"/api/repos/{repo_id}/proposals/00000000-0000-0000-0000-000000000000/labels/{label_id}",
515 headers=auth_headers,
516 )
517 assert response.status_code == 404
518
519
520 async def test_delete_label_cascades_to_issue_associations(
521 client: AsyncClient,
522 auth_headers: StrDict,
523 ) -> None:
524 """Deleting a label removes it from all issue associations (cascade)."""
525 repo_id = await _create_repo(client, auth_headers, "cascade-delete-repo")
526 label = await _create_label(client, auth_headers, repo_id)
527 label_id = label.get("label_id") or label.get("labelId")
528 issue = await _create_issue(client, auth_headers, repo_id)
529 issue_number = issue["number"]
530
531 await client.post(
532 f"/api/repos/{repo_id}/issues/{issue_number}/labels",
533 json={"label_ids": [label_id]},
534 headers=auth_headers,
535 )
536
537 delete_resp = await client.delete(
538 f"/api/repos/{repo_id}/labels/{label_id}",
539 headers=auth_headers,
540 )
541 assert delete_resp.status_code == 204
542
543 # The deleted label must not appear in the repo's label list (seeded defaults remain).
544 list_resp = await client.get(f"/api/repos/{repo_id}/labels")
545 remaining_ids = {lbl.get("label_id") or lbl.get("labelId") for lbl in list_resp.json()["items"]}
546 assert label_id not in remaining_ids
547
548
549 # ── Seed default labels ──────────────────────────────────────────────────────
550
551
552 async def test_create_repo_seeds_default_labels(
553 client: AsyncClient,
554 auth_headers: StrDict,
555 ) -> None:
556 """Creating a repo must automatically seed the default label set."""
557 repo_resp = await client.post(
558 "/api/repos",
559 json={"name": "seed-test-repo", "owner": "testuser", "initialize": False},
560 headers=auth_headers,
561 )
562 assert repo_resp.status_code == 201
563 repo_id: str = repo_resp.json()["repoId"]
564
565 label_resp = await client.get(f"/api/repos/{repo_id}/labels")
566 assert label_resp.status_code == 200
567 data = label_resp.json()
568 assert data["total"] > 0
569 names = {lbl["name"] for lbl in data["items"]}
570 # Standard VCS labels expected.
571 assert "bug" in names
572 assert "enhancement" in names
573 assert "documentation" in names
574 # Music-domain labels must NOT be present.
575 assert "needs-arrangement" not in names
576 assert "musical-theory" not in names
577
578
579 async def test_create_label_forbidden_for_non_owner(
580 client: AsyncClient,
581 auth_headers: StrDict,
582 db_session: AsyncSession,
583 ) -> None:
584 """POST /labels as a non-owner returns 403."""
585 from datetime import datetime, timezone
586 from musehub.core.genesis import compute_identity_id, compute_repo_id
587 from musehub.db.musehub_repo_models import MusehubRepo
588
589 # Create a repo owned by someone other than "testuser".
590 _created_at = datetime.now(tz=timezone.utc)
591 _owner_id = compute_identity_id(b"other-owner")
592 other_repo = MusehubRepo(
593 repo_id=compute_repo_id(_owner_id, "other-owner-repo", "code", _created_at.isoformat()),
594 name="other-owner-repo",
595 owner="other-owner",
596 slug="other-owner-repo",
597 visibility="public",
598 owner_user_id=_owner_id,
599 created_at=_created_at,
600 updated_at=_created_at,
601 )
602 db_session.add(other_repo)
603 await db_session.commit()
604
605 response = await client.post(
606 f"/api/repos/{other_repo.repo_id}/labels",
607 json={"name": "bug", "color": "#d73a4a"},
608 headers=auth_headers,
609 )
610 assert response.status_code == 403
611
612
613 async def test_delete_label_forbidden_for_non_owner(
614 client: AsyncClient,
615 auth_headers: StrDict,
616 db_session: AsyncSession,
617 ) -> None:
618 """DELETE /labels/{id} as a non-owner returns 403."""
619 from datetime import datetime, timezone
620 from musehub.core.genesis import compute_identity_id, compute_repo_id
621 from musehub.db.musehub_repo_models import MusehubRepo
622
623 _created_at = datetime.now(tz=timezone.utc)
624 _owner_id = compute_identity_id(b"other-owner")
625 other_repo = MusehubRepo(
626 repo_id=compute_repo_id(_owner_id, "other-owner-repo-del", "code", _created_at.isoformat()),
627 name="other-owner-repo-del",
628 owner="other-owner",
629 slug="other-owner-repo-del",
630 visibility="public",
631 owner_user_id=_owner_id,
632 created_at=_created_at,
633 updated_at=_created_at,
634 )
635 db_session.add(other_repo)
636 await db_session.commit()
637
638 response = await client.delete(
639 f"/api/repos/{other_repo.repo_id}/labels/00000000-0000-0000-0000-000000000000",
640 headers=auth_headers,
641 )
642 assert response.status_code == 403
643
644
645 async def test_seed_default_labels_is_idempotent(
646 client: AsyncClient,
647 auth_headers: StrDict,
648 ) -> None:
649 """seed_default_labels must not create duplicates when called twice."""
650 from musehub.db.database import AsyncSessionLocal
651 from musehub.api.routes.musehub.labels import seed_default_labels
652
653 repo_resp = await client.post(
654 "/api/repos",
655 json={"name": "idempotent-seed-repo", "owner": "testuser", "initialize": False},
656 headers=auth_headers,
657 )
658 assert repo_resp.status_code == 201
659 repo_id: str = repo_resp.json()["repoId"]
660
661 # Call seed a second time — should not raise and should not add duplicates.
662 async with AsyncSessionLocal() as session:
663 await seed_default_labels(session, repo_id)
664 await session.commit()
665
666 label_resp = await client.get(f"/api/repos/{repo_id}/labels")
667 data = label_resp.json()
668 names = [lbl["name"] for lbl in data["items"]]
669 # No duplicate names.
670 assert len(names) == len(set(names))
File History 1 commit
sha256:3ff9c9863a9891bdcde71b4a43228f66d0493e38b7cc1d09fe9eb7de774046b2 feat: add repair-commit wire endpoint (API parity with repa… Opus 4.8 minor 1 day ago