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:
@@ -13,10 +13,10 @@ pytestmark = pytest.mark.usefixtures("_bind_user")
|
||||
async def test_list_rulebooks_wraps_in_dict():
|
||||
rows = [fake_rulebook(id=1, title="t"), fake_rulebook(id=2, title="t")]
|
||||
with patch(
|
||||
"scribe.mcp.tools.rulebooks.rulebooks_svc.list_rulebooks",
|
||||
"grimoire.mcp.tools.rulebooks.rulebooks_svc.list_rulebooks",
|
||||
AsyncMock(return_value=rows),
|
||||
):
|
||||
from scribe.mcp.tools.rulebooks import list_rulebooks
|
||||
from grimoire.mcp.tools.rulebooks import list_rulebooks
|
||||
out = await list_rulebooks()
|
||||
assert len(out["rulebooks"]) == 2
|
||||
|
||||
@@ -26,13 +26,13 @@ async def test_get_rulebook_includes_topics():
|
||||
rb = fake_rulebook(id=1, title="t")
|
||||
topics = [fake_topic(id=10, title="git"), fake_topic(id=11, title="git")]
|
||||
with patch(
|
||||
"scribe.mcp.tools.rulebooks.rulebooks_svc.get_rulebook",
|
||||
"grimoire.mcp.tools.rulebooks.rulebooks_svc.get_rulebook",
|
||||
AsyncMock(return_value=rb),
|
||||
), patch(
|
||||
"scribe.mcp.tools.rulebooks.rulebooks_svc.list_topics",
|
||||
"grimoire.mcp.tools.rulebooks.rulebooks_svc.list_topics",
|
||||
AsyncMock(return_value=topics),
|
||||
):
|
||||
from scribe.mcp.tools.rulebooks import get_rulebook
|
||||
from grimoire.mcp.tools.rulebooks import get_rulebook
|
||||
out = await get_rulebook(rulebook_id=1)
|
||||
assert out["id"] == 1
|
||||
assert len(out["topics"]) == 2
|
||||
@@ -41,10 +41,10 @@ async def test_get_rulebook_includes_topics():
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_rulebook_raises_when_not_found():
|
||||
with patch(
|
||||
"scribe.mcp.tools.rulebooks.rulebooks_svc.get_rulebook",
|
||||
"grimoire.mcp.tools.rulebooks.rulebooks_svc.get_rulebook",
|
||||
AsyncMock(return_value=None),
|
||||
):
|
||||
from scribe.mcp.tools.rulebooks import get_rulebook
|
||||
from grimoire.mcp.tools.rulebooks import get_rulebook
|
||||
with pytest.raises(ValueError, match="rulebook 999 not found"):
|
||||
await get_rulebook(rulebook_id=999)
|
||||
|
||||
@@ -58,8 +58,8 @@ async def test_get_rulebook_raises_when_not_found():
|
||||
async def test_create_rule_passes_required_fields():
|
||||
rule = fake_rule(id=100, title="r", statement="s", topic_id=10)
|
||||
mock = AsyncMock(return_value=rule)
|
||||
with patch("scribe.mcp.tools.rulebooks.rulebooks_svc.create_rule", mock), _plain_detail():
|
||||
from scribe.mcp.tools.rulebooks import create_rule
|
||||
with patch("grimoire.mcp.tools.rulebooks.rulebooks_svc.create_rule", mock), _plain_detail():
|
||||
from grimoire.mcp.tools.rulebooks import create_rule
|
||||
await create_rule(
|
||||
topic_id=10, title="dev is home", statement="Work directly on dev",
|
||||
when_to_apply="when the moment this fixture stands in for arises",
|
||||
@@ -72,13 +72,13 @@ async def test_create_rule_passes_required_fields():
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_create_rule_blocked_by_duplicate_gate():
|
||||
from scribe.services.dedup import DuplicateMatch
|
||||
from grimoire.services.dedup import DuplicateMatch
|
||||
dup = DuplicateMatch(id=47, title="dev is home", similarity=1.0, reason="title")
|
||||
create_mock = AsyncMock()
|
||||
with patch("scribe.mcp.tools.rulebooks.dedup_svc.find_duplicate_rule",
|
||||
with patch("grimoire.mcp.tools.rulebooks.dedup_svc.find_duplicate_rule",
|
||||
AsyncMock(return_value=dup)), \
|
||||
patch("scribe.mcp.tools.rulebooks.rulebooks_svc.create_rule", create_mock):
|
||||
from scribe.mcp.tools.rulebooks import create_rule
|
||||
patch("grimoire.mcp.tools.rulebooks.rulebooks_svc.create_rule", create_mock):
|
||||
from grimoire.mcp.tools.rulebooks import create_rule
|
||||
out = await create_rule(topic_id=10, title="dev is home", statement="x", when_to_apply="when the moment this fixture stands in for arises")
|
||||
assert out["duplicate"] is True
|
||||
assert out["existing_id"] == 47
|
||||
@@ -89,11 +89,11 @@ async def test_create_rule_blocked_by_duplicate_gate():
|
||||
@pytest.mark.asyncio
|
||||
async def test_create_rule_force_bypasses_duplicate_gate():
|
||||
find_mock = AsyncMock()
|
||||
with patch("scribe.mcp.tools.rulebooks.dedup_svc.find_duplicate_rule", find_mock), \
|
||||
patch("scribe.mcp.tools.rulebooks.rulebooks_svc.create_rule",
|
||||
with patch("grimoire.mcp.tools.rulebooks.dedup_svc.find_duplicate_rule", find_mock), \
|
||||
patch("grimoire.mcp.tools.rulebooks.rulebooks_svc.create_rule",
|
||||
AsyncMock(return_value=fake_rule(id=5, title="r", statement="s", topic_id=10))), \
|
||||
_plain_detail():
|
||||
from scribe.mcp.tools.rulebooks import create_rule
|
||||
from grimoire.mcp.tools.rulebooks import create_rule
|
||||
out = await create_rule(topic_id=10, title="dev is home", statement="x", force=True, when_to_apply="when the moment this fixture stands in for arises")
|
||||
assert out["id"] == 5
|
||||
find_mock.assert_not_called()
|
||||
@@ -103,8 +103,8 @@ async def test_create_rule_force_bypasses_duplicate_gate():
|
||||
async def test_update_rule_only_sends_non_default_fields():
|
||||
rule = fake_rule(id=100, title="r", statement="s", topic_id=10)
|
||||
mock = AsyncMock(return_value=rule)
|
||||
with patch("scribe.mcp.tools.rulebooks.rulebooks_svc.update_rule", mock), _plain_detail():
|
||||
from scribe.mcp.tools.rulebooks import update_rule
|
||||
with patch("grimoire.mcp.tools.rulebooks.rulebooks_svc.update_rule", mock), _plain_detail():
|
||||
from grimoire.mcp.tools.rulebooks import update_rule
|
||||
await update_rule(rule_id=1, statement="new statement")
|
||||
args, kwargs = mock.call_args
|
||||
assert args == (1, 7)
|
||||
@@ -124,8 +124,8 @@ async def test_update_rule_forwards_the_fields_named_for_clearing():
|
||||
"""
|
||||
rule = fake_rule(id=100, title="r", statement="s", topic_id=10)
|
||||
mock = AsyncMock(return_value=rule)
|
||||
with patch("scribe.mcp.tools.rulebooks.rulebooks_svc.update_rule", mock), _plain_detail():
|
||||
from scribe.mcp.tools.rulebooks import update_rule
|
||||
with patch("grimoire.mcp.tools.rulebooks.rulebooks_svc.update_rule", mock), _plain_detail():
|
||||
from grimoire.mcp.tools.rulebooks import update_rule
|
||||
await update_rule(rule_id=1, clear_fields=["verify_with"])
|
||||
_args, kwargs = mock.call_args
|
||||
assert kwargs == {"clear": ["verify_with"]}
|
||||
@@ -135,8 +135,8 @@ async def test_update_rule_forwards_the_fields_named_for_clearing():
|
||||
async def test_update_rule_sends_the_check_fields_when_given():
|
||||
rule = fake_rule(id=100, title="r", statement="s", topic_id=10)
|
||||
mock = AsyncMock(return_value=rule)
|
||||
with patch("scribe.mcp.tools.rulebooks.rulebooks_svc.update_rule", mock), _plain_detail():
|
||||
from scribe.mcp.tools.rulebooks import update_rule
|
||||
with patch("grimoire.mcp.tools.rulebooks.rulebooks_svc.update_rule", mock), _plain_detail():
|
||||
from grimoire.mcp.tools.rulebooks import update_rule
|
||||
await update_rule(
|
||||
rule_id=1,
|
||||
verify_with="cat CI-runner/renovate/config.js",
|
||||
@@ -155,13 +155,13 @@ async def test_delete_rule_without_confirmed_returns_warning():
|
||||
"""delete_rule with confirmed=False returns a preview, not an action."""
|
||||
rule = fake_rule(id=100, title="r", statement="s", topic_id=10)
|
||||
with patch(
|
||||
"scribe.mcp.tools.rulebooks.rulebooks_svc.get_rule",
|
||||
"grimoire.mcp.tools.rulebooks.rulebooks_svc.get_rule",
|
||||
AsyncMock(return_value=rule),
|
||||
), patch(
|
||||
"scribe.mcp.tools.rulebooks.rulebooks_svc.delete_rule",
|
||||
"grimoire.mcp.tools.rulebooks.rulebooks_svc.delete_rule",
|
||||
AsyncMock(),
|
||||
) as mock_delete:
|
||||
from scribe.mcp.tools.rulebooks import delete_rule
|
||||
from grimoire.mcp.tools.rulebooks import delete_rule
|
||||
out = await delete_rule(rule_id=1, confirmed=False)
|
||||
assert out.get("confirmed_required") is True
|
||||
assert "confirmed=True" in out.get("warning", "")
|
||||
@@ -173,13 +173,13 @@ async def test_delete_rule_with_confirmed_soft_deletes():
|
||||
rule = fake_rule(id=100, title="r", statement="s", topic_id=10)
|
||||
mock_delete = AsyncMock(return_value="batch-1")
|
||||
with patch(
|
||||
"scribe.mcp.tools.rulebooks.rulebooks_svc.get_rule",
|
||||
"grimoire.mcp.tools.rulebooks.rulebooks_svc.get_rule",
|
||||
AsyncMock(return_value=rule),
|
||||
), patch(
|
||||
"scribe.mcp.tools.rulebooks.trash_svc.delete",
|
||||
"grimoire.mcp.tools.rulebooks.trash_svc.delete",
|
||||
mock_delete,
|
||||
):
|
||||
from scribe.mcp.tools.rulebooks import delete_rule
|
||||
from grimoire.mcp.tools.rulebooks import delete_rule
|
||||
out = await delete_rule(rule_id=1, confirmed=True)
|
||||
assert out["deleted"] == 1
|
||||
assert out["deleted_batch_id"] == "batch-1"
|
||||
@@ -194,7 +194,7 @@ def test_register_attaches_every_tool():
|
||||
said "sixteen" for ten tools' worth of growth — the number lives in the
|
||||
assertion, not the title, so it cannot drift again.
|
||||
"""
|
||||
from scribe.mcp.tools.rulebooks import register
|
||||
from grimoire.mcp.tools.rulebooks import register
|
||||
mcp = FakeMCP()
|
||||
|
||||
register(mcp)
|
||||
@@ -235,8 +235,8 @@ def test_register_attaches_every_tool():
|
||||
async def test_create_project_rule_passes_required_fields():
|
||||
rule = fake_rule(id=100, title="r", statement="s", topic_id=10)
|
||||
mock = AsyncMock(return_value=rule)
|
||||
with patch("scribe.mcp.tools.rulebooks.rulebooks_svc.create_project_rule", mock), _plain_detail():
|
||||
from scribe.mcp.tools.rulebooks import create_project_rule
|
||||
with patch("grimoire.mcp.tools.rulebooks.rulebooks_svc.create_project_rule", mock), _plain_detail():
|
||||
from grimoire.mcp.tools.rulebooks import create_project_rule
|
||||
await create_project_rule(
|
||||
project_id=42,
|
||||
statement="Always run migrations through alembic, not raw SQL.",
|
||||
@@ -254,8 +254,8 @@ async def test_create_project_rule_passes_required_fields():
|
||||
async def test_create_project_rule_derives_title_from_statement():
|
||||
rule = fake_rule(id=100, title="r", statement="s", topic_id=10)
|
||||
mock = AsyncMock(return_value=rule)
|
||||
with patch("scribe.mcp.tools.rulebooks.rulebooks_svc.create_project_rule", mock), _plain_detail():
|
||||
from scribe.mcp.tools.rulebooks import create_project_rule
|
||||
with patch("grimoire.mcp.tools.rulebooks.rulebooks_svc.create_project_rule", mock), _plain_detail():
|
||||
from grimoire.mcp.tools.rulebooks import create_project_rule
|
||||
await create_project_rule(
|
||||
project_id=42,
|
||||
statement="Avoid auto-generated docstrings. Reviewers find them noise.",
|
||||
@@ -270,8 +270,8 @@ async def test_create_project_rule_derives_title_from_statement():
|
||||
async def test_create_project_rule_uses_explicit_title_when_given():
|
||||
rule = fake_rule(id=100, title="r", statement="s", topic_id=10)
|
||||
mock = AsyncMock(return_value=rule)
|
||||
with patch("scribe.mcp.tools.rulebooks.rulebooks_svc.create_project_rule", mock), _plain_detail():
|
||||
from scribe.mcp.tools.rulebooks import create_project_rule
|
||||
with patch("grimoire.mcp.tools.rulebooks.rulebooks_svc.create_project_rule", mock), _plain_detail():
|
||||
from grimoire.mcp.tools.rulebooks import create_project_rule
|
||||
await create_project_rule(
|
||||
project_id=42,
|
||||
statement="anything",
|
||||
@@ -292,8 +292,8 @@ async def test_relate_rules_forwards_the_kind_and_the_why():
|
||||
relation = MagicMock()
|
||||
relation.to_dict.return_value = {"id": 9, "kind": "co_surfaces"}
|
||||
mock = AsyncMock(return_value=relation)
|
||||
with patch("scribe.mcp.tools.rulebooks.rulebooks_svc.add_rule_relation", mock):
|
||||
from scribe.mcp.tools.rulebooks import relate_rules
|
||||
with patch("grimoire.mcp.tools.rulebooks.rulebooks_svc.add_rule_relation", mock):
|
||||
from grimoire.mcp.tools.rulebooks import relate_rules
|
||||
out = await relate_rules(
|
||||
from_rule_id=46, to_rule_id=144, kind="co_surfaces",
|
||||
note="a stale channel tag and an unparseable version both read as "
|
||||
@@ -310,18 +310,18 @@ async def test_relate_rules_forwards_the_kind_and_the_why():
|
||||
async def test_relate_rules_raises_when_either_end_is_not_yours():
|
||||
"""The service returns None when it cannot see both rules — a one-sided
|
||||
edge would surface a rule the caller has no business reading."""
|
||||
with patch("scribe.mcp.tools.rulebooks.rulebooks_svc.add_rule_relation",
|
||||
with patch("grimoire.mcp.tools.rulebooks.rulebooks_svc.add_rule_relation",
|
||||
AsyncMock(return_value=None)):
|
||||
from scribe.mcp.tools.rulebooks import relate_rules
|
||||
from grimoire.mcp.tools.rulebooks import relate_rules
|
||||
with pytest.raises(ValueError, match="not found"):
|
||||
await relate_rules(from_rule_id=1, to_rule_id=2, kind="overrides")
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_unrelate_rules_raises_when_the_edge_is_gone():
|
||||
with patch("scribe.mcp.tools.rulebooks.rulebooks_svc.remove_rule_relation",
|
||||
with patch("grimoire.mcp.tools.rulebooks.rulebooks_svc.remove_rule_relation",
|
||||
AsyncMock(return_value=False)):
|
||||
from scribe.mcp.tools.rulebooks import unrelate_rules
|
||||
from grimoire.mcp.tools.rulebooks import unrelate_rules
|
||||
with pytest.raises(ValueError, match="not found"):
|
||||
await unrelate_rules(relation_id=99)
|
||||
|
||||
@@ -332,7 +332,7 @@ def _fake_version(**over):
|
||||
"""A RuleVersion-shaped stand-in. A real model instance rather than a
|
||||
MagicMock, because the tool calls `to_dict` and a mock would hand back
|
||||
another mock instead of failing."""
|
||||
from scribe.models.rule_version import RuleVersion
|
||||
from grimoire.models.rule_version import RuleVersion
|
||||
from datetime import datetime, timezone
|
||||
|
||||
defaults = {
|
||||
@@ -353,15 +353,15 @@ async def test_rule_history_lists_without_the_heavy_text():
|
||||
thousands of characters, so a history carrying every field would cost
|
||||
more to read than the answer is worth."""
|
||||
with patch(
|
||||
"scribe.mcp.tools.rulebooks.rulebooks_svc.list_rule_versions",
|
||||
"grimoire.mcp.tools.rulebooks.rulebooks_svc.list_rule_versions",
|
||||
AsyncMock(return_value=[_fake_version(), _fake_version(id=4)]),
|
||||
), patch(
|
||||
"scribe.mcp.tools.rulebooks.rulebooks_svc.get_rule",
|
||||
"grimoire.mcp.tools.rulebooks.rulebooks_svc.get_rule",
|
||||
AsyncMock(return_value=fake_rule(
|
||||
id=100, title="The runner has no bash", statement="s", topic_id=10,
|
||||
)),
|
||||
):
|
||||
from scribe.mcp.tools.rulebooks import rule_history
|
||||
from grimoire.mcp.tools.rulebooks import rule_history
|
||||
out = await rule_history(rule_id=100)
|
||||
|
||||
assert out["total"] == 2
|
||||
@@ -375,10 +375,10 @@ async def test_rule_history_opens_one_version_in_full():
|
||||
"""Passing a version id switches from the index to the text — which is
|
||||
the whole reason the listing can afford to omit it."""
|
||||
with patch(
|
||||
"scribe.mcp.tools.rulebooks.rulebooks_svc.get_rule_version",
|
||||
"grimoire.mcp.tools.rulebooks.rulebooks_svc.get_rule_version",
|
||||
AsyncMock(return_value=_fake_version()),
|
||||
):
|
||||
from scribe.mcp.tools.rulebooks import rule_history
|
||||
from grimoire.mcp.tools.rulebooks import rule_history
|
||||
out = await rule_history(rule_id=100, version_id=5)
|
||||
|
||||
assert out["statement"] == "Use sh."
|
||||
@@ -390,15 +390,15 @@ async def test_rule_history_says_an_empty_history_is_ordinary():
|
||||
"""Most rules have never been reworded. Without this line an empty list
|
||||
reads as a lost history or a broken tool."""
|
||||
with patch(
|
||||
"scribe.mcp.tools.rulebooks.rulebooks_svc.list_rule_versions",
|
||||
"grimoire.mcp.tools.rulebooks.rulebooks_svc.list_rule_versions",
|
||||
AsyncMock(return_value=[]),
|
||||
), patch(
|
||||
"scribe.mcp.tools.rulebooks.rulebooks_svc.get_rule",
|
||||
"grimoire.mcp.tools.rulebooks.rulebooks_svc.get_rule",
|
||||
AsyncMock(return_value=fake_rule(
|
||||
id=100, title="The runner has no bash", statement="s", topic_id=10,
|
||||
)),
|
||||
):
|
||||
from scribe.mcp.tools.rulebooks import rule_history
|
||||
from grimoire.mcp.tools.rulebooks import rule_history
|
||||
out = await rule_history(rule_id=100)
|
||||
|
||||
assert out["total"] == 0
|
||||
@@ -411,10 +411,10 @@ async def test_rule_history_raises_when_the_rule_is_not_yours():
|
||||
turn that into an empty history — which would read as "this rule has no
|
||||
past" rather than "this is not your rule"."""
|
||||
with patch(
|
||||
"scribe.mcp.tools.rulebooks.rulebooks_svc.list_rule_versions",
|
||||
"grimoire.mcp.tools.rulebooks.rulebooks_svc.list_rule_versions",
|
||||
AsyncMock(return_value=None),
|
||||
):
|
||||
from scribe.mcp.tools.rulebooks import rule_history
|
||||
from grimoire.mcp.tools.rulebooks import rule_history
|
||||
with pytest.raises(ValueError, match="rule 100 not found"):
|
||||
await rule_history(rule_id=100)
|
||||
|
||||
@@ -422,10 +422,10 @@ async def test_rule_history_raises_when_the_rule_is_not_yours():
|
||||
@pytest.mark.asyncio
|
||||
async def test_rule_history_raises_for_a_version_on_another_rule():
|
||||
with patch(
|
||||
"scribe.mcp.tools.rulebooks.rulebooks_svc.get_rule_version",
|
||||
"grimoire.mcp.tools.rulebooks.rulebooks_svc.get_rule_version",
|
||||
AsyncMock(return_value=None),
|
||||
):
|
||||
from scribe.mcp.tools.rulebooks import rule_history
|
||||
from grimoire.mcp.tools.rulebooks import rule_history
|
||||
with pytest.raises(ValueError, match="version 5 not found"):
|
||||
await rule_history(rule_id=100, version_id=5)
|
||||
|
||||
@@ -435,7 +435,7 @@ def test_rule_history_docstring_says_what_a_version_HOLDS():
|
||||
edit REPLACED, not the text it introduced. Read the other way, every
|
||||
diff comes out backwards — so the docstring has to say it, and this is
|
||||
the guard against a later tidy-up dropping the line."""
|
||||
from scribe.mcp.tools.rulebooks import rule_history
|
||||
from grimoire.mcp.tools.rulebooks import rule_history
|
||||
|
||||
doc = rule_history.__doc__ or ""
|
||||
assert "REPLACED" in doc
|
||||
@@ -454,9 +454,9 @@ async def test_move_rule_passes_one_destination_and_returns_the_detail():
|
||||
moved = fake_rule(id=94, topic_id=12, project_id=None)
|
||||
move = AsyncMock(return_value=moved)
|
||||
detail = AsyncMock(return_value={"id": 94, "topic_id": 12, "project_id": None})
|
||||
with patch("scribe.mcp.tools.rulebooks.rulebooks_svc.move_rule", move), \
|
||||
patch("scribe.mcp.tools.rulebooks.rulebooks_svc.rule_detail", detail):
|
||||
from scribe.mcp.tools.rulebooks import move_rule
|
||||
with patch("grimoire.mcp.tools.rulebooks.rulebooks_svc.move_rule", move), \
|
||||
patch("grimoire.mcp.tools.rulebooks.rulebooks_svc.rule_detail", detail):
|
||||
from grimoire.mcp.tools.rulebooks import move_rule
|
||||
out = await move_rule(rule_id=94, topic_id=12)
|
||||
assert move.await_args.args[0] == 94
|
||||
assert move.await_args.kwargs == {"topic_id": 12, "project_id": 0}
|
||||
@@ -465,8 +465,8 @@ async def test_move_rule_passes_one_destination_and_returns_the_detail():
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_move_rule_on_someone_elses_rule_is_not_found():
|
||||
with patch("scribe.mcp.tools.rulebooks.rulebooks_svc.move_rule", AsyncMock(return_value=None)):
|
||||
from scribe.mcp.tools.rulebooks import move_rule
|
||||
with patch("grimoire.mcp.tools.rulebooks.rulebooks_svc.move_rule", AsyncMock(return_value=None)):
|
||||
from grimoire.mcp.tools.rulebooks import move_rule
|
||||
with pytest.raises(ValueError, match="not found"):
|
||||
await move_rule(rule_id=94, project_id=3)
|
||||
|
||||
@@ -477,12 +477,12 @@ async def test_move_rule_on_someone_elses_rule_is_not_found():
|
||||
async def test_rule_outcome_records_an_application():
|
||||
rule = fake_rule(id=156, title="`dev` is home")
|
||||
with patch(
|
||||
"scribe.mcp.tools.rulebooks.rulebooks_svc.get_rule",
|
||||
"grimoire.mcp.tools.rulebooks.rulebooks_svc.get_rule",
|
||||
AsyncMock(return_value=rule),
|
||||
), patch(
|
||||
"scribe.mcp.tools.rulebooks.record_rule_outcome", MagicMock()
|
||||
"grimoire.mcp.tools.rulebooks.record_rule_outcome", MagicMock()
|
||||
) as rec:
|
||||
from scribe.mcp.tools.rulebooks import rule_outcome
|
||||
from grimoire.mcp.tools.rulebooks import rule_outcome
|
||||
out = await rule_outcome(rule_id=156, outcome="applied")
|
||||
assert out["outcome"] == "applied" and out["recorded"] is True
|
||||
assert out["why"] is None
|
||||
@@ -494,12 +494,12 @@ async def test_rule_outcome_records_an_application():
|
||||
async def test_rule_outcome_records_a_departure_with_its_reason():
|
||||
rule = fake_rule(id=156, title="`dev` is home")
|
||||
with patch(
|
||||
"scribe.mcp.tools.rulebooks.rulebooks_svc.get_rule",
|
||||
"grimoire.mcp.tools.rulebooks.rulebooks_svc.get_rule",
|
||||
AsyncMock(return_value=rule),
|
||||
), patch(
|
||||
"scribe.mcp.tools.rulebooks.record_rule_outcome", MagicMock()
|
||||
"grimoire.mcp.tools.rulebooks.record_rule_outcome", MagicMock()
|
||||
) as rec:
|
||||
from scribe.mcp.tools.rulebooks import rule_outcome
|
||||
from grimoire.mcp.tools.rulebooks import rule_outcome
|
||||
out = await rule_outcome(
|
||||
rule_id=156, outcome="departed", why="the operator asked for main"
|
||||
)
|
||||
@@ -514,12 +514,12 @@ async def test_a_departure_without_a_reason_is_refused_at_the_door():
|
||||
and "it cannot be told from a miss" is the argument that lands."""
|
||||
rule = fake_rule(id=156, title="`dev` is home")
|
||||
with patch(
|
||||
"scribe.mcp.tools.rulebooks.rulebooks_svc.get_rule",
|
||||
"grimoire.mcp.tools.rulebooks.rulebooks_svc.get_rule",
|
||||
AsyncMock(return_value=rule),
|
||||
), patch(
|
||||
"scribe.mcp.tools.rulebooks.record_rule_outcome", MagicMock()
|
||||
"grimoire.mcp.tools.rulebooks.record_rule_outcome", MagicMock()
|
||||
) as rec:
|
||||
from scribe.mcp.tools.rulebooks import rule_outcome
|
||||
from grimoire.mcp.tools.rulebooks import rule_outcome
|
||||
with pytest.raises(ValueError, match="departure needs its reason"):
|
||||
await rule_outcome(rule_id=156, outcome="departed", why=" ")
|
||||
rec.assert_not_called()
|
||||
@@ -533,12 +533,12 @@ async def test_there_is_no_way_to_report_having_ignored_a_rule():
|
||||
error rather than a row that would read as measurement."""
|
||||
rule = fake_rule(id=156, title="`dev` is home")
|
||||
with patch(
|
||||
"scribe.mcp.tools.rulebooks.rulebooks_svc.get_rule",
|
||||
"grimoire.mcp.tools.rulebooks.rulebooks_svc.get_rule",
|
||||
AsyncMock(return_value=rule),
|
||||
), patch(
|
||||
"scribe.mcp.tools.rulebooks.record_rule_outcome", MagicMock()
|
||||
"grimoire.mcp.tools.rulebooks.record_rule_outcome", MagicMock()
|
||||
) as rec:
|
||||
from scribe.mcp.tools.rulebooks import rule_outcome
|
||||
from grimoire.mcp.tools.rulebooks import rule_outcome
|
||||
for bogus in ("ignored", "skipped", "read", ""):
|
||||
with pytest.raises(ValueError, match="must be 'applied' or 'departed'"):
|
||||
await rule_outcome(rule_id=156, outcome=bogus)
|
||||
@@ -551,12 +551,12 @@ async def test_rule_outcome_refuses_a_rule_the_caller_cannot_see():
|
||||
the existence of someone else's rule, and nothing is recorded against
|
||||
an id the caller has no claim on."""
|
||||
with patch(
|
||||
"scribe.mcp.tools.rulebooks.rulebooks_svc.get_rule",
|
||||
"grimoire.mcp.tools.rulebooks.rulebooks_svc.get_rule",
|
||||
AsyncMock(return_value=None),
|
||||
), patch(
|
||||
"scribe.mcp.tools.rulebooks.record_rule_outcome", MagicMock()
|
||||
"grimoire.mcp.tools.rulebooks.record_rule_outcome", MagicMock()
|
||||
) as rec:
|
||||
from scribe.mcp.tools.rulebooks import rule_outcome
|
||||
from grimoire.mcp.tools.rulebooks import rule_outcome
|
||||
with pytest.raises(ValueError, match="rule 999 not found"):
|
||||
await rule_outcome(rule_id=999, outcome="applied")
|
||||
rec.assert_not_called()
|
||||
@@ -566,8 +566,8 @@ def test_rule_outcome_is_registered_and_is_not_read_only():
|
||||
"""Its whole effect is a write, and `why` is prose the agent authored —
|
||||
a read-scoped key that can put text in the operator's database is not
|
||||
read-scoped, whatever table it lands in."""
|
||||
from scribe.mcp.server import _READ_ONLY_TOOLS, _WRITE_TOOLS
|
||||
from scribe.mcp.tools import rulebooks as mod
|
||||
from grimoire.mcp.server import _READ_ONLY_TOOLS, _WRITE_TOOLS
|
||||
from grimoire.mcp.tools import rulebooks as mod
|
||||
mcp = FakeMCP()
|
||||
mod.register(mcp)
|
||||
assert "rule_outcome" in mcp.names
|
||||
|
||||
Reference in New Issue
Block a user