rename(547): the server is grimoire — package, image, DB defaults, backups
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
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>
This commit is contained in:
@@ -9,8 +9,8 @@ from unittest.mock import AsyncMock, MagicMock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
from scribe.services import record_refs
|
||||
from scribe.services.record_batch import BatchItem, BatchMilestone, check_batch
|
||||
from grimoire.services import record_refs
|
||||
from grimoire.services.record_batch import BatchItem, BatchMilestone, check_batch
|
||||
|
||||
pytestmark = pytest.mark.usefixtures("_bind_user")
|
||||
|
||||
@@ -23,14 +23,14 @@ def _max(n: int):
|
||||
|
||||
|
||||
@pytest.mark.parametrize(("module", "name", "kwargs"), [
|
||||
("scribe.mcp.tools.tasks", "create_task", {"title": "t", "body": "after #101"}),
|
||||
("scribe.mcp.tools.tasks", "update_task", {"task_id": 5, "body": "see #101"}),
|
||||
("scribe.mcp.tools.notes", "create_note", {"title": "n", "body": "index: #101"}),
|
||||
("scribe.mcp.tools.notes", "update_note", {"note_id": 5, "body": "index: #101"}),
|
||||
("scribe.mcp.tools.milestones", "create_milestone", {"project_id": 1, "title": "m", "body": "#101"}),
|
||||
("scribe.mcp.tools.milestones", "update_milestone", {"project_id": 1, "milestone_id": 2, "body": "#101"}),
|
||||
("scribe.mcp.tools.tasks", "start_planning", {"project_id": 1, "title": "p", "body": "step #101"}),
|
||||
("scribe.mcp.tools.tasks", "create_records", {"records": [{"title": "a", "body": "#101"}]}),
|
||||
("grimoire.mcp.tools.tasks", "create_task", {"title": "t", "body": "after #101"}),
|
||||
("grimoire.mcp.tools.tasks", "update_task", {"task_id": 5, "body": "see #101"}),
|
||||
("grimoire.mcp.tools.notes", "create_note", {"title": "n", "body": "index: #101"}),
|
||||
("grimoire.mcp.tools.notes", "update_note", {"note_id": 5, "body": "index: #101"}),
|
||||
("grimoire.mcp.tools.milestones", "create_milestone", {"project_id": 1, "title": "m", "body": "#101"}),
|
||||
("grimoire.mcp.tools.milestones", "update_milestone", {"project_id": 1, "milestone_id": 2, "body": "#101"}),
|
||||
("grimoire.mcp.tools.tasks", "start_planning", {"project_id": 1, "title": "p", "body": "step #101"}),
|
||||
("grimoire.mcp.tools.tasks", "create_records", {"records": [{"title": "a", "body": "#101"}]}),
|
||||
])
|
||||
async def test_every_writing_door_refuses_a_guessed_id_before_writing(module, name, kwargs):
|
||||
import importlib
|
||||
@@ -40,13 +40,13 @@ async def test_every_writing_door_refuses_a_guessed_id_before_writing(module, na
|
||||
# Every service a door could reach is a MagicMock that must stay untouched:
|
||||
# the refusal has to land before the duplicate gate and before the write.
|
||||
with _max(100), \
|
||||
patch("scribe.services.dedup.find_duplicate_note", AsyncMock()) as dedup, \
|
||||
patch("scribe.services.notes.create_note", AsyncMock()) as create, \
|
||||
patch("scribe.services.notes.update_note", AsyncMock()) as update, \
|
||||
patch("scribe.services.milestones.create_milestone", AsyncMock()) as ms_create, \
|
||||
patch("scribe.services.milestones.update_milestone", AsyncMock()) as ms_update, \
|
||||
patch("scribe.services.record_batch.create_batch", AsyncMock()) as batch, \
|
||||
patch("scribe.services.planning.start_planning", AsyncMock()) as plan:
|
||||
patch("grimoire.services.dedup.find_duplicate_note", AsyncMock()) as dedup, \
|
||||
patch("grimoire.services.notes.create_note", AsyncMock()) as create, \
|
||||
patch("grimoire.services.notes.update_note", AsyncMock()) as update, \
|
||||
patch("grimoire.services.milestones.create_milestone", AsyncMock()) as ms_create, \
|
||||
patch("grimoire.services.milestones.update_milestone", AsyncMock()) as ms_update, \
|
||||
patch("grimoire.services.record_batch.create_batch", AsyncMock()) as batch, \
|
||||
patch("grimoire.services.planning.start_planning", AsyncMock()) as plan:
|
||||
with pytest.raises(ValueError, match="#101"):
|
||||
await tool(**kwargs)
|
||||
for mock in (dedup, create, update, ms_create, ms_update, batch, plan):
|
||||
@@ -54,14 +54,14 @@ async def test_every_writing_door_refuses_a_guessed_id_before_writing(module, na
|
||||
|
||||
|
||||
async def test_a_real_id_passes_the_door():
|
||||
from scribe.mcp.tools.tasks import create_task
|
||||
from grimoire.mcp.tools.tasks import create_task
|
||||
|
||||
fake = MagicMock(id=90, project_id=None)
|
||||
fake.to_dict.return_value = {"id": 90}
|
||||
with _max(100), \
|
||||
patch("scribe.mcp.tools.tasks.dedup_svc.find_duplicate_note", AsyncMock(return_value=None)), \
|
||||
patch("scribe.mcp.tools.tasks.notes_svc.create_note", AsyncMock(return_value=fake)), \
|
||||
patch("scribe.mcp.tools.tasks.systems_tools.attach_systems", AsyncMock()):
|
||||
patch("grimoire.mcp.tools.tasks.dedup_svc.find_duplicate_note", AsyncMock(return_value=None)), \
|
||||
patch("grimoire.mcp.tools.tasks.notes_svc.create_note", AsyncMock(return_value=fake)), \
|
||||
patch("grimoire.mcp.tools.tasks.systems_tools.attach_systems", AsyncMock()):
|
||||
out = await create_task(title="t", body="follows #42")
|
||||
assert out["id"] == 90
|
||||
|
||||
@@ -70,26 +70,26 @@ async def test_a_real_id_passes_the_door():
|
||||
|
||||
|
||||
async def test_unknown_record_fields_are_refused_not_dropped():
|
||||
from scribe.mcp.tools.tasks import create_records
|
||||
from grimoire.mcp.tools.tasks import create_records
|
||||
|
||||
with pytest.raises(ValueError, match=r"record 2 has unknown field\(s\) \['milestone'\]"):
|
||||
await create_records(records=[{"title": "a"}, {"title": "b", "milestone": 4}])
|
||||
|
||||
|
||||
async def test_a_record_type_must_be_task_or_note():
|
||||
from scribe.mcp.tools.tasks import create_records
|
||||
from grimoire.mcp.tools.tasks import create_records
|
||||
|
||||
with pytest.raises(ValueError, match="type must be 'task' or 'note'"):
|
||||
await create_records(records=[{"title": "a", "type": "rule"}])
|
||||
|
||||
|
||||
async def test_one_duplicate_blocks_the_whole_batch():
|
||||
from scribe.mcp.tools.tasks import create_records
|
||||
from grimoire.mcp.tools.tasks import create_records
|
||||
|
||||
dup = MagicMock(id=33, title="existing", similarity=1.0, reason="title")
|
||||
gate = AsyncMock(side_effect=[None, dup])
|
||||
with patch("scribe.mcp.tools.tasks.dedup_svc.find_duplicate_note", gate), \
|
||||
patch("scribe.mcp.tools.tasks.batch_svc.create_batch", AsyncMock()) as batch:
|
||||
with patch("grimoire.mcp.tools.tasks.dedup_svc.find_duplicate_note", gate), \
|
||||
patch("grimoire.mcp.tools.tasks.batch_svc.create_batch", AsyncMock()) as batch:
|
||||
out = await create_records(records=[{"title": "fresh"}, {"title": "existing"}], project_id=3)
|
||||
assert out["duplicate"] is True and out["record"] == 2 and out["existing_id"] == 33
|
||||
assert "Nothing in the batch was created" in out["message"]
|
||||
@@ -97,15 +97,15 @@ async def test_one_duplicate_blocks_the_whole_batch():
|
||||
|
||||
|
||||
async def test_create_records_returns_ids_in_order():
|
||||
from scribe.mcp.tools.tasks import create_records
|
||||
from grimoire.mcp.tools.tasks import create_records
|
||||
|
||||
notes = []
|
||||
for nid in (51, 53):
|
||||
n = MagicMock(id=nid)
|
||||
n.to_dict.return_value = {"id": nid}
|
||||
notes.append(n)
|
||||
with patch("scribe.mcp.tools.tasks.dedup_svc.find_duplicate_note", AsyncMock(return_value=None)), \
|
||||
patch("scribe.mcp.tools.tasks.batch_svc.create_batch", AsyncMock(return_value=(None, notes))) as batch:
|
||||
with patch("grimoire.mcp.tools.tasks.dedup_svc.find_duplicate_note", AsyncMock(return_value=None)), \
|
||||
patch("grimoire.mcp.tools.tasks.batch_svc.create_batch", AsyncMock(return_value=(None, notes))) as batch:
|
||||
out = await create_records(
|
||||
records=[{"title": "a"}, {"title": "b", "type": "note", "body": "after {{ref:1}}"}],
|
||||
milestone_id=8,
|
||||
@@ -117,11 +117,11 @@ async def test_create_records_returns_ids_in_order():
|
||||
|
||||
|
||||
async def test_start_planning_hands_its_steps_to_the_service():
|
||||
from scribe.mcp.tools.tasks import start_planning
|
||||
from grimoire.mcp.tools.tasks import start_planning
|
||||
|
||||
with patch("scribe.mcp.tools.tasks.dedup_svc.find_duplicate_note", AsyncMock(return_value=None)), \
|
||||
patch("scribe.mcp.tools.tasks.dedup_svc.plan_gate", AsyncMock(return_value=None)), \
|
||||
patch("scribe.mcp.tools.tasks.planning_svc.start_planning",
|
||||
with patch("grimoire.mcp.tools.tasks.dedup_svc.find_duplicate_note", AsyncMock(return_value=None)), \
|
||||
patch("grimoire.mcp.tools.tasks.dedup_svc.plan_gate", AsyncMock(return_value=None)), \
|
||||
patch("grimoire.mcp.tools.tasks.planning_svc.start_planning",
|
||||
AsyncMock(return_value={"milestone": {"id": 1}})) as svc:
|
||||
await start_planning(project_id=3, title="Plan", body="see {{ref:1}}",
|
||||
steps=[{"title": "Step 1", "kind": "spike"}])
|
||||
@@ -155,17 +155,17 @@ def test_every_record_needs_a_title():
|
||||
|
||||
|
||||
def test_a_batch_has_a_ceiling():
|
||||
from scribe.services.record_batch import MAX_BATCH
|
||||
from grimoire.services.record_batch import MAX_BATCH
|
||||
|
||||
with pytest.raises(ValueError, match="at most"):
|
||||
check_batch([BatchItem(title=f"t{i}") for i in range(MAX_BATCH + 1)])
|
||||
|
||||
|
||||
async def test_start_planning_refuses_placeholders_with_no_steps():
|
||||
from scribe.services.planning import start_planning
|
||||
from grimoire.services.planning import start_planning
|
||||
|
||||
with patch("scribe.services.planning.projects_svc.get_project", AsyncMock(return_value=MagicMock())), \
|
||||
patch("scribe.services.planning.milestones_svc.create_milestone", AsyncMock()) as create:
|
||||
with patch("grimoire.services.planning.projects_svc.get_project", AsyncMock(return_value=MagicMock())), \
|
||||
patch("grimoire.services.planning.milestones_svc.create_milestone", AsyncMock()) as create:
|
||||
with pytest.raises(ValueError, match="no steps were given"):
|
||||
await start_planning(user_id=7, project_id=3, title="p", body="see {{ref:1}}")
|
||||
create.assert_not_awaited()
|
||||
|
||||
Reference in New Issue
Block a user