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