Files
FabledScribe/tests/test_mcp_tool_tasks_kind.py
T
bvandeusenandClaude Opus 5 e2e64b94c0 feat(tasks): task_kind gains 'spike' — the investigation, not the change (#3099, milestone 312 step 5)
A spike is a shape the other kinds cannot hold. `work` ships a change;
`issue` fixes something broken. A spike is time-boxed and its output is
KNOWLEDGE — it succeeds by producing an answer, and nothing ships at the end
of it. Filing one as `work` makes a finished investigation look like an
abandoned change, which is why the distinction earns a value rather than a
convention.

It is also the record a failed check asks for. This milestone gave rules a
verify_with; when one fails the rule is wrong, and the next move is often to
go and find out what replaced it. notes.arose_from_id already exists (0065),
so constraint -> spike provenance needed no schema at all — only a docstring
saying it is there.

Rule 36: the value and the widened CHECK land in the same migration, DROP
then ADD, exactly as 0065 did for 'issue'. The two whitelists live in one
tuple each so upgrade and downgrade cannot disagree about what the list was
on either side. The downgrade demotes existing spikes to 'work' first —
lossy, deliberately, because the alternative is a downgrade that fails on
real data, and one that says what it did beats one that cannot run.

'plan' stays whitelisted though retired: historical plan-tasks carry it, and
a row that cannot be rewritten cannot be edited, restored or migrated.

The integration test asserts both halves. A test that only proved 'spike' is
accepted would pass just as happily against a table whose CHECK had been
dropped and never re-added — which is the other way rule 36's failure
happens — so an unknown kind is asserted to still raise.

Not in scope, deliberately: any special lifecycle, time-box enforcement, or
gating relationship. It is a kind, not a workflow.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-27 13:43:57 -04:00

50 lines
1.8 KiB
Python

from unittest.mock import AsyncMock, patch
import pytest
from tests.helpers import fake_note
pytestmark = pytest.mark.usefixtures("_bind_user")
@pytest.mark.asyncio
async def test_create_task_passes_kind():
# kind=plan is retired (plans are milestones); 'issue' exercises passthrough.
mock = AsyncMock(return_value=fake_note(task_kind="issue"))
with patch("scribe.mcp.tools.tasks.notes_svc.create_note", mock):
from scribe.mcp.tools.tasks import create_task
await create_task(title="P", kind="issue")
assert mock.call_args.kwargs["task_kind"] == "issue"
@pytest.mark.asyncio
async def test_list_tasks_passes_kind_filter():
mock = AsyncMock(return_value=([], 0))
with patch("scribe.mcp.tools.tasks.notes_svc.list_notes", mock):
from scribe.mcp.tools.tasks import list_tasks
await list_tasks(kind="plan")
assert mock.call_args.kwargs["task_kind"] == "plan"
@pytest.mark.asyncio
async def test_list_tasks_kind_empty_means_no_filter():
mock = AsyncMock(return_value=([], 0))
with patch("scribe.mcp.tools.tasks.notes_svc.list_notes", mock):
from scribe.mcp.tools.tasks import list_tasks
await list_tasks()
assert mock.call_args.kwargs["task_kind"] is None
@pytest.mark.asyncio
async def test_create_task_passes_spike():
"""The kind a failed rule-check asks for (milestone 312).
Time-boxed, and its output is knowledge rather than a change — filing one
as `work` makes a finished investigation look like an abandoned change.
"""
mock = AsyncMock(return_value=fake_note(task_kind="spike"))
with patch("scribe.mcp.tools.tasks.notes_svc.create_note", mock):
from scribe.mcp.tools.tasks import create_task
await create_task(title="Can the runner be given a bash shell?", kind="spike")
assert mock.call_args.kwargs["task_kind"] == "spike"