refactor(systems): one seam — the Systems question rides every read and write of a record
CI & Build / Python lint (push) Successful in 3s
CI & Build / Plugin hooks (push) Successful in 9s
CI & Build / TypeScript typecheck (push) Successful in 10s
CI & Build / integration (push) Successful in 12s
CI & Build / Python tests (push) Failing after 29s
CI & Build / Build & push image (push) Skipped
CI & Build / Python lint (push) Successful in 3s
CI & Build / Plugin hooks (push) Successful in 9s
CI & Build / TypeScript typecheck (push) Successful in 10s
CI & Build / integration (push) Successful in 12s
CI & Build / Python tests (push) Failing after 29s
CI & Build / Build & push image (push) Skipped
Scribe issue #2570, from the operator's challenge: the invariant is "always be asking whether what you're touching is a System's territory and whether the work is filed there" — not a nudge in one corner. The prior shape failed it twice: the hint fired only on creates (with a special-cased zero-Systems branch), and get_task/get_note returned records WITHOUT their Systems, so the read-side reflex had nothing to fire on (same per-kind asymmetry as #2481). - attach_systems(): single helper used by get/create/update for tasks, notes, and snippets, plus add_task_log. Tagged records always show `systems`; an untagged project record carries the `systems_hint` question instead. Neither field attaches empty (#2483). Hint is owner-only; everything fail-open (#2109). - untagged_systems_hint unified to ONE question — the vocabulary listing varies, the question doesn't; the zero-Systems branch stops being special text. - Docstrings state the uniform contract; floor prose now names the read-side reflex (systems visible -> list_system_records the pile). - Plugin 0.1.27 -> 0.1.28. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -132,36 +132,79 @@ async def test_create_system_distinct_name_passes_the_gate():
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_create_task_untagged_in_project_with_systems_carries_hint():
|
||||
async def test_create_task_untagged_in_project_carries_the_question():
|
||||
note = MagicMock(); note.id = 60; note.to_dict.return_value = {"id": 60}
|
||||
sys_a = MagicMock(); sys_a.id = 4; sys_a.name = "plugin-hooks"
|
||||
with patch("scribe.mcp.tools.tasks.current_user_id", return_value=1), \
|
||||
patch("scribe.mcp.tools.tasks.notes_svc") as notes_svc, \
|
||||
patch("scribe.mcp.tools.tasks.dedup_svc") as dedup_svc, \
|
||||
patch("scribe.mcp.tools.systems.systems_svc") as hint_svc:
|
||||
patch("scribe.mcp.tools.systems.systems_svc") as seam_svc:
|
||||
notes_svc.create_note = AsyncMock(return_value=note)
|
||||
dedup_svc.find_duplicate_note = AsyncMock(return_value=None)
|
||||
hint_svc.list_systems = AsyncMock(return_value=[sys_a])
|
||||
seam_svc.list_record_systems = AsyncMock(return_value=[])
|
||||
seam_svc.list_systems = AsyncMock(return_value=[sys_a])
|
||||
from scribe.mcp.tools.tasks import create_task
|
||||
result = await create_task(title="untagged", project_id=5)
|
||||
assert "plugin-hooks" in result["systems_hint"]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_create_task_tagged_or_projectless_carries_no_hint():
|
||||
async def test_create_task_tagged_shows_systems_and_projectless_gets_neither():
|
||||
note = MagicMock(); note.id = 61; note.to_dict.return_value = {"id": 61}
|
||||
tagged_sys = MagicMock(); tagged_sys.to_dict.return_value = {"id": 4, "name": "plugin-hooks"}
|
||||
with patch("scribe.mcp.tools.tasks.current_user_id", return_value=1), \
|
||||
patch("scribe.mcp.tools.tasks.notes_svc") as notes_svc, \
|
||||
patch("scribe.mcp.tools.tasks.dedup_svc") as dedup_svc, \
|
||||
patch("scribe.mcp.tools.tasks.systems_svc") as systems_svc, \
|
||||
patch("scribe.mcp.tools.systems.systems_svc") as hint_svc:
|
||||
patch("scribe.mcp.tools.systems.systems_svc") as seam_svc:
|
||||
notes_svc.create_note = AsyncMock(return_value=note)
|
||||
dedup_svc.find_duplicate_note = AsyncMock(return_value=None)
|
||||
systems_svc.set_record_systems = AsyncMock()
|
||||
systems_svc.list_record_systems = AsyncMock(return_value=[])
|
||||
hint_svc.list_systems = AsyncMock(return_value=[MagicMock()])
|
||||
seam_svc.list_record_systems = AsyncMock(return_value=[tagged_sys])
|
||||
from scribe.mcp.tools.tasks import create_task
|
||||
tagged = await create_task(title="tagged", project_id=5, system_ids=[4])
|
||||
seam_svc.list_record_systems = AsyncMock(return_value=[])
|
||||
orphan = await create_task(title="orphan", project_id=0)
|
||||
assert tagged["systems"] == [{"id": 4, "name": "plugin-hooks"}]
|
||||
assert "systems_hint" not in tagged
|
||||
assert "systems_hint" not in orphan
|
||||
assert "systems" not in orphan and "systems_hint" not in orphan
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_task_shows_the_records_systems_on_read():
|
||||
"""The touching-a-System reflex needs the affiliation visible on READ —
|
||||
a session opening the task it is about to work must see its areas."""
|
||||
note = MagicMock()
|
||||
note.id = 70; note.user_id = 1; note.project_id = 5
|
||||
note.deleted_at = None; note.parent_id = None
|
||||
note.to_dict.return_value = {"id": 70, "task_kind": "work"}
|
||||
tagged_sys = MagicMock(); tagged_sys.to_dict.return_value = {"id": 2, "name": "exporter"}
|
||||
with patch("scribe.mcp.tools.tasks.current_user_id", return_value=1), \
|
||||
patch("scribe.mcp.tools.tasks.notes_svc") as notes_svc, \
|
||||
patch("scribe.mcp.tools.tasks.access_svc") as access_svc, \
|
||||
patch("scribe.mcp.tools.tasks.record_pulled"), \
|
||||
patch("scribe.mcp.tools.systems.systems_svc") as seam_svc:
|
||||
notes_svc.get_note_for_user = AsyncMock(return_value=(note, "owner"))
|
||||
access_svc.describe_provenance = AsyncMock(return_value={})
|
||||
seam_svc.list_record_systems = AsyncMock(return_value=[tagged_sys])
|
||||
from scribe.mcp.tools.tasks import get_task
|
||||
result = await get_task(task_id=70)
|
||||
assert result["systems"] == [{"id": 2, "name": "exporter"}]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_add_task_log_on_untagged_project_task_asks_the_question():
|
||||
"""Logging work IS working in some area — the strongest moment to ask."""
|
||||
log = MagicMock(); log.to_dict.return_value = {"id": 9, "task_id": 70}
|
||||
task = MagicMock(); task.user_id = 1; task.project_id = 5
|
||||
with patch("scribe.mcp.tools.tasks.current_user_id", return_value=1), \
|
||||
patch("scribe.mcp.tools.tasks.task_logs_svc") as logs_svc, \
|
||||
patch("scribe.mcp.tools.tasks.notes_svc") as notes_svc, \
|
||||
patch("scribe.mcp.tools.systems.systems_svc") as seam_svc:
|
||||
logs_svc.create_log = AsyncMock(return_value=log)
|
||||
notes_svc.get_note_for_user = AsyncMock(return_value=(task, "owner"))
|
||||
seam_svc.list_record_systems = AsyncMock(return_value=[])
|
||||
seam_svc.list_systems = AsyncMock(return_value=[])
|
||||
from scribe.mcp.tools.tasks import add_task_log
|
||||
result = await add_task_log(task_id=70, content="progress")
|
||||
assert "no Systems yet" in result["systems_hint"]
|
||||
|
||||
Reference in New Issue
Block a user