Module and test docstrings state what is pinned without the record number or step that introduced it. Incident accounts become the failure shape in the present tense: the pipefail head, the code_shapes import, the export order, the facet dialects, the cross-kind repeat, the write-once kind. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
97 lines
4.2 KiB
Python
97 lines
4.2 KiB
Python
"""Real-Postgres tests for task placement.
|
|
|
|
What a mock cannot show: that step order is creation order rather than update
|
|
order, that position and progress are computed over the steps the CALLER may
|
|
read, and that a collaborator holding one shared task is not shown the titles
|
|
of steps they cannot open.
|
|
"""
|
|
import pytest
|
|
import pytest_asyncio
|
|
|
|
from scribe.models import async_session
|
|
from scribe.models.note import Note
|
|
from scribe.models.project import Project
|
|
from scribe.models.share import NoteShare, ProjectShare
|
|
from scribe.services import notes as notes_svc
|
|
from scribe.services.placement import task_placement
|
|
from scribe.services.record_batch import BatchItem, BatchMilestone, create_batch
|
|
from tests.helpers import ensure_user
|
|
|
|
pytestmark = [pytest.mark.integration, pytest.mark.usefixtures("_dispose_engine", "_no_embedding")]
|
|
|
|
|
|
@pytest_asyncio.fixture
|
|
async def plan():
|
|
"""Owner, a collaborator, a project and a four-step milestone."""
|
|
async with async_session() as s:
|
|
owner = await ensure_user(s, "placement_owner")
|
|
other = await ensure_user(s, "placement_collaborator")
|
|
project = Project(user_id=owner.id, title="Placement target")
|
|
s.add(project)
|
|
await s.flush()
|
|
ids = {"owner": owner.id, "other": other.id, "pid": project.id}
|
|
await s.commit()
|
|
ms, steps = await create_batch(ids["owner"], [
|
|
BatchItem(title="Step 1", status="done"),
|
|
BatchItem(title="Step 2", status="in_progress"),
|
|
BatchItem(title="Step 3"),
|
|
BatchItem(title="Step 4"),
|
|
], project_id=ids["pid"], milestone=BatchMilestone(title="The plan", body="b"))
|
|
ids.update({"mid": ms.id, "steps": steps})
|
|
return ids
|
|
|
|
|
|
async def test_the_owner_sees_the_whole_placement(plan):
|
|
step2 = plan["steps"][1]
|
|
out = await task_placement(plan["owner"], step2)
|
|
assert out["project"] == {"id": plan["pid"], "title": "Placement target"}
|
|
assert out["milestone"] == {"id": plan["mid"], "title": "The plan", "status": "active"}
|
|
assert out["position"] == {"step": 2, "of": 4}
|
|
assert out["progress"] == {"completed": 1, "total": 4, "pct": 25.0}
|
|
assert out["next"] == {"id": plan["steps"][2].id, "title": "Step 3", "status": "todo"}
|
|
|
|
|
|
async def test_step_order_is_creation_order_not_last_update(plan):
|
|
"""Touching step 1 must not move it to the end of the list."""
|
|
first = plan["steps"][0]
|
|
updated = await notes_svc.update_note(plan["owner"], first.id, body="edited later")
|
|
out = await task_placement(plan["owner"], updated)
|
|
assert out["position"] == {"step": 1, "of": 4}
|
|
|
|
|
|
async def test_the_last_step_points_back_at_the_earliest_open_one(plan):
|
|
last = await notes_svc.update_note(plan["owner"], plan["steps"][3].id, status="done")
|
|
out = await task_placement(plan["owner"], last)
|
|
assert out["next"]["title"] == "Step 2"
|
|
assert out["progress"]["completed"] == 2
|
|
|
|
|
|
async def test_a_task_outside_any_milestone_gets_its_project_alone(plan):
|
|
loose = await notes_svc.create_note(plan["owner"], title="Loose task", status="todo",
|
|
project_id=plan["pid"])
|
|
out = await task_placement(plan["owner"], loose)
|
|
assert out == {"project": {"id": plan["pid"], "title": "Placement target"}}
|
|
|
|
|
|
async def test_a_collaborator_holding_one_shared_task_sees_no_plan(plan):
|
|
"""A note share opens one task. It does not open the project, so neither the
|
|
milestone nor its other steps' titles may leak through the placement."""
|
|
step3 = plan["steps"][2]
|
|
async with async_session() as s:
|
|
s.add(NoteShare(note_id=step3.id, shared_with_user_id=plan["other"],
|
|
permission="editor", invited_by=plan["owner"]))
|
|
await s.commit()
|
|
assert await task_placement(plan["other"], step3) is None
|
|
|
|
|
|
async def test_a_project_collaborator_sees_the_plan(plan):
|
|
async with async_session() as s:
|
|
s.add(ProjectShare(project_id=plan["pid"], shared_with_user_id=plan["other"],
|
|
permission="viewer", invited_by=plan["owner"]))
|
|
await s.commit()
|
|
step3 = await s.get(Note, plan["steps"][2].id)
|
|
out = await task_placement(plan["other"], step3)
|
|
assert out["milestone"]["title"] == "The plan"
|
|
assert out["position"] == {"step": 3, "of": 4}
|
|
assert out["next"]["title"] == "Step 4"
|