gabriel / musehub public
feat BREAKING labels feat/label-crud-coverage #2 / 2
gabriel · 170 days ago · Apr 10, 2026 · Diff

feat(labels): complete label CRUD coverage across CLI, MCP, and REST

REST: - Seed default labels (bug, enhancement, question, docs, etc.) on repo creation - Remove music-domain labels (needs-arrangement, musical-theory) from defaults - Fix dead seed_default_labels — now wired into create_repo route

MCP: - Add musehub_list_labels (read tool, no auth), musehub_update_label, musehub_delete_label - Move execute_create_label to mcp/write_tools/labels.py (own module) - Fix color validation: require #xxxxxx format (was incorrectly documented as 6-char) - Move musehub_list_labels to MUSEHUB_READ_TOOLS (no auth required)

UI: - Delete /labels web page, templates, and tests (CRUD requires MSign; no GUI needed) - Strip empty filter params before HTMX fires (htmx:configRequest) to fix URL pollution

Tests: - 120 passing: REST labels, MCP write tools, MCP dispatcher - Fix test_tool_catalogue count 65→68, fix proposal key 'proposals'→'pulls' - Fix prompts.py NameError ({hostname} in f-string)

sha256:9496a4368ef28185ddf11503bfd4d3811bc360097ae8837980c637ec3de670f7 sha
+48 ~61 −197 symbols
sha256:99654d87fb3428edab9fa102398cb76a5cddaeb9602ea381ab04aee59445c91b snapshot
+48
symbols added
~61
symbols modified
−197
symbols removed
0
dead code introduced
Semantic Changes 306 symbols
~ musehub/mcp/write_tools/labels.py .py 17 symbols added
+ _HEX_COLOR_RE variable variable _HEX_COLOR_RE L24–24
+ _MAX_LABEL_DESC variable variable _MAX_LABEL_DESC L23–23
+ _MAX_LABEL_NAME variable variable _MAX_LABEL_NAME L22–22
+ _validate_color function function _validate_color L27–29
+ execute_create_label function async_function execute_create_label L32–150
+ execute_delete_label function async_function execute_delete_label L298–369
+ execute_update_label function async_function execute_update_label L153–295
+ AsyncSessionLocal import import AsyncSessionLocal L15–15
+ JSONObject import import JSONObject L14–14 ← moved from musehub/api/routes/musehub/ui_labels.py
+ MusehubToolResult import import MusehubToolResult L17–17
+ _check_db_available import import _check_db_available L17–17
+ annotations import import annotations L8–8 ← moved from tests/test_musehub_ui_labels_ssr.py
+ logging import import logging L10–10 ← moved from musehub/api/routes/musehub/ui_labels.py
+ musehub_repository import import musehub_repository L16–16
+ re import import re L11–11
+ uuid import import uuid L12–12 ← moved from tests/test_labels_section28.py
+ logger variable variable logger L19–19 ← moved from musehub/api/routes/musehub/ui_labels.py
~ musehub/api/routes/musehub/ui_labels.py .py 46 symbols removed
− _LabelActionResponse class class _LabelActionResponse L103–108
− _LabelCreateBody class class _LabelCreateBody L87–92
− _LabelEditBody class class _LabelEditBody L95–100
− _LabelListPayload class class _LabelListPayload L80–84
− _LabelRow class class _LabelRow L67–77
− _assert_label_exists function async_function _assert_label_exists L243–258
− _fetch_labels function async_function _fetch_labels L116–142
− _label_list_fragment function async_function _label_list_fragment L204–240
− _parse_label_create_body function async_function _parse_label_create_body L145–172
− _parse_label_edit_body function async_function _parse_label_edit_body L175–201
− create_label function async_function create_label L340–402
− delete_label function async_function delete_label L504–554
− edit_label function async_function edit_label L414–492
− APIRouter import import APIRouter L39–39
− AsyncSession import import AsyncSession L44–44
− BaseModel import import BaseModel L42–42
− BranchParam import import BranchParam L48–48
− DEFAULT_LABELS import import DEFAULT_LABELS L51–51
− Depends import import Depends L39–39
− Field import import Field L42–42
− FilePathParam import import FilePathParam L48–48
− HTTPException import import HTTPException L39–39
− JSONObject import import JSONObject L55–55 → moved to musehub/mcp/write_tools/labels.py
− JSONResponse import import JSONResponse L41–41
− Request import import Request L39–39
− SlugParam import import SlugParam L48–48
− StarletteResponse import import StarletteResponse L45–45
− TokenClaims import import TokenClaims L53–53
− ValidationError import import ValidationError L42–42
− _resolve_repo import import _resolve_repo L49–49
− annotations import import annotations L33–33
− get_db import import get_db L54–54
− htmx_trigger import import htmx_trigger L50–50
− http_status import import http_status L40–40
− is_htmx import import is_htmx L50–50
− logging import import logging L36–36 → moved to musehub/mcp/write_tools/labels.py
− negotiate_response import import negotiate_response L52–52
− optional_token import import optional_token L53–53
− require_valid_token import import require_valid_token L53–53
− templates import import templates L47–47
− text import import text L43–43
− uuid import import uuid L37–37
− labels_page function async_function labels_page L270–327
− logger variable variable logger L57–57 → moved to musehub/mcp/write_tools/labels.py
− reset_labels function async_function reset_labels L566–641
− router variable variable router L59–59
~ tests/test_labels_section28.py .py 92 symbols removed
− TestDataIntegrityLabels class class TestDataIntegrityLabels L734–837
− test_delete_label_removes_issue_associations method async_method test_delete_label_removes_issue_associations L778–806
− test_issue_label_composite_pk method async_method test_issue_label_composite_pk L754–775
− test_label_color_stored_correctly method async_method test_label_color_stored_correctly L828–837
− test_label_unique_constraint_db method async_method test_label_unique_constraint_db L736–751
− test_remove_label_from_issue_idempotent method async_method test_remove_label_from_issue_idempotent L809–825
− TestE2ELabels class class TestE2ELabels L309–655
− test_assign_issue_labels_via_issues_route method async_method test_assign_issue_labels_via_issues_route L554–574
− test_assign_labels_to_proposal method async_method test_assign_labels_to_proposal L577–595
− test_assign_labels_to_proposal_not_found method async_method test_assign_labels_to_proposal_not_found L598–613
− test_create_label_duplicate_name_409 method async_method test_create_label_duplicate_name_409 L382–397
− test_create_label_returns_label_fields method async_method test_create_label_returns_label_fields L636–655
− test_create_label_success method async_method test_create_label_success L361–379
− test_delete_label_not_found method async_method test_delete_label_not_found L474–487
− test_delete_label_success method async_method test_delete_label_success L457–471
− test_delete_then_list_removes_label method async_method test_delete_then_list_removes_label L534–551
− test_list_labels_after_create method async_method test_list_labels_after_create L490–509
− test_list_labels_empty method async_method test_list_labels_empty L313–323
− test_list_labels_repo_not_found method async_method test_list_labels_repo_not_found L356–358
− test_list_labels_sorted_alphabetically method async_method test_list_labels_sorted_alphabetically L343–353
− test_list_labels_with_data method async_method test_list_labels_with_data L326–340
− test_remove_label_from_proposal method async_method test_remove_label_from_proposal L616–633
− test_update_label_color method async_method test_update_label_color L419–435
− test_update_label_name method async_method test_update_label_name L400–416
− test_update_label_name_conflict_409 method async_method test_update_label_name_conflict_409 L438–454
− test_update_then_list_reflects_change method async_method test_update_then_list_reflects_change L512–531
− TestIntegrationLabelDB class class TestIntegrationLabelDB L194–301
− test_get_label_or_404_found method async_method test_get_label_or_404_found L227–233
− test_get_label_or_404_missing method async_method test_get_label_or_404_missing L236–244
− test_issue_label_assignment_db method async_method test_issue_label_assignment_db L270–284
− test_label_unique_constraint_within_repo method async_method test_label_unique_constraint_within_repo L247–259
− test_proposal_label_assignment_db method async_method test_proposal_label_assignment_db L287–301
− test_same_name_different_repos_allowed method async_method test_same_name_different_repos_allowed L262–267
− test_seed_default_labels_idempotent method async_method test_seed_default_labels_idempotent L212–224
− test_seed_default_labels_inserts_all method async_method test_seed_default_labels_inserts_all L198–209
− TestPerformanceLabels class class TestPerformanceLabels L969–1027
− test_create_label_latency method async_method test_create_label_latency L988–1006
− test_delete_label_latency method async_method test_delete_label_latency L1009–1027
− test_list_labels_latency method async_method test_list_labels_latency L971–985
− TestSecurityLabels class class TestSecurityLabels L845–961
− test_assign_label_from_different_repo_to_proposal_404 method async_method test_assign_label_from_different_repo_to_proposal_404 L928–947
− test_assign_labels_to_proposal_requires_auth method async_method test_assign_labels_to_proposal_requires_auth L887–899
− test_create_label_requires_auth method async_method test_create_label_requires_auth L847–857
− test_create_label_wrong_repo_404 method async_method test_create_label_wrong_repo_404 L914–925
− test_delete_label_requires_auth method async_method test_delete_label_requires_auth L874–884
− test_list_labels_public_no_auth method async_method test_list_labels_public_no_auth L902–911
− test_remove_label_from_proposal_requires_auth method async_method test_remove_label_from_proposal_requires_auth L950–961
− test_update_label_requires_auth method async_method test_update_label_requires_auth L860–871
− TestStressLabels class class TestStressLabels L663–726
− test_assign_many_labels_to_proposal method async_method test_assign_many_labels_to_proposal L709–726
− test_bulk_create_labels method async_method test_bulk_create_labels L665–684
− test_sequential_label_creates method async_method test_sequential_label_creates L687–706
− TestUnitLabelModels class class TestUnitLabelModels L121–186
− test_assign_labels_request_requires_min_one method method test_assign_labels_request_requires_min_one L180–182
− test_assign_labels_request_valid method method test_assign_labels_request_valid L184–186
− test_default_labels_have_required_fields method method test_default_labels_have_required_fields L127–132
− test_default_labels_names_unique method method test_default_labels_names_unique L134–136
− test_default_labels_not_empty method method test_default_labels_not_empty L124–125
− test_label_create_valid method method test_label_create_valid L138–142
− test_label_create_with_description method method test_label_create_with_description L144–146
− test_label_list_response method method test_label_list_response L171–178
− test_label_response_round_trip method method test_label_response_round_trip L159–169
− test_label_update_all_optional method method test_label_update_all_optional L148–152
− test_label_update_partial method method test_label_update_partial L154–157
− _db_issue function async_function _db_issue L81–94
− _db_label function async_function _db_label L62–78
− _db_proposal function async_function _db_proposal L97–113
− _db_repo function async_function _db_repo L47–59
− _uid function function _uid L43–44
− AssignLabelsRequest import import AssignLabelsRequest L26–26
− AsyncClient import import AsyncClient L21–21
− AsyncGenerator import import AsyncGenerator L18–18
− AsyncSession import import AsyncSession L23–23
− DEFAULT_LABELS import import DEFAULT_LABELS L26–26
− LabelCreate import import LabelCreate L26–26
− LabelListResponse import import LabelListResponse L26–26
− LabelResponse import import LabelResponse L26–26
− LabelUpdate import import LabelUpdate L26–26
− MusehubIssue import import MusehubIssue L37–37
− MusehubIssueLabel import import MusehubIssueLabel L36–36
− MusehubLabel import import MusehubLabel L36–36
− MusehubProposal import import MusehubProposal L37–37
− MusehubProposalLabel import import MusehubProposalLabel L36–36
− MusehubRepo import import MusehubRepo L37–37
− StrDict import import StrDict L25–25
− _get_label_or_404 import import _get_label_or_404 L26–26
− annotations import import annotations L14–14
− pytest import import pytest L20–20
− seed_default_labels import import seed_default_labels L26–26
− text import import text L22–22
− time import import time L16–16
− uuid import import uuid L17–17 → moved to musehub/mcp/write_tools/labels.py
~ tests/test_musehub_ui_labels.py .py 36 symbols removed
− _make_label function async_function _make_label L74–92
− _make_repo function async_function _make_repo L55–71
− AsyncClient import import AsyncClient L42–42
− AsyncSession import import AsyncSession L43–43
− MusehubIssue import import MusehubIssue L46–46
− MusehubLabel import import MusehubLabel L45–45
− MusehubRepo import import MusehubRepo L46–46
− StrDict import import StrDict L47–47
− annotations import import annotations L39–39
− pytest import import pytest L41–41
− test_create_label_duplicate_name_409 function async_function test_create_label_duplicate_name_409 L248–261
− test_create_label_invalid_color_422 function async_function test_create_label_invalid_color_422 L265–277
− test_create_label_requires_auth function async_function test_create_label_requires_auth L235–244
− test_create_label_success function async_function test_create_label_success L215–231
− test_create_label_unknown_repo_404 function async_function test_create_label_unknown_repo_404 L281–292
− test_delete_label_requires_auth function async_function test_delete_label_requires_auth L413–422
− test_delete_label_success function async_function test_delete_label_success L395–409
− test_delete_label_unknown_label_404 function async_function test_delete_label_unknown_label_404 L426–437
− test_edit_label_name_conflict_409 function async_function test_edit_label_name_conflict_409 L351–365
− test_edit_label_partial_update function async_function test_edit_label_partial_update L369–386
− test_edit_label_requires_auth function async_function test_edit_label_requires_auth L321–331
− test_edit_label_success function async_function test_edit_label_success L301–317
− test_edit_label_unknown_label_404 function async_function test_edit_label_unknown_label_404 L335–347
− test_labels_page_base_url_uses_slug function async_function test_labels_page_base_url_uses_slug L197–206
− test_labels_page_has_color_picker_js function async_function test_labels_page_has_color_picker_js L131–139
− test_labels_page_has_label_list_js function async_function test_labels_page_has_label_list_js L143–151
− test_labels_page_json_format function async_function test_labels_page_json_format L155–162
− test_labels_page_json_has_items_key function async_function test_labels_page_json_has_items_key L166–177
− test_labels_page_no_auth_required function async_function test_labels_page_no_auth_required L112–118
− test_labels_page_returns_200 function async_function test_labels_page_returns_200 L101–108
− test_labels_page_shows_issue_count function async_function test_labels_page_shows_issue_count L181–193
− test_labels_page_unknown_repo_404 function async_function test_labels_page_unknown_repo_404 L122–127
− test_reset_labels_requires_auth function async_function test_reset_labels_requires_auth L466–472
− test_reset_labels_success function async_function test_reset_labels_success L446–462
− test_reset_labels_unknown_repo_404 function async_function test_reset_labels_unknown_repo_404 L508–518
− test_reset_labels_wipes_custom_labels function async_function test_reset_labels_wipes_custom_labels L476–504
~ tests/test_musehub_ui_labels_ssr.py .py 15 symbols removed
− _make_label function async_function _make_label L52–70
− _make_repo function async_function _make_repo L33–49
− AsyncClient import import AsyncClient L20–20
− AsyncSession import import AsyncSession L21–21
− MusehubLabel import import MusehubLabel L23–23
− MusehubRepo import import MusehubRepo L24–24
− StrDict import import StrDict L25–25
− annotations import import annotations L17–17 → moved to musehub/mcp/write_tools/labels.py
− pytest import import pytest L19–19
− test_labels_htmx_create_returns_fragment function async_function test_labels_htmx_create_returns_fragment L129–147
− test_labels_htmx_delete_returns_fragment function async_function test_labels_htmx_delete_returns_fragment L151–166
− test_labels_htmx_reset_returns_10_defaults function async_function test_labels_htmx_reset_returns_10_defaults L170–185
− test_labels_page_fragment_on_htmx_request function async_function test_labels_page_fragment_on_htmx_request L107–120
− test_labels_page_renders_label_name_server_side function async_function test_labels_page_renders_label_name_server_side L79–89
− test_labels_page_shows_issue_count_server_side function async_function test_labels_page_shows_issue_count_server_side L93–103
~ docs/reference/type-contracts.md .md 3 symbols added, 5 symbols removed, 7 symbols modified
− py` section _LabelActionResponse — `musehub/api/routes/musehub/ui_labels.py` L816–828
− py` section _LabelCreateBody — `musehub/api/routes/musehub/ui_labels.py` L829–841
− py` section _LabelEditBody — `musehub/api/routes/musehub/ui_labels.py` L842–854
− py` section _LabelListPayload — `musehub/api/routes/musehub/ui_labels.py` L855–866
− py` section _LabelRow — `musehub/api/routes/musehub/ui_labels.py` L867–882
+ py`@L3689 section MyModel — `tests/test_api_contracts_section37.py` L3689–3697
+ py`@L3698 section MyModel — `tests/test_api_contracts_section37.py` L3698–3706
+ py`@L3716 section MyModel — `tests/test_api_contracts_section37.py` L3716–3724
~ musehub/mcp/dispatcher.py .py 1 symbol modified
~ musehub/mcp/prompts.py .py 1 symbol modified
~ musehub/mcp/write_tools/issues.py .py 1 symbol removed
− execute_create_label function async_function execute_create_label L260–344
+ execute_list_labels function async_function execute_list_labels L1195–1246
~ tests/test_mcp_dispatcher.py .py 5 symbols added, 1 symbol removed, 1 symbol modified
− test_tool_catalogue_has_65_tools function function test_tool_catalogue_has_65_tools L170–172
+ test_label_tools_in_write_tool_names function async_function test_label_tools_in_write_tool_names L508–512
+ test_musehub_delete_label_requires_auth function async_function test_musehub_delete_label_requires_auth L494–504
+ test_musehub_list_labels_routes_to_executor function async_function test_musehub_list_labels_routes_to_executor L460–476
+ test_musehub_update_label_requires_auth function async_function test_musehub_update_label_requires_auth L480–490
+ test_tool_catalogue_has_68_tools function function test_tool_catalogue_has_68_tools L170–172
~ tests/test_mcp_write_tools.py .py 20 symbols added, 10 symbols modified
+ test_empty_name_rejected method async_method test_empty_name_rejected L565–570
+ test_invalid_color_rejected method async_method test_invalid_color_rejected L555–562
+ test_unknown_repo_returns_error method async_method test_unknown_repo_returns_error L573–580
+ TestIntegrationDeleteLabel class class TestIntegrationDeleteLabel L686–710
+ test_deletes_label method async_method test_deletes_label L688–699
+ test_not_found_returns_error method async_method test_not_found_returns_error L702–710
+ TestIntegrationListLabels class class TestIntegrationListLabels L583–609
+ test_empty_repo_returns_empty_list method async_method test_empty_repo_returns_empty_list L597–603
+ test_returns_all_labels method async_method test_returns_all_labels L585–594
+ test_unknown_repo_returns_error method async_method test_unknown_repo_returns_error L606–609
+ TestIntegrationUpdateLabel class class TestIntegrationUpdateLabel L612–683
+ test_change_color method async_method test_change_color L627–637
+ test_invalid_color_rejected method async_method test_invalid_color_rejected L675–683
+ test_no_fields_returns_error method async_method test_no_fields_returns_error L640–648
+ test_not_found_returns_error method async_method test_not_found_returns_error L663–672
+ test_rename_conflict_rejected method async_method test_rename_conflict_rejected L651–660
+ test_rename_label method async_method test_rename_label L614–624
+ execute_delete_label import import execute_delete_label L90–90
+ execute_list_labels import import execute_list_labels L95–95
+ execute_update_label import import execute_update_label L90–90

0 comments

No comments yet. Be the first to start the discussion.

To add a comment, use the Muse CLI: muse hub commit comment sha256:9496a4368ef28185ddf11503bfd4d3811bc360097ae8837980c637ec3de670f7 --body "your comment"