ci: exact-version changelog match; test: auth-gate matrix syncs with registry
The release-notes awk start pattern was an unterminated prefix match ("## v0.3.0"
also re-armed on "## v0.3.01"); escape dots and anchor on the trailing space.
test_tool_auth's hand-counted matrix had drifted (update_wellness from 0.2.0 and
all 11 new tools were missing). Add all 12 and replace the count guard with a
comparison against the live mcp tool registry so drift fails loudly.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NGzHtDvJur9U7ysgRKRUTN
This commit is contained in:
@@ -87,7 +87,10 @@ jobs:
|
|||||||
# Body = this version's section from CHANGELOG.md (fallback to a stub).
|
# Body = this version's section from CHANGELOG.md (fallback to a stub).
|
||||||
# Commitizen writes headings as "## vX.Y.Z (date)", so match that form
|
# Commitizen writes headings as "## vX.Y.Z (date)", so match that form
|
||||||
# (not the Keep-a-Changelog "## [X.Y.Z]" brackets) and stop at the next.
|
# (not the Keep-a-Changelog "## [X.Y.Z]" brackets) and stop at the next.
|
||||||
body=$(awk "/^## v$v/{f=1;next} /^## v/{f=0} f" CHANGELOG.md)
|
# Escape dots and anchor on the trailing space so the start pattern is an
|
||||||
|
# exact version match ("## v0.3.0 " won't re-arm on "## v0.3.01 ...").
|
||||||
|
ve=$(printf '%s' "$v" | sed 's/\./\\./g')
|
||||||
|
body=$(awk "/^## v$ve /{f=1;next} /^## v/{f=0} f" CHANGELOG.md)
|
||||||
[ -z "$body" ] && body="Release v$v"
|
[ -z "$body" ] && body="Release v$v"
|
||||||
jq -n --arg tag "v$v" --arg name "v$v" --arg body "$body" \
|
jq -n --arg tag "v$v" --arg name "v$v" --arg body "$body" \
|
||||||
'{tag_name:$tag, name:$name, body:$body, draft:false, prerelease:false}' \
|
'{tag_name:$tag, name:$name, body:$body, draft:false, prerelease:false}' \
|
||||||
|
|||||||
+37
-5
@@ -11,16 +11,28 @@ import pytest
|
|||||||
|
|
||||||
from intervals_mcp_server import credentials
|
from intervals_mcp_server import credentials
|
||||||
from intervals_mcp_server.credentials import CredentialError
|
from intervals_mcp_server.credentials import CredentialError
|
||||||
from intervals_mcp_server.tools import activities, custom_items, events, gear, power_curves, wellness
|
from intervals_mcp_server.tools import (
|
||||||
|
activities,
|
||||||
|
athlete,
|
||||||
|
custom_items,
|
||||||
|
events,
|
||||||
|
gear,
|
||||||
|
power_curves,
|
||||||
|
wellness,
|
||||||
|
workouts,
|
||||||
|
)
|
||||||
|
|
||||||
# (tool callable, minimal required positional args)
|
# (tool callable, minimal required positional args)
|
||||||
TOOL_CALLS = [
|
TOOL_CALLS: list[tuple] = [
|
||||||
(activities.get_activities, ()),
|
(activities.get_activities, ()),
|
||||||
(activities.get_activity_details, ("1",)),
|
(activities.get_activity_details, ("1",)),
|
||||||
(activities.get_activity_intervals, ("1",)),
|
(activities.get_activity_intervals, ("1",)),
|
||||||
(activities.get_activity_streams, ("1",)),
|
(activities.get_activity_streams, ("1",)),
|
||||||
(activities.get_activity_messages, ("1",)),
|
(activities.get_activity_messages, ("1",)),
|
||||||
(activities.add_activity_message, ("1", "hi")),
|
(activities.add_activity_message, ("1", "hi")),
|
||||||
|
(activities.search_activities, ("ride",)),
|
||||||
|
(activities.get_activity_best_efforts, ("1",)),
|
||||||
|
(activities.get_activity_interval_stats, ("1", 0, 100)),
|
||||||
(events.get_events, ()),
|
(events.get_events, ()),
|
||||||
(events.get_event_by_id, ("e1",)),
|
(events.get_event_by_id, ("e1",)),
|
||||||
(events.delete_event, ("e1",)),
|
(events.delete_event, ("e1",)),
|
||||||
@@ -28,6 +40,15 @@ TOOL_CALLS = [
|
|||||||
(events.add_or_update_event, ("Ride", "Name")),
|
(events.add_or_update_event, ("Ride", "Name")),
|
||||||
(events.add_or_update_note, ("Name", "desc")),
|
(events.add_or_update_note, ("Name", "desc")),
|
||||||
(wellness.get_wellness_data, ()),
|
(wellness.get_wellness_data, ()),
|
||||||
|
(wellness.update_wellness, ()),
|
||||||
|
(wellness.update_wellness_bulk, ([],)),
|
||||||
|
(wellness.get_training_readiness, ()),
|
||||||
|
(athlete.get_athlete_profile, ()),
|
||||||
|
(athlete.get_sport_settings, ()),
|
||||||
|
(athlete.get_athlete_summary, ()),
|
||||||
|
(athlete.update_sport_settings, (1,)),
|
||||||
|
(workouts.get_workouts, ()),
|
||||||
|
(workouts.get_workout, (1,)),
|
||||||
(power_curves.get_athlete_power_curves, ()),
|
(power_curves.get_athlete_power_curves, ()),
|
||||||
(gear.get_gear_list, ()),
|
(gear.get_gear_list, ()),
|
||||||
(custom_items.get_custom_items, ()),
|
(custom_items.get_custom_items, ()),
|
||||||
@@ -50,6 +71,17 @@ def test_tool_returns_message_when_unauthorized(monkeypatch, func, args):
|
|||||||
assert result == "ACCOUNT NOT APPROVED"
|
assert result == "ACCOUNT NOT APPROVED"
|
||||||
|
|
||||||
|
|
||||||
def test_all_20_tools_covered():
|
def test_all_tools_covered():
|
||||||
"""Guard: if a tool is added, add it here so its auth gate is tested."""
|
"""Guard: every registered MCP tool must appear in TOOL_CALLS.
|
||||||
assert len(TOOL_CALLS) == 20
|
|
||||||
|
Compares against the live tool registry instead of a hand-maintained count,
|
||||||
|
so adding a tool without adding its auth-gate test fails loudly here.
|
||||||
|
"""
|
||||||
|
from intervals_mcp_server.mcp_instance import mcp
|
||||||
|
|
||||||
|
registered = {t.name for t in asyncio.run(mcp.list_tools())}
|
||||||
|
covered = {f.__name__ for f, _ in TOOL_CALLS}
|
||||||
|
assert covered == registered, (
|
||||||
|
f"auth-gate matrix out of sync: missing={sorted(registered - covered)} "
|
||||||
|
f"extra={sorted(covered - registered)}"
|
||||||
|
)
|
||||||
|
|||||||
Reference in New Issue
Block a user