CI & Build / Python lint (push) Successful in 3s
CI & Build / Plugin hooks (push) Successful in 12s
CI & Build / TypeScript typecheck (push) Successful in 52s
CI & Build / integration (push) Successful in 1m1s
CI & Build / Python tests (push) Successful in 1m55s
CI & Build / Build & push image (push) Successful in 36s
Step 1 of the Scribe -> Grimoire rename: every capitalised "Scribe" in prose
becomes "Grimoire" ("Fabled Scribe" -> "Fabled Grimoire") across the UI,
the PWA manifest and page title, the MCP instructions and tool docstrings,
hook and skill text, docs and README, with the tests that assert on that
prose moving with it. Old migrations keep their wording.
Identifiers are untouched here: the package, plugin name, MCP server key,
hook file names, env vars, the .scribe marker and FabledScribe repo/image
names move in steps 2 and 3, along with prose that quotes them.
The sweep matched "Scribe" only as a whole word, so describe/subscribe and
friends are untouched. The longer name took the MCP instructions one
character over their 1,600 budget; two phrases are shortened to fit.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
105 lines
4.2 KiB
Python
105 lines
4.2 KiB
Python
"""Shared records must announce themselves.
|
|
|
|
A record another user owns is that person's suggestion, not the operator's
|
|
settled practice — and the two are indistinguishable unless every surface that
|
|
hands one over says whose it is. These tests hold that line at the labelling
|
|
helpers and at the process→skill manifest, which is the most consequential
|
|
passive surface Grimoire has (each entry becomes an auto-surfacing skill file).
|
|
"""
|
|
from unittest.mock import AsyncMock, MagicMock, patch
|
|
|
|
import pytest
|
|
from tests.helpers import make_mock_session
|
|
|
|
|
|
def _session_returning_rows(rows):
|
|
result = MagicMock()
|
|
result.all.return_value = rows
|
|
session = make_mock_session()
|
|
session.execute = AsyncMock(return_value=result)
|
|
return session
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_label_marks_only_foreign_rows():
|
|
from scribe.services.access import label_shared_items
|
|
items = [
|
|
{"id": 1, "user_id": 7}, # mine
|
|
{"id": 2, "user_id": 9}, # someone else's
|
|
]
|
|
with patch("scribe.services.access.async_session") as cls:
|
|
cls.return_value = _session_returning_rows([(9, "alex")])
|
|
out = await label_shared_items(user_id=7, items=items)
|
|
|
|
mine, theirs = out
|
|
# An unmarked row must be unambiguously the caller's own.
|
|
assert "shared" not in mine and "owner" not in mine
|
|
assert theirs["shared"] is True
|
|
assert theirs["owner"] == "alex"
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_label_skips_the_query_when_everything_is_mine():
|
|
"""No foreign rows means no user lookup at all — labelling shouldn't cost a
|
|
query on the common single-owner path."""
|
|
from scribe.services.access import label_shared_items
|
|
with patch("scribe.services.access.async_session") as cls:
|
|
out = await label_shared_items(user_id=7, items=[{"id": 1, "user_id": 7}])
|
|
cls.assert_not_called()
|
|
assert out == [{"id": 1, "user_id": 7}]
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_provenance_is_empty_for_your_own_record():
|
|
from scribe.services.access import describe_provenance
|
|
note = MagicMock(id=1, user_id=7)
|
|
assert await describe_provenance(user_id=7, note=note) == {}
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_provenance_names_the_owner_and_permission():
|
|
from scribe.services.access import describe_provenance
|
|
note = MagicMock(id=1, user_id=9)
|
|
owner = MagicMock(username="alex")
|
|
session = make_mock_session()
|
|
session.get = AsyncMock(return_value=owner)
|
|
with patch("scribe.services.access.async_session") as cls, \
|
|
patch("scribe.services.access.get_note_permission",
|
|
AsyncMock(return_value="viewer")):
|
|
cls.return_value = session
|
|
got = await describe_provenance(user_id=7, note=note)
|
|
assert got == {"shared": True, "owner": "alex", "permission": "viewer"}
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_shared_process_skill_stub_never_claims_to_be_the_operators():
|
|
"""The stub description IS the skill's auto-surface trigger and the only
|
|
provenance a reader sees. For a shared Process it must name the author and
|
|
require a go-ahead — never read as "the operator's saved process"."""
|
|
from scribe.services import plugin_context
|
|
|
|
items = [
|
|
{"id": 1, "title": "Drift Audit", "snippet": "Sweep for drift.", "user_id": 7},
|
|
{"id": 2, "title": "Deploy Dance", "snippet": "Ship it.", "user_id": 9},
|
|
]
|
|
with patch.object(plugin_context.knowledge_svc, "query_knowledge",
|
|
AsyncMock(return_value=(items, 2))), \
|
|
patch.object(plugin_context, "label_shared_items",
|
|
AsyncMock(side_effect=lambda uid, its: [
|
|
it if it["user_id"] == uid
|
|
else {**it, "shared": True, "owner": "alex"}
|
|
for it in its
|
|
])):
|
|
out = await plugin_context.build_process_manifest(user_id=7)
|
|
|
|
own, shared = out["processes"]
|
|
|
|
assert "operator's saved Grimoire process" in own["description"]
|
|
assert "shared" not in own
|
|
|
|
assert shared["shared"] is True
|
|
assert shared["owner"] == "alex"
|
|
assert "operator's saved Grimoire process" not in shared["description"]
|
|
assert "alex" in shared["description"]
|
|
assert "go-ahead" in shared["description"]
|