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
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>
49 lines
2.0 KiB
Python
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}
|