Files
FabledGrimoire/tests/test_services_placement.py
bvandeusenandClaude Opus 5.5 0f5ef0be63
CI & Build / Python lint (push) Successful in 3s
CI & Build / Plugin hooks (push) Successful in 12s
CI & Build / TypeScript typecheck (push) Successful in 56s
CI & Build / integration (push) Successful in 1m4s
CI & Build / Python tests (push) Successful in 1m55s
CI & Build / Build & push image (push) Successful in 44s
rename(547): the server is grimoire — package, image, DB defaults, backups
Step 2 of the Scribe -> Grimoire rename.

- src/scribe -> src/grimoire, and every import, patch target, module path,
  hypercorn entry point and `python -m grimoire.restore` with it; pyproject,
  uv.lock and the frontend package are named grimoire.
- Image git.fabledsword.com/bvandeusen/fabledgrimoire; CI's Postgres service
  runs as grimoire / grimoire_test.
- Compose files default the DB user, database and (prod) volume to grimoire,
  each overridable from .env, so an install created as Scribe keeps its data
  by setting POSTGRES_USER, POSTGRES_DB and PGDATA_VOLUME.
- Backups are written as grimoire-<ts>.tar with format grimoire-backup-tar.
  Archives named scribe-* with the scribe-backup-tar manifest still restore
  and are still counted at the destination (folder and S3), with tests.
- Exports, the MCP auth realm, the ASGI scope key and the persistent logger
  carry the new name.

Kept on purpose: the secret-sealing key label stays "scribe.secrets.v1",
because every stored secret was derived under it.

Plugin identity (plugin name, hooks, env vars, .scribe marker, skills) is
step 3.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-22 10:50:45 -04:00

49 lines
2.0 KiB
Python

"""Placement — the pure half: what "next" means, and that a lookup never breaks a write.
The query half (position over readable steps, what a collaborator may see)
needs Postgres and lives in test_integration_placement.py.
"""
from types import SimpleNamespace
from unittest.mock import AsyncMock, patch
from grimoire.services import placement
from grimoire.services.placement import _next_open
def _steps(*statuses):
return [SimpleNamespace(id=i, title=f"step {i}", status=s) for i, s in enumerate(statuses, start=1)]
def test_next_is_the_first_open_step_after_this_one():
steps = _steps("done", "in_progress", "done", "todo", "todo")
assert _next_open(steps, current_id=2)["id"] == 4
def test_next_falls_back_to_an_earlier_open_step():
"""The last step finishing does not mean the milestone is finished."""
steps = _steps("todo", "done", "done")
assert _next_open(steps, current_id=3) == {"id": 1, "title": "step 1", "status": "todo"}
def test_nothing_open_means_no_next():
assert _next_open(_steps("done", "cancelled", "done"), current_id=3) is None
async def test_a_task_with_no_project_or_milestone_has_no_placement_and_no_query():
boom = AsyncMock(side_effect=AssertionError("must not open a session"))
with patch.object(placement, "async_session", boom):
task = SimpleNamespace(id=1, project_id=None, milestone_id=None)
assert await placement.task_placement(7, task) is None
async def test_attach_omits_placement_rather_than_sending_it_empty():
with patch.object(placement, "task_placement", AsyncMock(return_value=None)):
data = await placement.attach_placement(7, {"id": 1}, SimpleNamespace(id=1))
assert "placement" not in data
async def test_a_failing_lookup_returns_the_payload_untouched():
with patch.object(placement, "task_placement", AsyncMock(side_effect=RuntimeError("db down"))):
data = await placement.attach_placement(7, {"id": 1}, SimpleNamespace(id=1))
assert data == {"id": 1}