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>
50 lines
1.8 KiB
Python
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"
|