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:
@@ -10,7 +10,7 @@ pytestmark = pytest.mark.usefixtures("_bind_user")
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_create_snippet_requires_name_and_code():
|
||||
from scribe.mcp.tools.snippets import create_snippet
|
||||
from grimoire.mcp.tools.snippets import create_snippet
|
||||
with pytest.raises(ValueError):
|
||||
await create_snippet(name="", code="x")
|
||||
with pytest.raises(ValueError):
|
||||
@@ -20,10 +20,10 @@ async def test_create_snippet_requires_name_and_code():
|
||||
@pytest.mark.asyncio
|
||||
async def test_create_snippet_records_and_returns_parsed():
|
||||
created = fake_snippet()
|
||||
with patch("scribe.services.dedup.find_duplicate_note", AsyncMock(return_value=None)), \
|
||||
patch("scribe.services.snippets.create_snippet",
|
||||
with patch("grimoire.services.dedup.find_duplicate_note", AsyncMock(return_value=None)), \
|
||||
patch("grimoire.services.snippets.create_snippet",
|
||||
AsyncMock(return_value=created)) as mock_create:
|
||||
from scribe.mcp.tools.snippets import create_snippet
|
||||
from grimoire.mcp.tools.snippets import create_snippet
|
||||
out = await create_snippet(
|
||||
name="debounce", code="return 1", language="js",
|
||||
when_to_use="rate-limit a callback",
|
||||
@@ -37,11 +37,11 @@ async def test_create_snippet_records_and_returns_parsed():
|
||||
@pytest.mark.asyncio
|
||||
async def test_create_snippet_dedup_blocks_and_labels_snippet():
|
||||
dup = MagicMock(id=99)
|
||||
with patch("scribe.services.dedup.find_duplicate_note", AsyncMock(return_value=dup)), \
|
||||
patch("scribe.services.dedup.duplicate_response",
|
||||
with patch("grimoire.services.dedup.find_duplicate_note", AsyncMock(return_value=dup)), \
|
||||
patch("grimoire.services.dedup.duplicate_response",
|
||||
MagicMock(return_value={"duplicate": True, "existing_id": 99})) as mock_resp, \
|
||||
patch("scribe.services.snippets.create_snippet", AsyncMock()) as mock_create:
|
||||
from scribe.mcp.tools.snippets import create_snippet
|
||||
patch("grimoire.services.snippets.create_snippet", AsyncMock()) as mock_create:
|
||||
from grimoire.mcp.tools.snippets import create_snippet
|
||||
out = await create_snippet(name="debounce", code="return 1")
|
||||
assert out["duplicate"] is True
|
||||
mock_create.assert_not_awaited()
|
||||
@@ -50,16 +50,16 @@ async def test_create_snippet_dedup_blocks_and_labels_snippet():
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_snippet_not_found_raises():
|
||||
with patch("scribe.services.snippets.get_snippet", AsyncMock(return_value=None)):
|
||||
from scribe.mcp.tools.snippets import get_snippet
|
||||
with patch("grimoire.services.snippets.get_snippet", AsyncMock(return_value=None)):
|
||||
from grimoire.mcp.tools.snippets import get_snippet
|
||||
with pytest.raises(ValueError):
|
||||
await get_snippet(123)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_update_snippet_missing_raises():
|
||||
with patch("scribe.services.snippets.update_snippet", AsyncMock(return_value=None)):
|
||||
from scribe.mcp.tools.snippets import update_snippet
|
||||
with patch("grimoire.services.snippets.update_snippet", AsyncMock(return_value=None)):
|
||||
from grimoire.mcp.tools.snippets import update_snippet
|
||||
with pytest.raises(ValueError):
|
||||
await update_snippet(123, name="x")
|
||||
|
||||
@@ -69,9 +69,9 @@ async def test_update_snippet_empty_string_clears_a_field():
|
||||
# An omitted field must stay None ("leave alone"), but an explicit empty
|
||||
# string has to reach the service as "" so a stale field can be removed.
|
||||
updated = fake_snippet()
|
||||
with patch("scribe.services.snippets.update_snippet",
|
||||
with patch("grimoire.services.snippets.update_snippet",
|
||||
AsyncMock(return_value=updated)) as mock_update:
|
||||
from scribe.mcp.tools.snippets import update_snippet
|
||||
from grimoire.mcp.tools.snippets import update_snippet
|
||||
await update_snippet(1, signature="")
|
||||
kwargs = mock_update.await_args.kwargs
|
||||
assert kwargs["signature"] == "" # cleared
|
||||
@@ -80,13 +80,13 @@ async def test_update_snippet_empty_string_clears_a_field():
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_update_snippet_project_id_conventions():
|
||||
from scribe.services import snippets as snippets_svc
|
||||
from grimoire.services import snippets as snippets_svc
|
||||
updated = fake_snippet()
|
||||
cases = {0: snippets_svc.UNSET, -1: None, 5: 5}
|
||||
for given, expected in cases.items():
|
||||
with patch("scribe.services.snippets.update_snippet",
|
||||
with patch("grimoire.services.snippets.update_snippet",
|
||||
AsyncMock(return_value=updated)) as mock_update:
|
||||
from scribe.mcp.tools.snippets import update_snippet
|
||||
from grimoire.mcp.tools.snippets import update_snippet
|
||||
await update_snippet(1, project_id=given)
|
||||
assert mock_update.await_args.kwargs["project_id"] is expected
|
||||
|
||||
@@ -96,16 +96,16 @@ async def test_create_and_update_pass_locations_through():
|
||||
locs = [{"repo": "a", "path": "a.py", "symbol": "f"},
|
||||
{"repo": "b", "path": "b.py", "symbol": "g"}]
|
||||
created = fake_snippet()
|
||||
with patch("scribe.services.dedup.find_duplicate_note", AsyncMock(return_value=None)), \
|
||||
patch("scribe.services.snippets.create_snippet",
|
||||
with patch("grimoire.services.dedup.find_duplicate_note", AsyncMock(return_value=None)), \
|
||||
patch("grimoire.services.snippets.create_snippet",
|
||||
AsyncMock(return_value=created)) as mock_create:
|
||||
from scribe.mcp.tools.snippets import create_snippet
|
||||
from grimoire.mcp.tools.snippets import create_snippet
|
||||
await create_snippet(name="f", code="x", locations=locs)
|
||||
assert mock_create.await_args.kwargs["locations"] == locs
|
||||
|
||||
with patch("scribe.services.snippets.update_snippet",
|
||||
with patch("grimoire.services.snippets.update_snippet",
|
||||
AsyncMock(return_value=created)) as mock_update:
|
||||
from scribe.mcp.tools.snippets import update_snippet
|
||||
from grimoire.mcp.tools.snippets import update_snippet
|
||||
await update_snippet(1, locations=locs)
|
||||
assert mock_update.await_args.kwargs["locations"] == locs
|
||||
|
||||
@@ -115,50 +115,50 @@ async def test_update_snippet_read_only_share_says_why():
|
||||
"""A viewer grant must be refused with the REAL reason. "Not found" would be
|
||||
a lie about a record the caller can plainly open, and would send an agent
|
||||
hunting for a missing id instead of recording its own version."""
|
||||
with patch("scribe.services.snippets.update_snippet",
|
||||
with patch("grimoire.services.snippets.update_snippet",
|
||||
AsyncMock(side_effect=PermissionError(
|
||||
"snippet 1 is shared with you read-only — ask its owner for "
|
||||
"edit access, or record your own version"))):
|
||||
from scribe.mcp.tools.snippets import update_snippet
|
||||
from grimoire.mcp.tools.snippets import update_snippet
|
||||
with pytest.raises(ValueError, match="read-only"):
|
||||
await update_snippet(1, name="x")
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_merge_snippets_read_only_target_says_why():
|
||||
with patch("scribe.services.snippets.merge_snippets",
|
||||
with patch("grimoire.services.snippets.merge_snippets",
|
||||
AsyncMock(side_effect=PermissionError(
|
||||
"snippet 1 is shared with you read-only — you can't merge "
|
||||
"into a record you can't edit"))):
|
||||
from scribe.mcp.tools.snippets import merge_snippets
|
||||
from grimoire.mcp.tools.snippets import merge_snippets
|
||||
with pytest.raises(ValueError, match="read-only"):
|
||||
await merge_snippets(target_id=1, source_ids=[2])
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_delete_snippet_retires_or_raises():
|
||||
from scribe.mcp.tools.snippets import delete_snippet
|
||||
from grimoire.mcp.tools.snippets import delete_snippet
|
||||
|
||||
doomed = MagicMock()
|
||||
doomed.title = "debounce helper"
|
||||
# The confirmation names what went — after the delete there is
|
||||
# nothing left to look the title up from.
|
||||
with patch("scribe.services.snippets.delete_snippet", AsyncMock(return_value=True)), \
|
||||
patch("scribe.services.snippets.get_snippet", AsyncMock(return_value=doomed)):
|
||||
with patch("grimoire.services.snippets.delete_snippet", AsyncMock(return_value=True)), \
|
||||
patch("grimoire.services.snippets.get_snippet", AsyncMock(return_value=doomed)):
|
||||
assert await delete_snippet(1) == {
|
||||
"deleted": True, "id": 1, "title": "debounce helper",
|
||||
}
|
||||
with patch("scribe.services.snippets.delete_snippet", AsyncMock(return_value=False)), \
|
||||
patch("scribe.services.snippets.get_snippet", AsyncMock(return_value=None)):
|
||||
with patch("grimoire.services.snippets.delete_snippet", AsyncMock(return_value=False)), \
|
||||
patch("grimoire.services.snippets.get_snippet", AsyncMock(return_value=None)):
|
||||
with pytest.raises(ValueError):
|
||||
await delete_snippet(404)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_list_snippets_defaults_to_every_project():
|
||||
with patch("scribe.services.snippets.list_snippets",
|
||||
with patch("grimoire.services.snippets.list_snippets",
|
||||
AsyncMock(return_value=([], 0))) as mock_list:
|
||||
from scribe.mcp.tools.snippets import list_snippets
|
||||
from grimoire.mcp.tools.snippets import list_snippets
|
||||
await list_snippets(q="debounce")
|
||||
assert mock_list.await_args.kwargs["project_id"] is None
|
||||
await list_snippets(q="debounce", project_id=3)
|
||||
@@ -167,7 +167,7 @@ async def test_list_snippets_defaults_to_every_project():
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_merge_snippets_requires_a_source():
|
||||
from scribe.mcp.tools.snippets import merge_snippets
|
||||
from grimoire.mcp.tools.snippets import merge_snippets
|
||||
with pytest.raises(ValueError):
|
||||
await merge_snippets(target_id=1, source_ids=[])
|
||||
with pytest.raises(ValueError):
|
||||
@@ -177,9 +177,9 @@ async def test_merge_snippets_requires_a_source():
|
||||
@pytest.mark.asyncio
|
||||
async def test_merge_snippets_returns_survivor_and_merged_ids():
|
||||
survivor = fake_snippet()
|
||||
with patch("scribe.services.snippets.merge_snippets",
|
||||
with patch("grimoire.services.snippets.merge_snippets",
|
||||
AsyncMock(return_value=(survivor, [2, 3]))) as mock_merge:
|
||||
from scribe.mcp.tools.snippets import merge_snippets
|
||||
from grimoire.mcp.tools.snippets import merge_snippets
|
||||
out = await merge_snippets(target_id=1, source_ids=[2, 3, 1])
|
||||
assert out["merged_ids"] == [2, 3]
|
||||
assert out["snippet"]["name"] == "debounce"
|
||||
@@ -190,14 +190,14 @@ async def test_merge_snippets_returns_survivor_and_merged_ids():
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_merge_snippets_not_found_raises():
|
||||
with patch("scribe.services.snippets.merge_snippets", AsyncMock(return_value=None)):
|
||||
from scribe.mcp.tools.snippets import merge_snippets
|
||||
with patch("grimoire.services.snippets.merge_snippets", AsyncMock(return_value=None)):
|
||||
from grimoire.mcp.tools.snippets import merge_snippets
|
||||
with pytest.raises(ValueError):
|
||||
await merge_snippets(target_id=1, source_ids=[2])
|
||||
|
||||
|
||||
def test_register_attaches_all_tools():
|
||||
from scribe.mcp.tools import snippets
|
||||
from grimoire.mcp.tools import snippets
|
||||
mcp = FakeMCP()
|
||||
|
||||
snippets.register(mcp)
|
||||
|
||||
Reference in New Issue
Block a user