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" @pytest.mark.asyncio async def test_update_task_can_re_file_a_kind(): """A task's kind must be CORRECTABLE, not write-once (#3129). What a piece of work turns out to be is often clear only once it is under way. Before this, `update_task` had no `kind` parameter at all and the REST route dropped the field the editor was already sending — the save reported success and reverted on reload. """ mock = AsyncMock(return_value=fake_note(task_kind="spike")) with patch("scribe.mcp.tools.tasks.notes_svc.update_note", mock): from scribe.mcp.tools.tasks import update_task await update_task(task_id=1, kind="spike") assert mock.call_args.kwargs["task_kind"] == "spike" @pytest.mark.asyncio async def test_update_task_leaves_kind_alone_when_not_given(): """"" means leave unchanged, as it does for every other field here.""" mock = AsyncMock(return_value=fake_note()) with patch("scribe.mcp.tools.tasks.notes_svc.update_note", mock): from scribe.mcp.tools.tasks import update_task await update_task(task_id=1, title="renamed") assert "task_kind" not in mock.call_args.kwargs @pytest.mark.asyncio async def test_minting_a_plan_is_refused_at_both_doors(): """`plan` is a valid stored value and NOT a mintable one. Historical plan-tasks carry it and must stay writable, so the column keeps accepting it — but plans became milestones in 0066, so no door hands out a new one. The error names start_planning rather than just refusing, because a caller reaching for kind='plan' wants a plan. """ from scribe.services.notes import minted_kind with pytest.raises(ValueError, match="start_planning"): minted_kind("plan") @pytest.mark.asyncio async def test_an_unknown_kind_raises_rather_than_defaulting(): """A silently-corrected kind is the defect this whole fix exists to end.""" from scribe.services.notes import minted_kind with pytest.raises(ValueError, match="kind must be one of"): minted_kind("investigation")