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:
+29
-29
@@ -45,7 +45,7 @@ def _bind_user():
|
||||
``pytestmark = pytest.mark.usefixtures("_bind_user")`` and builds its fakes
|
||||
with user_id=7 so ownership checks see the caller as the owner.
|
||||
"""
|
||||
from scribe.mcp._context import _user_id_ctx
|
||||
from grimoire.mcp._context import _user_id_ctx
|
||||
|
||||
token = _user_id_ctx.set(7)
|
||||
yield
|
||||
@@ -63,7 +63,7 @@ async def _dispose_engine():
|
||||
pool cleanly. The import is deferred so merely collecting a module that
|
||||
mixes unit and integration tests never builds an engine.
|
||||
"""
|
||||
from scribe.models import engine
|
||||
from grimoire.models import engine
|
||||
|
||||
yield
|
||||
await engine.dispose()
|
||||
@@ -82,8 +82,8 @@ def _no_embedding():
|
||||
|
||||
# Milestones embed too; a plan created in a test would
|
||||
# otherwise detach the same model-loading task.
|
||||
with patch("scribe.services.notes.embed_note", MagicMock()), \
|
||||
patch("scribe.services.milestones.embed_milestone", MagicMock()):
|
||||
with patch("grimoire.services.notes.embed_note", MagicMock()), \
|
||||
patch("grimoire.services.milestones.embed_milestone", MagicMock()):
|
||||
yield
|
||||
|
||||
|
||||
@@ -97,7 +97,7 @@ def _no_supersession():
|
||||
lie about what it does. The label's own behaviour is covered in
|
||||
tests/test_supersession_ranking.py.
|
||||
"""
|
||||
with patch("scribe.services.plugin_context.superseded_ids",
|
||||
with patch("grimoire.services.plugin_context.superseded_ids",
|
||||
AsyncMock(return_value=set())):
|
||||
yield
|
||||
|
||||
@@ -112,7 +112,7 @@ def _no_reply_shape_log():
|
||||
tests/test_reply_shapes.py binds the real function at import time,
|
||||
before this patch runs.
|
||||
"""
|
||||
with patch("scribe.services.reply_shapes.record_delivery", AsyncMock()):
|
||||
with patch("grimoire.services.reply_shapes.record_delivery", AsyncMock()):
|
||||
yield
|
||||
|
||||
|
||||
@@ -125,7 +125,7 @@ def _no_system_labels():
|
||||
to "no labels", the state of any untagged record. Tests of the label
|
||||
itself patch it with a value.
|
||||
"""
|
||||
with patch("scribe.services.plugin_context.system_names_for",
|
||||
with patch("grimoire.services.plugin_context.system_names_for",
|
||||
AsyncMock(return_value={})):
|
||||
yield
|
||||
|
||||
@@ -140,7 +140,7 @@ def _no_rulings_arm():
|
||||
tests/test_system_rulings.py binds the real function at import time,
|
||||
before this patch runs.
|
||||
"""
|
||||
with patch("scribe.services.system_rulings.rulings_for_paths",
|
||||
with patch("grimoire.services.system_rulings.rulings_for_paths",
|
||||
AsyncMock(return_value={"lines": [], "system_ids": []})):
|
||||
yield
|
||||
|
||||
@@ -154,12 +154,12 @@ def _no_system_usage_readout():
|
||||
the zero shape for whatever ids were asked. tests/test_system_usage_readout.py
|
||||
binds the real function at import time, before this patch runs.
|
||||
"""
|
||||
from scribe.services.system_usage import empty_system_usage
|
||||
from grimoire.services.system_usage import empty_system_usage
|
||||
|
||||
async def _zeros(system_ids):
|
||||
return {int(s): empty_system_usage() for s in system_ids}
|
||||
|
||||
with patch("scribe.services.system_usage.usage_for_systems",
|
||||
with patch("grimoire.services.system_usage.usage_for_systems",
|
||||
AsyncMock(side_effect=_zeros)):
|
||||
yield
|
||||
|
||||
@@ -181,11 +181,11 @@ def _no_task_log_arm():
|
||||
tests/test_integration_task_work_log.py. A test that wants the arm live
|
||||
re-patches it, same as the rules arm.
|
||||
"""
|
||||
with patch("scribe.services.task_logs.logs_for_task",
|
||||
with patch("grimoire.services.task_logs.logs_for_task",
|
||||
AsyncMock(return_value=[])), \
|
||||
patch("scribe.services.task_logs.count_logs_for_task",
|
||||
patch("grimoire.services.task_logs.count_logs_for_task",
|
||||
AsyncMock(return_value=0)), \
|
||||
patch("scribe.services.task_logs.log_counts_for_tasks",
|
||||
patch("grimoire.services.task_logs.log_counts_for_tasks",
|
||||
AsyncMock(return_value={})):
|
||||
yield
|
||||
|
||||
@@ -218,13 +218,13 @@ def _no_lesson_rule_links(request):
|
||||
if request.node.get_closest_marker("integration"):
|
||||
yield
|
||||
return
|
||||
with patch("scribe.services.lesson_rules.attach_lesson_rules", AsyncMock()), \
|
||||
patch("scribe.services.lesson_rules.attach_rule_lessons", AsyncMock()), \
|
||||
patch("scribe.services.lesson_rules.rule_candidates", AsyncMock(return_value=[])), \
|
||||
patch("scribe.services.lesson_rules.co_surfaced", AsyncMock(return_value="")), \
|
||||
patch("scribe.services.plugin_context._rules_via_lessons",
|
||||
with patch("grimoire.services.lesson_rules.attach_lesson_rules", AsyncMock()), \
|
||||
patch("grimoire.services.lesson_rules.attach_rule_lessons", AsyncMock()), \
|
||||
patch("grimoire.services.lesson_rules.rule_candidates", AsyncMock(return_value=[])), \
|
||||
patch("grimoire.services.lesson_rules.co_surfaced", AsyncMock(return_value="")), \
|
||||
patch("grimoire.services.plugin_context._rules_via_lessons",
|
||||
AsyncMock(return_value=([], []))), \
|
||||
patch("scribe.services.lesson_rules.convergence_for", AsyncMock(return_value=None)):
|
||||
patch("grimoire.services.lesson_rules.convergence_for", AsyncMock(return_value=None)):
|
||||
yield
|
||||
|
||||
|
||||
@@ -245,10 +245,10 @@ def _no_family_triggers(request):
|
||||
if request.node.get_closest_marker("integration"):
|
||||
yield
|
||||
return
|
||||
with patch("scribe.services.family.citation_trigger", AsyncMock(return_value=None)), \
|
||||
patch("scribe.services.family.repeat_trigger", AsyncMock(return_value=None)), \
|
||||
patch("scribe.services.family.milestone_trigger", AsyncMock(return_value=None)), \
|
||||
patch("scribe.services.family.milestone_is_open", AsyncMock(return_value=False)):
|
||||
with patch("grimoire.services.family.citation_trigger", AsyncMock(return_value=None)), \
|
||||
patch("grimoire.services.family.repeat_trigger", AsyncMock(return_value=None)), \
|
||||
patch("grimoire.services.family.milestone_trigger", AsyncMock(return_value=None)), \
|
||||
patch("grimoire.services.family.milestone_is_open", AsyncMock(return_value=False)):
|
||||
yield
|
||||
|
||||
|
||||
@@ -266,9 +266,9 @@ def _no_moment_delivery(request):
|
||||
if request.node.get_closest_marker("integration"):
|
||||
yield
|
||||
return
|
||||
from scribe.services.retrieval_pipeline import RuleResult
|
||||
from grimoire.services.retrieval_pipeline import RuleResult
|
||||
|
||||
with patch("scribe.services.moment_delivery.deliver_for_act",
|
||||
with patch("grimoire.services.moment_delivery.deliver_for_act",
|
||||
AsyncMock(return_value=([], RuleResult()))):
|
||||
yield
|
||||
|
||||
@@ -289,7 +289,7 @@ def _no_rule_arm():
|
||||
channel in tests/test_write_path_trigger.py. A test that wants the arm
|
||||
live can re-patch it.
|
||||
"""
|
||||
with patch("scribe.services.plugin_context.semantic_search_rules",
|
||||
with patch("grimoire.services.plugin_context.semantic_search_rules",
|
||||
AsyncMock(return_value=[])):
|
||||
yield
|
||||
|
||||
@@ -306,7 +306,7 @@ def _no_rule_overlap():
|
||||
tests/test_rule_overlap_gate.py, which binds the real function at import
|
||||
time — before this patch runs — and stubs the search beneath it instead.
|
||||
"""
|
||||
with patch("scribe.services.dedup.find_overlapping_rules",
|
||||
with patch("grimoire.services.dedup.find_overlapping_rules",
|
||||
AsyncMock(return_value=[])):
|
||||
yield
|
||||
|
||||
@@ -323,10 +323,10 @@ def _default_gate_bars():
|
||||
read itself is tested in tests/test_gate_settings.py, which binds the real
|
||||
function at import time, before this patch runs.
|
||||
"""
|
||||
from scribe.services.dedup import GATE_DEFAULT_THRESHOLDS
|
||||
from grimoire.services.dedup import GATE_DEFAULT_THRESHOLDS
|
||||
|
||||
async def _default(user_id, key, lo):
|
||||
return GATE_DEFAULT_THRESHOLDS[key]
|
||||
|
||||
with patch("scribe.services.dedup._gate_setting", AsyncMock(side_effect=_default)):
|
||||
with patch("grimoire.services.dedup._gate_setting", AsyncMock(side_effect=_default)):
|
||||
yield
|
||||
|
||||
+11
-11
@@ -26,12 +26,12 @@ async def drive_update_note(note, **kwargs):
|
||||
result = MagicMock()
|
||||
result.scalars.return_value.first.return_value = note
|
||||
session.execute = _AsyncMock(return_value=result)
|
||||
with patch("scribe.services.notes.async_session") as cls, \
|
||||
patch("scribe.services.notes.embed_note", MagicMock()), \
|
||||
patch("scribe.services.notes._maybe_reactivate_project", _AsyncMock()), \
|
||||
patch("scribe.services.note_versions.create_version", _AsyncMock()):
|
||||
with patch("grimoire.services.notes.async_session") as cls, \
|
||||
patch("grimoire.services.notes.embed_note", MagicMock()), \
|
||||
patch("grimoire.services.notes._maybe_reactivate_project", _AsyncMock()), \
|
||||
patch("grimoire.services.note_versions.create_version", _AsyncMock()):
|
||||
cls.return_value = session
|
||||
from scribe.services.notes import update_note
|
||||
from grimoire.services.notes import update_note
|
||||
return await update_note(user_id=7, note_id=note.id, **kwargs)
|
||||
|
||||
|
||||
@@ -111,7 +111,7 @@ async def ensure_user(session, username: str, role: str = "user"):
|
||||
"""
|
||||
from sqlalchemy import select
|
||||
|
||||
from scribe.models.user import User
|
||||
from grimoire.models.user import User
|
||||
|
||||
existing = (
|
||||
await session.execute(select(User).where(User.username == username))
|
||||
@@ -309,7 +309,7 @@ def plain_rule_detail():
|
||||
"""
|
||||
async def _detail(_uid, rule, _system_ids=None, _moments=None):
|
||||
return rule.to_dict()
|
||||
return patch("scribe.mcp.tools.rulebooks.rulebooks_svc.rule_detail", _detail)
|
||||
return patch("grimoire.mcp.tools.rulebooks.rulebooks_svc.rule_detail", _detail)
|
||||
|
||||
|
||||
class FakeMCP:
|
||||
@@ -420,8 +420,8 @@ def writepath_cfg(**over):
|
||||
real_one_does` is what makes the next addition fail loudly here rather than
|
||||
silently no-op an arm, which is the whole claim this docstring makes.
|
||||
"""
|
||||
from scribe.services.plugin_context import _CHECKPOINT_DEFAULT
|
||||
from scribe.services.retrieval_surfaces import SURFACES
|
||||
from grimoire.services.plugin_context import _CHECKPOINT_DEFAULT
|
||||
from grimoire.services.retrieval_surfaces import SURFACES
|
||||
|
||||
cfg = {
|
||||
"enabled": True,
|
||||
@@ -475,8 +475,8 @@ async def rule_row(rule_id: int):
|
||||
database and must not pay for one — the same reason `plugin_config` defers
|
||||
its service imports.
|
||||
"""
|
||||
from scribe.models import async_session
|
||||
from scribe.models.rulebook import Rule
|
||||
from grimoire.models import async_session
|
||||
from grimoire.models.rulebook import Rule
|
||||
|
||||
async with async_session() as s:
|
||||
return await s.get(Rule, rule_id)
|
||||
|
||||
+10
-10
@@ -4,7 +4,7 @@ from unittest.mock import AsyncMock, MagicMock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
from scribe.services.api_keys import (
|
||||
from grimoire.services.api_keys import (
|
||||
hash_token,
|
||||
_key_prefix,
|
||||
generate_key,
|
||||
@@ -45,7 +45,7 @@ async def test_create_api_key_returns_full_key():
|
||||
mock_key_obj.id = 1
|
||||
mock_key_obj.to_dict.return_value = {"id": 1, "name": "test", "scope": "read", "key_prefix": "fmcp_xxx"}
|
||||
|
||||
with patch("scribe.services.api_keys.async_session") as mock_session_ctx:
|
||||
with patch("grimoire.services.api_keys.async_session") as mock_session_ctx:
|
||||
mock_session = make_mock_session()
|
||||
mock_session.add = MagicMock()
|
||||
mock_session.commit = AsyncMock()
|
||||
@@ -56,7 +56,7 @@ async def test_create_api_key_returns_full_key():
|
||||
mock_session_ctx.return_value = mock_session
|
||||
|
||||
# Patch the select result
|
||||
with patch("scribe.services.api_keys.ApiKey") as mock_model:
|
||||
with patch("grimoire.services.api_keys.ApiKey") as mock_model:
|
||||
mock_model.return_value = mock_key_obj
|
||||
full_key, key_dict = await create_api_key(user_id=1, name="test", scope="read")
|
||||
|
||||
@@ -65,7 +65,7 @@ async def test_create_api_key_returns_full_key():
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_lookup_key_returns_none_for_unknown():
|
||||
with patch("scribe.services.api_keys.async_session") as mock_session_ctx:
|
||||
with patch("grimoire.services.api_keys.async_session") as mock_session_ctx:
|
||||
mock_session = make_mock_session()
|
||||
mock_result = MagicMock()
|
||||
mock_result.scalars.return_value.first.return_value = None
|
||||
@@ -109,15 +109,15 @@ async def test_read_only_key_blocked_on_post():
|
||||
from quart import jsonify
|
||||
return jsonify({"ok": True})
|
||||
|
||||
from scribe.auth import _check_auth
|
||||
from grimoire.auth import _check_auth
|
||||
wrapped = _check_auth(dummy)
|
||||
|
||||
async with app.test_request_context(
|
||||
"/test", method="POST",
|
||||
headers={"Authorization": "Bearer fmcp_readonly"},
|
||||
):
|
||||
with patch("scribe.auth.lookup_key", AsyncMock(return_value=fake_key)), \
|
||||
patch("scribe.auth.get_user_by_id", AsyncMock(return_value=fake_user)):
|
||||
with patch("grimoire.auth.lookup_key", AsyncMock(return_value=fake_key)), \
|
||||
patch("grimoire.auth.get_user_by_id", AsyncMock(return_value=fake_user)):
|
||||
resp = await wrapped()
|
||||
assert resp[1] == 403
|
||||
|
||||
@@ -146,15 +146,15 @@ async def test_bearer_token_valid_write_key_passes():
|
||||
from quart import jsonify
|
||||
return jsonify({"ok": True})
|
||||
|
||||
from scribe.auth import _check_auth
|
||||
from grimoire.auth import _check_auth
|
||||
wrapped = _check_auth(dummy)
|
||||
|
||||
async with app.test_request_context(
|
||||
"/test", method="POST",
|
||||
headers={"Authorization": "Bearer fmcp_writekey"},
|
||||
):
|
||||
with patch("scribe.auth.lookup_key", AsyncMock(return_value=fake_key)), \
|
||||
patch("scribe.auth.get_user_by_id", AsyncMock(return_value=fake_user)):
|
||||
with patch("grimoire.auth.lookup_key", AsyncMock(return_value=fake_key)), \
|
||||
patch("grimoire.auth.get_user_by_id", AsyncMock(return_value=fake_user)):
|
||||
resp = await wrapped()
|
||||
|
||||
# Should not be a tuple (no error response)
|
||||
|
||||
@@ -16,7 +16,7 @@ from unittest.mock import AsyncMock, MagicMock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
from scribe.services.plugin_context import (
|
||||
from grimoire.services.plugin_context import (
|
||||
_AUTOINJECT_CONTEXT_MAX,
|
||||
_AUTOINJECT_CONTEXT_PROMPT_MAX,
|
||||
_autoinject_query,
|
||||
@@ -144,7 +144,7 @@ def test_a_title_only_match_hands_over_the_trigger_it_matched_on():
|
||||
|
||||
|
||||
async def _menu(hits, seen, chunks, systems=None):
|
||||
from scribe.services import plugin_context as pc
|
||||
from grimoire.services import plugin_context as pc
|
||||
|
||||
calls: list[int] = []
|
||||
|
||||
|
||||
@@ -15,7 +15,7 @@ from unittest.mock import AsyncMock, MagicMock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
from scribe.services import embeddings as emb
|
||||
from grimoire.services import embeddings as emb
|
||||
|
||||
|
||||
def _ctx(session):
|
||||
|
||||
@@ -28,9 +28,9 @@ from unittest.mock import AsyncMock, MagicMock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
from scribe.services import embeddings as emb
|
||||
from scribe.services import retrieval_tuning as rt
|
||||
from scribe.services.retrieval_surfaces import SURFACES, get_surface
|
||||
from grimoire.services import embeddings as emb
|
||||
from grimoire.services import retrieval_tuning as rt
|
||||
from grimoire.services.retrieval_surfaces import SURFACES, get_surface
|
||||
from tests.helpers import make_mock_session, tool_doc
|
||||
|
||||
# Names for the thing that talks, as opposed to the thing that embeds. Any of
|
||||
@@ -72,7 +72,7 @@ def test_no_chat_model_identifier_reaches_the_calibration_path():
|
||||
"""
|
||||
import inspect
|
||||
|
||||
from scribe.services import retrieval_migration as rm
|
||||
from grimoire.services import retrieval_migration as rm
|
||||
|
||||
for label, obj in (
|
||||
("calibration_stamp", emb.calibration_stamp),
|
||||
@@ -183,7 +183,7 @@ def test_the_tool_says_nothing_auto_retunes():
|
||||
Without this, the obvious next move on seeing `stale: true` is to move the
|
||||
dial — which is tuning from a statistic, which can point the wrong way.
|
||||
"""
|
||||
doc = tool_doc("scribe.mcp.tools.retrieval_tuning", "retrieval_surfaces")
|
||||
doc = tool_doc("grimoire.mcp.tools.retrieval_tuning", "retrieval_surfaces")
|
||||
assert "calibration" in doc
|
||||
assert "NOTHING IS RETUNED AUTOMATICALLY" in doc
|
||||
assert "unstamped" in doc
|
||||
|
||||
@@ -10,7 +10,7 @@ its topical anchor.
|
||||
"""
|
||||
import re
|
||||
|
||||
from scribe.services.embeddings import (
|
||||
from grimoire.services.embeddings import (
|
||||
_CHUNK_CHAR_BUDGET,
|
||||
_MIN_CHUNK_CONTENT,
|
||||
chunk_document,
|
||||
@@ -228,7 +228,7 @@ async def test_search_collapses_chunk_rows_to_best_chunk_per_note():
|
||||
long record fills the top-k with copies of itself."""
|
||||
from unittest.mock import AsyncMock, MagicMock, patch
|
||||
|
||||
from scribe.services import embeddings as emb
|
||||
from grimoire.services import embeddings as emb
|
||||
|
||||
note_a, note_b = MagicMock(id=1), MagicMock(id=2)
|
||||
# Rows are (Note, distance, chunk_index, chunk_text) — the chunk columns
|
||||
@@ -289,7 +289,7 @@ def _session_ctx(log_rows=()):
|
||||
async def test_upsert_stores_one_versioned_row_per_chunk():
|
||||
from unittest.mock import AsyncMock, patch
|
||||
|
||||
from scribe.services import embeddings as emb
|
||||
from grimoire.services import embeddings as emb
|
||||
|
||||
body = "\n\n".join(
|
||||
f"## Section {i}\n\n{_long_section(f'sec-{i}')}" for i in range(6)
|
||||
@@ -324,7 +324,7 @@ async def test_a_tasks_work_logs_reach_the_rows_it_is_stored_as():
|
||||
import datetime
|
||||
from unittest.mock import AsyncMock, patch
|
||||
|
||||
from scribe.services import embeddings as emb
|
||||
from grimoire.services import embeddings as emb
|
||||
|
||||
session, ctx = _session_ctx(
|
||||
log_rows=[(datetime.datetime(2026, 9, 20), "ruled out the cache theory")]
|
||||
@@ -348,7 +348,7 @@ async def test_upsert_of_an_emptied_record_clears_rows_instead_of_embedding():
|
||||
longer says."""
|
||||
from unittest.mock import patch
|
||||
|
||||
from scribe.services import embeddings as emb
|
||||
from grimoire.services import embeddings as emb
|
||||
|
||||
session, ctx = _session_ctx()
|
||||
with (
|
||||
@@ -369,7 +369,7 @@ async def test_backfill_reembeds_notes_with_a_stale_chunker_version():
|
||||
table wipe. Only rows AT the current version count as done."""
|
||||
from unittest.mock import AsyncMock, MagicMock, patch
|
||||
|
||||
from scribe.services import embeddings as emb
|
||||
from grimoire.services import embeddings as emb
|
||||
|
||||
def _ids(*values):
|
||||
r = MagicMock()
|
||||
@@ -404,10 +404,10 @@ def test_a_row_is_current_only_in_the_live_models_space():
|
||||
"""The column is a width, not an identity, so a same-width model
|
||||
swap writes a second geometry with no error. The backfill's "current"
|
||||
predicate has to name BOTH halves of the stamp, on every embedding table."""
|
||||
from scribe.models.embedding import (
|
||||
from grimoire.models.embedding import (
|
||||
MilestoneEmbedding, NoteEmbedding, RuleEmbedding, SystemEmbedding,
|
||||
)
|
||||
from scribe.services import embeddings as emb
|
||||
from grimoire.services import embeddings as emb
|
||||
|
||||
for table in (NoteEmbedding, RuleEmbedding, MilestoneEmbedding, SystemEmbedding):
|
||||
clause = str(emb.is_current_stamp(table))
|
||||
|
||||
@@ -30,8 +30,8 @@ from unittest.mock import AsyncMock, MagicMock, patch
|
||||
import pytest
|
||||
from sqlalchemy import true
|
||||
|
||||
from scribe.services import milestones as ms
|
||||
from scribe.services import placement as pl
|
||||
from grimoire.services import milestones as ms
|
||||
from grimoire.services import placement as pl
|
||||
from tests.helpers import fake_note, make_mock_session
|
||||
|
||||
|
||||
@@ -243,7 +243,7 @@ async def test_the_listing_and_placement_agree_on_what_open_means():
|
||||
|
||||
def test_a_task_line_in_the_menu_says_where_the_work_stands():
|
||||
"""The other half: an id and a title with no status is what got cited."""
|
||||
from scribe.services.plugin_context import _record_kind
|
||||
from grimoire.services.plugin_context import _record_kind
|
||||
|
||||
assert _record_kind(fake_note(is_task=True, status="done")) == "task (done)"
|
||||
assert _record_kind(fake_note(is_task=True, status="todo")) == "task (todo)"
|
||||
@@ -255,7 +255,7 @@ def test_a_task_line_in_the_menu_says_where_the_work_stands():
|
||||
def test_a_record_with_no_status_gains_no_parenthesis():
|
||||
"""Only tasks. A note or a snippet has no status to be wrong about, and a
|
||||
marker that appeared on every line would stop being read."""
|
||||
from scribe.services.plugin_context import _record_kind
|
||||
from grimoire.services.plugin_context import _record_kind
|
||||
|
||||
assert _record_kind(fake_note(note_type="snippet")) == "snippet"
|
||||
assert _record_kind(fake_note(note_type="process")) == "process"
|
||||
@@ -272,9 +272,9 @@ def test_the_listing_tools_say_what_next_step_is_and_how_to_use_it():
|
||||
from tests.helpers import tool_doc
|
||||
|
||||
for module, name in (
|
||||
("scribe.mcp.tools.milestones", "list_milestones"),
|
||||
("scribe.mcp.tools.projects", "enter_project"),
|
||||
("scribe.mcp.tools.projects", "get_project"),
|
||||
("grimoire.mcp.tools.milestones", "list_milestones"),
|
||||
("grimoire.mcp.tools.projects", "enter_project"),
|
||||
("grimoire.mcp.tools.projects", "get_project"),
|
||||
):
|
||||
doc = tool_doc(module, name).lower()
|
||||
assert "next_step" in doc, f"{name} does not mention next_step"
|
||||
|
||||
@@ -33,14 +33,14 @@ from tests.helpers import tool_doc as _doc
|
||||
# The create surfaces and where they live. `start_planning` is here because
|
||||
# it is a create in everything but name — it is how a plan comes into being.
|
||||
_SURFACES = [
|
||||
("scribe.mcp.tools.notes", "create_note"),
|
||||
("scribe.mcp.tools.tasks", "create_task"),
|
||||
("scribe.mcp.tools.tasks", "create_records"),
|
||||
("scribe.mcp.tools.tasks", "start_planning"),
|
||||
("scribe.mcp.tools.snippets", "create_snippet"),
|
||||
("scribe.mcp.tools.processes", "create_process"),
|
||||
("scribe.mcp.tools.rulebooks", "create_rule"),
|
||||
("scribe.mcp.tools.rulebooks", "create_project_rule"),
|
||||
("grimoire.mcp.tools.notes", "create_note"),
|
||||
("grimoire.mcp.tools.tasks", "create_task"),
|
||||
("grimoire.mcp.tools.tasks", "create_records"),
|
||||
("grimoire.mcp.tools.tasks", "start_planning"),
|
||||
("grimoire.mcp.tools.snippets", "create_snippet"),
|
||||
("grimoire.mcp.tools.processes", "create_process"),
|
||||
("grimoire.mcp.tools.rulebooks", "create_rule"),
|
||||
("grimoire.mcp.tools.rulebooks", "create_project_rule"),
|
||||
]
|
||||
|
||||
# What a caller could have wanted instead. A surface naming two of these has
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
import asyncpg
|
||||
import pytest
|
||||
|
||||
from scribe.services.db_ready import asyncpg_dsn, wait_for_database
|
||||
from grimoire.services.db_ready import asyncpg_dsn, wait_for_database
|
||||
|
||||
URL = "postgresql+asyncpg://scribe:p%40ss@db:5432/scribe"
|
||||
|
||||
|
||||
@@ -116,9 +116,9 @@ async def test_a_title_change_reaches_the_mirror_too():
|
||||
def test_every_kind_with_a_derived_mirror_is_registered():
|
||||
"""The load-bearing one. A single `if` naming one kind leaves the next
|
||||
kind uncovered."""
|
||||
from scribe.services.lessons import LESSON_NOTE_TYPE
|
||||
from scribe.services.notes import _mirror_recomposers
|
||||
from scribe.services.snippets import SNIPPET_NOTE_TYPE
|
||||
from grimoire.services.lessons import LESSON_NOTE_TYPE
|
||||
from grimoire.services.notes import _mirror_recomposers
|
||||
from grimoire.services.snippets import SNIPPET_NOTE_TYPE
|
||||
|
||||
table = _mirror_recomposers()
|
||||
assert SNIPPET_NOTE_TYPE in table, "snippets lost their mirror recomposer"
|
||||
@@ -132,7 +132,7 @@ def test_every_kind_with_a_derived_mirror_is_registered():
|
||||
def test_the_dispatch_is_a_lookup_not_a_named_kind():
|
||||
"""Asserted on structure. A lookup extends in one line in one
|
||||
place; naming a kind inline is the shape that left lessons uncovered."""
|
||||
from scribe.services import notes as notes_module
|
||||
from grimoire.services import notes as notes_module
|
||||
|
||||
src = inspect.getsource(notes_module.update_note)
|
||||
assert "_mirror_recomposers()" in src
|
||||
@@ -144,7 +144,7 @@ def test_the_dispatch_is_a_lookup_not_a_named_kind():
|
||||
def test_each_recomposer_takes_the_note_and_nothing_else():
|
||||
"""A registry entry with the wrong signature fails inside a generic PATCH,
|
||||
which is the one moment nobody is watching."""
|
||||
from scribe.services.notes import _mirror_recomposers
|
||||
from grimoire.services.notes import _mirror_recomposers
|
||||
|
||||
for kind, fn in _mirror_recomposers().items():
|
||||
assert callable(fn), f"{kind} maps to something not callable"
|
||||
@@ -158,13 +158,13 @@ NEW_TRIGGER = "a CI run has sat in_progress far longer than its suite takes"
|
||||
|
||||
|
||||
def _lesson_body(trigger, insight="Read the job log.", sources=None):
|
||||
from scribe.services.lessons import compose_body
|
||||
from grimoire.services.lessons import compose_body
|
||||
return compose_body(insight, trigger, sources)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_a_body_write_moves_a_lessons_trigger_with_it():
|
||||
from scribe.services.lessons import TRIGGER_KEY
|
||||
from grimoire.services.lessons import TRIGGER_KEY
|
||||
|
||||
what = "Read the job log before waiting longer"
|
||||
note = fake_lesson(
|
||||
@@ -186,8 +186,8 @@ async def test_a_subject_containing_an_em_dash_still_splits():
|
||||
separator: a subject may legitimately contain one. The title here is an
|
||||
UN-MIGRATED one, still carrying its trigger, because that
|
||||
is the row the inverse still has to read correctly."""
|
||||
from scribe.services.embeddings import trigger_title
|
||||
from scribe.services.lessons import TRIGGER_KEY
|
||||
from grimoire.services.embeddings import trigger_title
|
||||
from grimoire.services.lessons import TRIGGER_KEY
|
||||
|
||||
what = "A wait with no deadline — the shape, not the symptom"
|
||||
trigger = "you are about to await something crossing a process boundary"
|
||||
@@ -205,7 +205,7 @@ async def test_dropping_the_provenance_line_drops_it_from_the_mirror():
|
||||
the failure this recompose exists to prevent, not a courtesy — the
|
||||
opposite call from a snippet's `verification`, which is carried because it
|
||||
was never in the body to delete."""
|
||||
from scribe.services.lessons import SOURCES_KEY
|
||||
from grimoire.services.lessons import SOURCES_KEY
|
||||
|
||||
note = fake_lesson(
|
||||
title="Something learned",
|
||||
@@ -220,7 +220,7 @@ async def test_dropping_the_provenance_line_drops_it_from_the_mirror():
|
||||
async def test_an_explicit_data_wins_for_a_lesson_too():
|
||||
"""`update_lesson` composes the mirror from the merged field set and passes
|
||||
it here; that caller knows things a re-read of the body cannot recover."""
|
||||
from scribe.services.lessons import TRIGGER_KEY
|
||||
from grimoire.services.lessons import TRIGGER_KEY
|
||||
|
||||
note = fake_lesson(data={TRIGGER_KEY: "old"}, project_id=None)
|
||||
authoritative = {TRIGGER_KEY: "from the service", "what": "x"}
|
||||
@@ -232,7 +232,7 @@ async def test_an_explicit_data_wins_for_a_lesson_too():
|
||||
async def test_a_lesson_title_change_reaches_the_mirror():
|
||||
"""A lesson's subject lives in its title, so a title edit is a trigger for
|
||||
recomposition exactly as it is for a snippet's name."""
|
||||
from scribe.services.lessons import TRIGGER_KEY
|
||||
from grimoire.services.lessons import TRIGGER_KEY
|
||||
|
||||
trigger = "two absolute siblings overlap"
|
||||
note = fake_lesson(
|
||||
@@ -261,7 +261,7 @@ async def test_a_lesson_title_change_reaches_the_mirror():
|
||||
def test_untrigger_title_inverts_trigger_title(subject, trigger):
|
||||
"""The join's inverse lives beside it rather than in whichever caller
|
||||
wanted it."""
|
||||
from scribe.services.embeddings import trigger_title, untrigger_title
|
||||
from grimoire.services.embeddings import trigger_title, untrigger_title
|
||||
|
||||
title = trigger_title(subject, trigger)
|
||||
assert untrigger_title(title, trigger) == (subject or trigger).strip()
|
||||
@@ -270,7 +270,7 @@ def test_untrigger_title_inverts_trigger_title(subject, trigger):
|
||||
def test_untrigger_title_degrades_to_the_whole_title():
|
||||
"""A record written before the join existed still answers with something a
|
||||
human recognises rather than with ""."""
|
||||
from scribe.services.embeddings import untrigger_title
|
||||
from grimoire.services.embeddings import untrigger_title
|
||||
|
||||
assert untrigger_title("a plain old title", "") == "a plain old title"
|
||||
assert untrigger_title("a plain old title", "a trigger it lacks") == (
|
||||
@@ -282,7 +282,7 @@ def test_the_trigger_separator_is_spelled_in_exactly_one_place():
|
||||
"""Two literals that must match are one edit away from not matching."""
|
||||
import pathlib
|
||||
|
||||
root = pathlib.Path(__file__).resolve().parents[1] / "src" / "scribe"
|
||||
root = pathlib.Path(__file__).resolve().parents[1] / "src" / "grimoire"
|
||||
offenders = [
|
||||
str(p.relative_to(root)) for p in root.rglob("*.py")
|
||||
if '" \u2014 "' in p.read_text() and p.name != "embeddings.py"
|
||||
@@ -295,7 +295,7 @@ def test_the_trigger_separator_is_spelled_in_exactly_one_place():
|
||||
|
||||
def test_the_mirror_guards_can_fail():
|
||||
"""Shown turning red once."""
|
||||
from scribe.services.lessons import TRIGGER_KEY, recompose_data
|
||||
from grimoire.services.lessons import TRIGGER_KEY, recompose_data
|
||||
|
||||
bare = fake_lesson(title="just a title", body="no composed lines", data=None)
|
||||
assert TRIGGER_KEY not in recompose_data(bare)
|
||||
|
||||
@@ -6,7 +6,7 @@ own import-free module — see services/design_cascade.py.
|
||||
"""
|
||||
from types import SimpleNamespace
|
||||
|
||||
from scribe.services.design_cascade import ancestry, resolve_tokens, would_cycle
|
||||
from grimoire.services.design_cascade import ancestry, resolve_tokens, would_cycle
|
||||
from tests.helpers import design_token_stub
|
||||
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@ import re
|
||||
|
||||
import pytest
|
||||
|
||||
from scribe.services import design_starter_roles as roles
|
||||
from grimoire.services import design_starter_roles as roles
|
||||
|
||||
|
||||
def test_no_role_carries_a_VALUE():
|
||||
|
||||
@@ -7,7 +7,7 @@ declaration.
|
||||
"""
|
||||
from types import SimpleNamespace
|
||||
|
||||
from scribe.services.design_stylesheet import (
|
||||
from grimoire.services.design_stylesheet import (
|
||||
check_code_against_tokens,
|
||||
derivation_report,
|
||||
duplicate_values,
|
||||
|
||||
@@ -25,8 +25,8 @@ import inspect
|
||||
import textwrap
|
||||
from unittest.mock import AsyncMock, patch
|
||||
|
||||
from scribe.services import shape_ledger
|
||||
from scribe.services.shape_ledger import _semantic_canon
|
||||
from grimoire.services import shape_ledger
|
||||
from grimoire.services.shape_ledger import _semantic_canon
|
||||
|
||||
# Comfortably over WRITEPATH_MIN_CODE_CHARS (48 non-whitespace characters), so
|
||||
# these tests exercise the comparison rather than the substance guard. A real
|
||||
@@ -53,7 +53,7 @@ def _hits(*hits: tuple[float, int]) -> AsyncMock:
|
||||
|
||||
|
||||
def _patch(mock: AsyncMock):
|
||||
return patch("scribe.services.embeddings.semantic_search_notes", mock)
|
||||
return patch("grimoire.services.embeddings.semantic_search_notes", mock)
|
||||
|
||||
|
||||
# ── the arm proposes on a hit ────────────────────────────────────────────
|
||||
@@ -95,7 +95,7 @@ async def test_the_arm_proposes_at_the_write_path_floor() -> None:
|
||||
"""It asks the write-path hint's question of the same documents, so it
|
||||
uses that surface's floor — not a stricter private one that yields no
|
||||
proposals for the judge to weigh."""
|
||||
from scribe.services.plugin_context import WRITEPATH_DEFAULT_THRESHOLD
|
||||
from grimoire.services.plugin_context import WRITEPATH_DEFAULT_THRESHOLD
|
||||
|
||||
mock = _hits()
|
||||
with _patch(mock):
|
||||
@@ -140,7 +140,7 @@ def test_the_capped_pass_reads_new_shapes_first() -> None:
|
||||
highest ids — was flagged before the arm could propose for it."""
|
||||
from datetime import datetime, timedelta, timezone
|
||||
|
||||
from scribe.services.shape_ledger import _semantic_priority
|
||||
from grimoire.services.shape_ledger import _semantic_priority
|
||||
|
||||
t = datetime(2026, 9, 22, tzinfo=timezone.utc)
|
||||
rows = [
|
||||
|
||||
@@ -20,9 +20,9 @@ from __future__ import annotations
|
||||
import ast
|
||||
import pathlib
|
||||
|
||||
from scribe.services.embeddings import embedding_text
|
||||
from grimoire.services.embeddings import embedding_text
|
||||
|
||||
SERVICES = pathlib.Path(__file__).resolve().parents[1] / "src" / "scribe"
|
||||
SERVICES = pathlib.Path(__file__).resolve().parents[1] / "src" / "grimoire"
|
||||
|
||||
|
||||
def test_title_and_body_are_joined_by_a_newline():
|
||||
|
||||
@@ -17,7 +17,7 @@ from unittest.mock import AsyncMock, MagicMock, patch
|
||||
from sqlalchemy.dialects import postgresql
|
||||
from sqlalchemy.exc import OperationalError
|
||||
|
||||
from scribe.services import embeddings as emb
|
||||
from grimoire.services import embeddings as emb
|
||||
from tests.helpers import compiled_sql
|
||||
|
||||
ONE_VECTOR = [[0.0] * 384]
|
||||
|
||||
@@ -7,7 +7,7 @@ from unittest.mock import AsyncMock, MagicMock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
from scribe.services.embeddings import (
|
||||
from grimoire.services.embeddings import (
|
||||
_cosine_similarity, get_embedding,
|
||||
)
|
||||
|
||||
@@ -54,7 +54,7 @@ async def test_get_embedding_returns_list_of_floats():
|
||||
fake_embedder = MagicMock()
|
||||
fake_embedder.embed = MagicMock(return_value=iter([fake_vec]))
|
||||
with patch(
|
||||
"scribe.services.embeddings._get_model",
|
||||
"grimoire.services.embeddings._get_model",
|
||||
AsyncMock(return_value=fake_embedder),
|
||||
):
|
||||
out = await get_embedding("hello world")
|
||||
@@ -67,7 +67,7 @@ async def test_get_embedding_propagates_model_load_failures():
|
||||
"""If fastembed can't initialize, the error propagates — callers catch
|
||||
and degrade to keyword search."""
|
||||
with patch(
|
||||
"scribe.services.embeddings._get_model",
|
||||
"grimoire.services.embeddings._get_model",
|
||||
AsyncMock(side_effect=RuntimeError("model load failed")),
|
||||
):
|
||||
with pytest.raises(RuntimeError, match="model load failed"):
|
||||
@@ -91,7 +91,7 @@ def _counting_embeddings():
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_memo_embeds_a_repeated_query_once_per_scope():
|
||||
from scribe.services import embeddings as emb
|
||||
from grimoire.services import embeddings as emb
|
||||
|
||||
fake, calls = _counting_embeddings()
|
||||
with patch.object(emb, "get_embeddings", fake):
|
||||
@@ -108,7 +108,7 @@ async def test_memo_embeds_a_repeated_query_once_per_scope():
|
||||
async def test_without_a_scope_every_call_reaches_the_model():
|
||||
"""The default is None: outside a request scope nothing is remembered, so
|
||||
every caller other than the plugin routes behaves exactly as before."""
|
||||
from scribe.services import embeddings as emb
|
||||
from grimoire.services import embeddings as emb
|
||||
|
||||
fake, calls = _counting_embeddings()
|
||||
with patch.object(emb, "get_embeddings", fake):
|
||||
@@ -119,7 +119,7 @@ async def test_without_a_scope_every_call_reaches_the_model():
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_a_scope_does_not_leak_into_the_next_one():
|
||||
from scribe.services import embeddings as emb
|
||||
from grimoire.services import embeddings as emb
|
||||
|
||||
fake, calls = _counting_embeddings()
|
||||
with patch.object(emb, "get_embeddings", fake):
|
||||
@@ -136,7 +136,7 @@ async def test_a_scope_does_not_leak_into_the_next_one():
|
||||
async def test_a_failed_embedding_is_not_remembered():
|
||||
"""The next arm in the request must try again and degrade on its own,
|
||||
not inherit a failure as if it were a vector."""
|
||||
from scribe.services import embeddings as emb
|
||||
from grimoire.services import embeddings as emb
|
||||
|
||||
attempts = {"n": 0}
|
||||
|
||||
@@ -156,7 +156,7 @@ async def test_a_failed_embedding_is_not_remembered():
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_the_route_decorator_opens_one_scope_per_call():
|
||||
from scribe.services import embeddings as emb
|
||||
from grimoire.services import embeddings as emb
|
||||
|
||||
fake, calls = _counting_embeddings()
|
||||
|
||||
|
||||
@@ -13,11 +13,11 @@ from unittest.mock import AsyncMock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
from scribe.mcp.server import _READ_ONLY_TOOLS, _WRITE_TOOLS
|
||||
from scribe.mcp.tools import family as family_tools
|
||||
from scribe.mcp.tools.milestones import update_milestone
|
||||
from scribe.services import family as family_svc
|
||||
from scribe.services.family import CRITERIA_KEYS, lineage_citations, vetoes
|
||||
from grimoire.mcp.server import _READ_ONLY_TOOLS, _WRITE_TOOLS
|
||||
from grimoire.mcp.tools import family as family_tools
|
||||
from grimoire.mcp.tools.milestones import update_milestone
|
||||
from grimoire.services import family as family_svc
|
||||
from grimoire.services.family import CRITERIA_KEYS, lineage_citations, vetoes
|
||||
from tests.helpers import fake_milestone
|
||||
|
||||
pytestmark = pytest.mark.usefixtures("_bind_user")
|
||||
@@ -143,7 +143,7 @@ async def test_a_failing_trigger_never_breaks_the_write():
|
||||
|
||||
async def test_closing_a_milestone_on_a_platform_carries_the_hint():
|
||||
closed = fake_milestone(id=5, project_id=3, status="done")
|
||||
with patch("scribe.mcp.tools.milestones.milestones_svc.update_milestone",
|
||||
with patch("grimoire.mcp.tools.milestones.milestones_svc.update_milestone",
|
||||
AsyncMock(return_value=closed)), \
|
||||
patch.object(family_svc, "milestone_is_open", AsyncMock(return_value=True)), \
|
||||
patch.object(family_svc, "milestone_trigger", AsyncMock(return_value="evaluate")):
|
||||
@@ -154,7 +154,7 @@ async def test_closing_a_milestone_on_a_platform_carries_the_hint():
|
||||
async def test_re_saving_a_closed_milestone_asks_nothing():
|
||||
closed = fake_milestone(id=5, project_id=3, status="done")
|
||||
trigger = AsyncMock(return_value="evaluate")
|
||||
with patch("scribe.mcp.tools.milestones.milestones_svc.update_milestone",
|
||||
with patch("grimoire.mcp.tools.milestones.milestones_svc.update_milestone",
|
||||
AsyncMock(return_value=closed)), \
|
||||
patch.object(family_svc, "milestone_is_open", AsyncMock(return_value=False)), \
|
||||
patch.object(family_svc, "milestone_trigger", trigger):
|
||||
|
||||
@@ -12,8 +12,8 @@ from __future__ import annotations
|
||||
|
||||
import pytest
|
||||
|
||||
from scribe.mcp.server import _READ_ONLY_TOOLS, _WRITE_TOOLS
|
||||
from scribe.services.family_adoption import (
|
||||
from grimoire.mcp.server import _READ_ONLY_TOOLS, _WRITE_TOOLS
|
||||
from grimoire.services.family_adoption import (
|
||||
CONFLICT_KEYS, CONFLICT_ORDER, OUTCOME_KEYS, _fold_section, _reference_line,
|
||||
assessment_problems, conflict_problems, needs_recheck,
|
||||
)
|
||||
@@ -169,10 +169,10 @@ def test_the_outcomes_the_agent_is_told_are_the_ones_enforced():
|
||||
"""The outcomes and the order are product text. The tool
|
||||
docstrings must name every outcome and every ground the service checks,
|
||||
in the service's order."""
|
||||
assess_doc = tool_doc("scribe.mcp.tools.family", "assess_family_adoption")
|
||||
assess_doc = tool_doc("grimoire.mcp.tools.family", "assess_family_adoption")
|
||||
for n, key in enumerate(OUTCOME_KEYS, start=1):
|
||||
assert f"{n}. {key} —" in assess_doc, f"outcome {n} is not taught as {key}"
|
||||
resolve_doc = tool_doc("scribe.mcp.tools.family", "resolve_family_conflict")
|
||||
resolve_doc = tool_doc("grimoire.mcp.tools.family", "resolve_family_conflict")
|
||||
for n, key in enumerate(CONFLICT_KEYS, start=1):
|
||||
assert f"{n}. {key} —" in resolve_doc, f"ground {n} is not taught as {key}"
|
||||
assert [c["fold_as"] for c in CONFLICT_ORDER] == ["alternative", "trap", "condition", "alternative"]
|
||||
|
||||
@@ -9,8 +9,8 @@ from __future__ import annotations
|
||||
import pathlib
|
||||
import re
|
||||
|
||||
from scribe.services import moment_actions
|
||||
from scribe.services.family_adoption import family_line
|
||||
from grimoire.services import moment_actions
|
||||
from grimoire.services.family_adoption import family_line
|
||||
from tests.helpers import skill_text, tool_doc
|
||||
|
||||
ROOT = pathlib.Path(__file__).resolve().parents[1]
|
||||
@@ -87,13 +87,13 @@ def test_the_plugin_names_the_skill_it_ships():
|
||||
# --- the pointers ---------------------------------------------------------------------
|
||||
|
||||
def test_every_surface_that_meets_family_canon_points_at_the_skill():
|
||||
server = (ROOT / "src" / "scribe" / "mcp" / "server.py").read_text()
|
||||
server = (ROOT / "src" / "grimoire" / "mcp" / "server.py").read_text()
|
||||
index = re.search(r'_INSTRUCTIONS = """(.*?)"""', server, re.S).group(1)
|
||||
assert "family-canon" in index and "`family`" in index
|
||||
for module, tool in (("notes", "create_note"), ("snippets", "create_snippet"),
|
||||
("shapes", "classify_shapes")):
|
||||
assert "family-canon" in tool_doc(f"scribe.mcp.tools.{module}", tool), tool
|
||||
assert "family_hint" in tool_doc("scribe.mcp.tools.notes", "create_note")
|
||||
assert "`family`" in tool_doc("scribe.mcp.tools.projects", "enter_project")
|
||||
assert "family_owed" in tool_doc("scribe.mcp.tools.tasks", "update_task")
|
||||
assert "family-canon" in tool_doc(f"grimoire.mcp.tools.{module}", tool), tool
|
||||
assert "family_hint" in tool_doc("grimoire.mcp.tools.notes", "create_note")
|
||||
assert "`family`" in tool_doc("grimoire.mcp.tools.projects", "enter_project")
|
||||
assert "family_owed" in tool_doc("grimoire.mcp.tools.tasks", "update_task")
|
||||
assert "family_owed" in skill_text("reporting-back")
|
||||
|
||||
@@ -11,7 +11,7 @@ from __future__ import annotations
|
||||
import importlib.util
|
||||
from pathlib import Path
|
||||
|
||||
from scribe.models import family
|
||||
from grimoire.models import family
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
|
||||
@@ -62,7 +62,7 @@ def test_a_canon_ideas_scope_lives_on_the_idea_and_nowhere_else():
|
||||
"""One scope source. A rule topic is linked FROM the idea and carries no
|
||||
platforms of its own, so the two cannot disagree about who a standard
|
||||
reaches — two sources of truth."""
|
||||
from scribe.models.rulebook import RulebookTopic
|
||||
from grimoire.models.rulebook import RulebookTopic
|
||||
|
||||
assert "topic_id" in family.FamilyIdea.__table__.c
|
||||
assert not {c.name for c in RulebookTopic.__table__.c} & {"platform_id", "platform_ids"}
|
||||
|
||||
@@ -13,10 +13,10 @@ from unittest.mock import AsyncMock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
from scribe.services.family_adoption import (
|
||||
from grimoire.services.family_adoption import (
|
||||
_contradiction, path_speaks, shape_outcome, shapes_say,
|
||||
)
|
||||
from scribe.services.shape_ledger import _FAMILY_FLOOR, _semantic_canon, pick_semantic
|
||||
from grimoire.services.shape_ledger import _FAMILY_FLOOR, _semantic_canon, pick_semantic
|
||||
from tests.helpers import tool_doc
|
||||
|
||||
|
||||
@@ -126,7 +126,7 @@ async def test_a_project_on_a_shared_platform_is_searched_with_no_own_canon():
|
||||
family references alone are reason to search."""
|
||||
body = "def throttle(key: str) -> bool:\n return attempts[key] < LIMIT and not locked(key)\n"
|
||||
hit = SimpleNamespace(id=REF)
|
||||
with patch("scribe.services.embeddings.semantic_search_notes",
|
||||
with patch("grimoire.services.embeddings.semantic_search_notes",
|
||||
AsyncMock(return_value=[(0.72, hit)])):
|
||||
assert await _semantic_canon(1, body, set(), {REF}) == (REF, 0.72, "family")
|
||||
|
||||
@@ -134,8 +134,8 @@ async def test_a_project_on_a_shared_platform_is_searched_with_no_own_canon():
|
||||
# --- the agent-facing contract -----------------------------------------------------
|
||||
|
||||
def test_the_tools_teach_idea_id_and_the_family_basis():
|
||||
classify = tool_doc("scribe.mcp.tools.shapes", "classify_shapes")
|
||||
classify = tool_doc("grimoire.mcp.tools.shapes", "classify_shapes")
|
||||
assert "idea_id" in classify and "shapes contradict" in classify
|
||||
assert "family" in tool_doc("scribe.mcp.tools.shapes", "list_shapes")
|
||||
assert '"family"' in tool_doc("scribe.mcp.tools.shapes", "confirm_shape_proposals")
|
||||
assert "classify_shapes" in tool_doc("scribe.mcp.tools.family", "assess_family_adoption")
|
||||
assert "family" in tool_doc("grimoire.mcp.tools.shapes", "list_shapes")
|
||||
assert '"family"' in tool_doc("grimoire.mcp.tools.shapes", "confirm_shape_proposals")
|
||||
assert "classify_shapes" in tool_doc("grimoire.mcp.tools.family", "assess_family_adoption")
|
||||
|
||||
@@ -11,8 +11,8 @@ from pathlib import Path
|
||||
from types import SimpleNamespace
|
||||
from unittest.mock import patch
|
||||
|
||||
from scribe.services import secrets
|
||||
from scribe.services.forge_connections import connection_token
|
||||
from grimoire.services import secrets
|
||||
from grimoire.services.forge_connections import connection_token
|
||||
|
||||
_MIGRATION = Path(__file__).resolve().parents[1] / "alembic" / "versions" / "0122_seal_forge_tokens.py"
|
||||
|
||||
|
||||
+11
-11
@@ -19,8 +19,8 @@ import hmac
|
||||
import pytest
|
||||
import pytest_asyncio
|
||||
|
||||
from scribe.routes.webhooks import delivered_signature, push_facts, signature_ok
|
||||
from scribe.services.snippets import _path_touches
|
||||
from grimoire.routes.webhooks import delivered_signature, push_facts, signature_ok
|
||||
from grimoire.services.snippets import _path_touches
|
||||
from tests.helpers import ensure_user
|
||||
|
||||
SECRET = "wh-secret"
|
||||
@@ -113,8 +113,8 @@ def test_path_touches_uses_the_location_semantics():
|
||||
|
||||
|
||||
def test_route_is_registered_and_unauthenticated_by_design():
|
||||
from scribe.app import create_app
|
||||
from scribe.routes import webhooks as wh
|
||||
from grimoire.app import create_app
|
||||
from grimoire.routes import webhooks as wh
|
||||
|
||||
assert callable(wh.forge_push)
|
||||
rules = {r.rule for r in create_app().url_map.iter_rules()}
|
||||
@@ -127,10 +127,10 @@ def test_route_is_registered_and_unauthenticated_by_design():
|
||||
@pytest_asyncio.fixture
|
||||
async def seeded(_dispose_engine):
|
||||
"""User + project + binding + two verified snippets + one unverified."""
|
||||
from scribe.models import async_session
|
||||
from scribe.models.project import Project
|
||||
from scribe.services import snippets as svc
|
||||
from scribe.services.repo_bindings import set_binding
|
||||
from grimoire.models import async_session
|
||||
from grimoire.models.project import Project
|
||||
from grimoire.services import snippets as svc
|
||||
from grimoire.services.repo_bindings import set_binding
|
||||
|
||||
async with async_session() as s:
|
||||
user = await ensure_user(s, "webhook_itest")
|
||||
@@ -165,7 +165,7 @@ async def seeded(_dispose_engine):
|
||||
|
||||
@pytest.mark.integration
|
||||
async def test_push_flags_matched_verdicts_and_replay_is_quiet(seeded):
|
||||
from scribe.services import snippets as svc
|
||||
from grimoire.services import snippets as svc
|
||||
|
||||
flagged = await svc.invalidate_for_push(
|
||||
"git.example.com/alice/widget",
|
||||
@@ -207,7 +207,7 @@ async def test_push_flags_matched_verdicts_and_replay_is_quiet(seeded):
|
||||
|
||||
@pytest.mark.integration
|
||||
async def test_reverifying_clears_the_flag(seeded):
|
||||
from scribe.services import snippets as svc
|
||||
from grimoire.services import snippets as svc
|
||||
|
||||
await svc.invalidate_for_push(
|
||||
"git.example.com/alice/widget", changed=["src/x.py"], removed=[],
|
||||
@@ -227,7 +227,7 @@ async def test_reverifying_clears_the_flag(seeded):
|
||||
|
||||
@pytest.mark.integration
|
||||
async def test_unbound_repo_flags_nothing(seeded):
|
||||
from scribe.services import snippets as svc
|
||||
from grimoire.services import snippets as svc
|
||||
|
||||
flagged = await svc.invalidate_for_push(
|
||||
"github.com/somebody/else", changed=["src/x.py"], removed=[],
|
||||
|
||||
@@ -3,7 +3,7 @@ from unittest.mock import AsyncMock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
from scribe.services.dedup import (
|
||||
from grimoire.services.dedup import (
|
||||
_GATE_MIN_BLOCK,
|
||||
_GATE_MIN_OVERLAP,
|
||||
GATE_DEFAULT_THRESHOLDS,
|
||||
@@ -16,7 +16,7 @@ _real_gate_setting = _gate_setting
|
||||
|
||||
|
||||
def _setting(value):
|
||||
return patch("scribe.services.settings.get_setting", AsyncMock(return_value=value))
|
||||
return patch("grimoire.services.settings.get_setting", AsyncMock(return_value=value))
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@@ -46,7 +46,7 @@ async def test_an_unparseable_value_falls_back_to_the_default():
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_an_unreadable_setting_falls_back_to_the_default():
|
||||
with patch("scribe.services.settings.get_setting",
|
||||
with patch("grimoire.services.settings.get_setting",
|
||||
AsyncMock(side_effect=RuntimeError("db down"))):
|
||||
got = await _real_gate_setting(7, "lesson", _GATE_MIN_BLOCK)
|
||||
assert got == GATE_DEFAULT_THRESHOLDS["lesson"]
|
||||
@@ -54,18 +54,18 @@ async def test_an_unreadable_setting_falls_back_to_the_default():
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_the_overlap_floor_never_sits_above_the_block_bar():
|
||||
from scribe.services.dedup import gate_bars
|
||||
from grimoire.services.dedup import gate_bars
|
||||
|
||||
async def _set(user_id, key, lo):
|
||||
return {"note_copy": 0.85, "note_overlap": 0.90}[key]
|
||||
|
||||
with patch("scribe.services.dedup._gate_setting", AsyncMock(side_effect=_set)):
|
||||
with patch("grimoire.services.dedup._gate_setting", AsyncMock(side_effect=_set)):
|
||||
assert await gate_bars(7, "note") == (0.85, 0.85)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_a_kind_outside_the_copy_band_reads_no_overlap_floor():
|
||||
from scribe.services.dedup import gate_bars
|
||||
from grimoire.services.dedup import gate_bars
|
||||
|
||||
seen = []
|
||||
|
||||
@@ -73,6 +73,6 @@ async def test_a_kind_outside_the_copy_band_reads_no_overlap_floor():
|
||||
seen.append(key)
|
||||
return GATE_DEFAULT_THRESHOLDS[key]
|
||||
|
||||
with patch("scribe.services.dedup._gate_setting", AsyncMock(side_effect=_set)):
|
||||
with patch("grimoire.services.dedup._gate_setting", AsyncMock(side_effect=_set)):
|
||||
assert await gate_bars(7, "process") == (0.90, 0.90)
|
||||
assert seen == ["general"]
|
||||
|
||||
@@ -64,7 +64,7 @@ def _live_session_context_source() -> str:
|
||||
Read as text rather than called: the function needs a database, and what
|
||||
this module checks is what the product SAYS, which is the literal strings.
|
||||
"""
|
||||
src = (ROOT / "src/scribe/services/plugin_context.py").read_text()
|
||||
src = (ROOT / "src/grimoire/services/plugin_context.py").read_text()
|
||||
start = src.index("async def build_session_context")
|
||||
nxt = re.search(r"\n(?:async def|def) ", src[start + 1:])
|
||||
return src[start:start + 1 + nxt.start()] if nxt else src[start:]
|
||||
@@ -73,7 +73,7 @@ def _live_session_context_source() -> str:
|
||||
def _delivered_shapes() -> str:
|
||||
"""Every reply shape as a session receives it in full. Rendered rather than
|
||||
read as source: the header a shape arrives under is guidance too."""
|
||||
from scribe.services import reply_shapes
|
||||
from grimoire.services import reply_shapes
|
||||
|
||||
return "\n\n".join(reply_shapes.render(s, full=True) for s in reply_shapes.SHAPES.values())
|
||||
|
||||
@@ -91,12 +91,12 @@ def delivered_surfaces() -> dict[str, str]:
|
||||
- `shapes` — the reply shapes the server delivers at their moments,
|
||||
in the full form a session first sees
|
||||
"""
|
||||
server = (ROOT / "src/scribe/mcp/server.py").read_text()
|
||||
server = (ROOT / "src/grimoire/mcp/server.py").read_text()
|
||||
match = re.search(r'_INSTRUCTIONS = """(.*?)"""', server, re.S)
|
||||
assert match, "server.py no longer defines _INSTRUCTIONS as a triple-quoted literal"
|
||||
surfaces = {
|
||||
"instructions": match.group(1),
|
||||
"docstrings": "".join(p.read_text() for p in sorted((ROOT / "src/scribe/mcp/tools").glob("*.py"))),
|
||||
"docstrings": "".join(p.read_text() for p in sorted((ROOT / "src/grimoire/mcp/tools").glob("*.py"))),
|
||||
"static": (ROOT / "plugin/hooks/scribe_static_context.md").read_text(),
|
||||
"commands": "".join(p.read_text() for p in sorted((ROOT / "plugin/commands").glob("*.md"))),
|
||||
"live": _live_session_context_source(),
|
||||
|
||||
@@ -6,8 +6,8 @@ validation is pinned here; the effects are the integration tests.
|
||||
Rules are not an inception choice: a rule in a rulebook is global, so there is
|
||||
nothing to choose.
|
||||
"""
|
||||
from scribe.models.project import Project
|
||||
from scribe.services.inception import (
|
||||
from grimoire.models.project import Project
|
||||
from grimoire.services.inception import (
|
||||
CHOICE_KEYS, INCEPTION_VIAS, is_decided, normalize_choices, validate_inception,
|
||||
)
|
||||
|
||||
@@ -76,7 +76,7 @@ def test_standard_systems_vocabulary_reads_the_catalog_not_a_constant():
|
||||
read the table, or the list they show and the list they mint diverge."""
|
||||
import inspect
|
||||
|
||||
from scribe.services import systems as systems_svc
|
||||
from grimoire.services import systems as systems_svc
|
||||
|
||||
assert not hasattr(systems_svc, "STANDARD_SYSTEMS"), (
|
||||
"the constant is gone — the catalog table is the single source"
|
||||
|
||||
@@ -35,7 +35,7 @@ def _all_surfaces() -> list[tuple[str, str]]:
|
||||
for p in (ROOT / "plugin" / "skills").rglob("*.md")]
|
||||
found += [(str(p.relative_to(ROOT)), p.read_text())
|
||||
for p in (ROOT / "plugin" / "hooks").glob("*.md")]
|
||||
server = ROOT / "src" / "scribe" / "mcp" / "server.py"
|
||||
server = ROOT / "src" / "grimoire" / "mcp" / "server.py"
|
||||
found.append((str(server.relative_to(ROOT)), server.read_text()))
|
||||
return found
|
||||
|
||||
@@ -43,7 +43,7 @@ def _all_surfaces() -> list[tuple[str, str]]:
|
||||
def _instructions_text() -> str:
|
||||
"""The _INSTRUCTIONS literal from server.py, as the client would see it."""
|
||||
import re
|
||||
src = (ROOT / "src" / "scribe" / "mcp" / "server.py").read_text()
|
||||
src = (ROOT / "src" / "grimoire" / "mcp" / "server.py").read_text()
|
||||
match = re.search(r'_INSTRUCTIONS = """(.*?)"""', src, re.S)
|
||||
assert match, "server.py no longer defines _INSTRUCTIONS as a triple-quoted literal"
|
||||
return match.group(1).strip()
|
||||
|
||||
@@ -23,12 +23,12 @@ import pytest_asyncio
|
||||
from datetime import datetime, timezone
|
||||
from sqlalchemy import select
|
||||
|
||||
from scribe.models import async_session
|
||||
from scribe.models.code_shape import CodeShape
|
||||
from scribe.models.note import Note
|
||||
from scribe.models.project import Project
|
||||
from scribe.models.user import User
|
||||
from scribe.services import backup
|
||||
from grimoire.models import async_session
|
||||
from grimoire.models.code_shape import CodeShape
|
||||
from grimoire.models.note import Note
|
||||
from grimoire.models.project import Project
|
||||
from grimoire.models.user import User
|
||||
from grimoire.services import backup
|
||||
from tests.helpers import ensure_user
|
||||
|
||||
pytestmark = [pytest.mark.integration, pytest.mark.usefixtures("_dispose_engine")]
|
||||
|
||||
@@ -15,11 +15,11 @@ import pytest
|
||||
import pytest_asyncio
|
||||
from sqlalchemy import select
|
||||
|
||||
from scribe.models import async_session
|
||||
from scribe.models.note import Note
|
||||
from scribe.models.user import User
|
||||
from scribe.services import backup
|
||||
from scribe.services.backup_file import write_archive_to
|
||||
from grimoire.models import async_session
|
||||
from grimoire.models.note import Note
|
||||
from grimoire.models.user import User
|
||||
from grimoire.services import backup
|
||||
from grimoire.services.backup_file import write_archive_to
|
||||
from tests.helpers import ensure_user
|
||||
|
||||
pytestmark = [pytest.mark.integration, pytest.mark.usefixtures("_dispose_engine")]
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
"""Real-Postgres proof that the scheduled backup's FILE restores: a payload
|
||||
written as the tar, read back through `scribe.restore`'s loader, and fed to
|
||||
written as the tar, read back through `grimoire.restore`'s loader, and fed to
|
||||
the real `restore_full_backup`.
|
||||
|
||||
The unit lane already shows the tar hands back the dict it was given. What
|
||||
@@ -11,14 +11,14 @@ import pytest
|
||||
import pytest_asyncio
|
||||
from sqlalchemy import select
|
||||
|
||||
from scribe.models import async_session
|
||||
from scribe.models.milestone import Milestone
|
||||
from scribe.models.note import Note
|
||||
from scribe.models.project import Project
|
||||
from scribe.models.user import User
|
||||
from scribe.restore import load_backup, run
|
||||
from scribe.services import backup
|
||||
from scribe.services.backup_file import write_archive
|
||||
from grimoire.models import async_session
|
||||
from grimoire.models.milestone import Milestone
|
||||
from grimoire.models.note import Note
|
||||
from grimoire.models.project import Project
|
||||
from grimoire.models.user import User
|
||||
from grimoire.restore import load_backup, run
|
||||
from grimoire.services import backup
|
||||
from grimoire.services.backup_file import write_archive
|
||||
from tests.helpers import ensure_user
|
||||
|
||||
pytestmark = [pytest.mark.integration, pytest.mark.usefixtures("_dispose_engine")]
|
||||
|
||||
@@ -15,10 +15,10 @@ import pytest
|
||||
import pytest_asyncio
|
||||
from sqlalchemy import select
|
||||
|
||||
from scribe.models import async_session
|
||||
from scribe.models.note import Note
|
||||
from scribe.models.user import User
|
||||
from scribe.services import backup
|
||||
from grimoire.models import async_session
|
||||
from grimoire.models.note import Note
|
||||
from grimoire.models.user import User
|
||||
from grimoire.services import backup
|
||||
from tests.helpers import ensure_user
|
||||
|
||||
pytestmark = [pytest.mark.integration, pytest.mark.usefixtures("_dispose_engine")]
|
||||
|
||||
@@ -23,14 +23,14 @@ import pytest
|
||||
import pytest_asyncio
|
||||
from sqlalchemy import select
|
||||
|
||||
from scribe.models import async_session
|
||||
from scribe.models.note import Note
|
||||
from scribe.models.rule_usage import (
|
||||
from grimoire.models import async_session
|
||||
from grimoire.models.note import Note
|
||||
from grimoire.models.rule_usage import (
|
||||
APPLIED, DEPARTED, PULLED, SURFACED, RuleUsageEvent,
|
||||
)
|
||||
from scribe.models.rulebook import Rule, Rulebook, RulebookTopic
|
||||
from scribe.models.user import User
|
||||
from scribe.services import backup
|
||||
from grimoire.models.rulebook import Rule, Rulebook, RulebookTopic
|
||||
from grimoire.models.user import User
|
||||
from grimoire.services import backup
|
||||
from tests.helpers import ensure_user
|
||||
|
||||
pytestmark = [pytest.mark.integration, pytest.mark.usefixtures("_dispose_engine")]
|
||||
|
||||
@@ -23,11 +23,11 @@ import pytest
|
||||
import pytest_asyncio
|
||||
from sqlalchemy import select
|
||||
|
||||
from scribe.models import async_session
|
||||
from scribe.models.rule_version import RuleVersion
|
||||
from scribe.models.rulebook import Rule, Rulebook, RulebookTopic
|
||||
from scribe.models.user import User
|
||||
from scribe.services import backup
|
||||
from grimoire.models import async_session
|
||||
from grimoire.models.rule_version import RuleVersion
|
||||
from grimoire.models.rulebook import Rule, Rulebook, RulebookTopic
|
||||
from grimoire.models.user import User
|
||||
from grimoire.services import backup
|
||||
from tests.helpers import ensure_user
|
||||
|
||||
pytestmark = [pytest.mark.integration, pytest.mark.usefixtures("_dispose_engine")]
|
||||
|
||||
@@ -8,7 +8,7 @@ AttributeError, reporting 0/6) passes the unit suite but fails here.
|
||||
"""
|
||||
import pytest
|
||||
|
||||
from scribe.services.db_maintenance import (
|
||||
from grimoire.services.db_maintenance import (
|
||||
MAINTENANCE_TABLES,
|
||||
get_table_health,
|
||||
run_maintenance,
|
||||
|
||||
@@ -20,12 +20,12 @@ import pytest
|
||||
import pytest_asyncio
|
||||
from sqlalchemy import delete, select
|
||||
|
||||
from scribe.models import async_session
|
||||
from scribe.models.embedding import NoteEmbedding, RuleEmbedding
|
||||
from scribe.models.note import Note
|
||||
from scribe.models.rulebook import Rulebook
|
||||
from scribe.services import embeddings as emb
|
||||
from scribe.services import rulebooks as rulebooks_svc
|
||||
from grimoire.models import async_session
|
||||
from grimoire.models.embedding import NoteEmbedding, RuleEmbedding
|
||||
from grimoire.models.note import Note
|
||||
from grimoire.models.rulebook import Rulebook
|
||||
from grimoire.services import embeddings as emb
|
||||
from grimoire.services import rulebooks as rulebooks_svc
|
||||
from tests.helpers import ensure_user
|
||||
|
||||
pytestmark = [pytest.mark.integration, pytest.mark.usefixtures("_dispose_engine")]
|
||||
|
||||
@@ -19,16 +19,16 @@ import pytest
|
||||
import pytest_asyncio
|
||||
from sqlalchemy import select
|
||||
|
||||
from scribe.models import async_session
|
||||
from scribe.models.canonical_system import CanonicalSystem
|
||||
from scribe.models.family import FamilyAdoption, FamilyDecision, FamilyIdea, Platform, ProjectPlatform
|
||||
from scribe.models.note import Note
|
||||
from scribe.models.project import Project
|
||||
from scribe.models.system import RecordSystem, System
|
||||
from scribe.models.task_log import TaskLog
|
||||
from scribe.models.user import User
|
||||
from scribe.services import family as family_svc
|
||||
from scribe.services import family_adoption as adoption_svc
|
||||
from grimoire.models import async_session
|
||||
from grimoire.models.canonical_system import CanonicalSystem
|
||||
from grimoire.models.family import FamilyAdoption, FamilyDecision, FamilyIdea, Platform, ProjectPlatform
|
||||
from grimoire.models.note import Note
|
||||
from grimoire.models.project import Project
|
||||
from grimoire.models.system import RecordSystem, System
|
||||
from grimoire.models.task_log import TaskLog
|
||||
from grimoire.models.user import User
|
||||
from grimoire.services import family as family_svc
|
||||
from grimoire.services import family_adoption as adoption_svc
|
||||
from tests.helpers import ensure_user
|
||||
|
||||
pytestmark = [pytest.mark.integration, pytest.mark.usefixtures("_dispose_engine")]
|
||||
@@ -63,7 +63,7 @@ async def _clean():
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def _no_meaning():
|
||||
with patch("scribe.services.embeddings.semantic_search_notes", AsyncMock(return_value=[])):
|
||||
with patch("grimoire.services.embeddings.semantic_search_notes", AsyncMock(return_value=[])):
|
||||
yield
|
||||
|
||||
|
||||
@@ -409,7 +409,7 @@ B_SHAPES = [
|
||||
|
||||
|
||||
async def _reference(f, *, language: str = "kotlin", name: str = "installUpdate") -> int:
|
||||
from scribe.services import snippets as snippets_svc
|
||||
from grimoire.services import snippets as snippets_svc
|
||||
|
||||
snippet = await snippets_svc.create_snippet(
|
||||
f["owner"], name=f"{name} ({language})", code=KOTLIN_BODY, language=language,
|
||||
@@ -422,7 +422,7 @@ async def _reference(f, *, language: str = "kotlin", name: str = "installUpdate"
|
||||
async def shapes(family):
|
||||
"""Android two's ledger has two live shapes, and the idea has a Kotlin
|
||||
reference in android one."""
|
||||
from scribe.services.shape_ledger import sync_repo_shapes
|
||||
from grimoire.services.shape_ledger import sync_repo_shapes
|
||||
|
||||
await sync_repo_shapes(family["b"], REPO, B_SHAPES, seen_marker="main")
|
||||
family["kotlin"] = await _reference(family)
|
||||
@@ -431,7 +431,7 @@ async def shapes(family):
|
||||
|
||||
|
||||
async def _classify(f, project: str, symbol: str, status: str, **item):
|
||||
from scribe.services import shape_ledger
|
||||
from grimoire.services import shape_ledger
|
||||
|
||||
path = next(p for p, _, s in B_SHAPES if s == symbol)
|
||||
return await shape_ledger.classify_shapes(
|
||||
@@ -461,7 +461,7 @@ async def test_a_shape_classified_against_the_idea_answers_it_adopted(shapes):
|
||||
|
||||
|
||||
async def test_idea_id_prefers_the_reference_in_the_shapes_language(shapes):
|
||||
from scribe.services.shape_ledger import live_rows
|
||||
from grimoire.services.shape_ledger import live_rows
|
||||
|
||||
python_ref = await _reference(shapes, language="python", name="install_update")
|
||||
await adoption_svc.set_references(shapes["owner"], shapes["note"],
|
||||
@@ -473,7 +473,7 @@ async def test_idea_id_prefers_the_reference_in_the_shapes_language(shapes):
|
||||
|
||||
|
||||
async def test_an_idea_with_no_reference_is_refused_and_nothing_applies(family):
|
||||
from scribe.services.shape_ledger import live_rows, sync_repo_shapes
|
||||
from grimoire.services.shape_ledger import live_rows, sync_repo_shapes
|
||||
|
||||
await sync_repo_shapes(family["b"], REPO, B_SHAPES, seen_marker="main")
|
||||
with pytest.raises(ValueError, match="no reference implementation"):
|
||||
@@ -532,7 +532,7 @@ async def test_nothing_is_answered_or_proposed_across_projects_sharing_no_platfo
|
||||
"""The Go service shares no platform with the idea: its shape judged
|
||||
against the reference is plain canon use, and the proposer never offers
|
||||
the reference to it."""
|
||||
from scribe.services import shape_ledger
|
||||
from grimoire.services import shape_ledger
|
||||
|
||||
await shape_ledger.sync_repo_shapes(shapes["c"], "git.example/go", [
|
||||
("cmd/update.go", "sym", "PushUpdate")], seen_marker="main")
|
||||
@@ -548,7 +548,7 @@ async def test_nothing_is_answered_or_proposed_across_projects_sharing_no_platfo
|
||||
|
||||
|
||||
async def test_the_proposer_offers_the_family_reference_as_the_top_hit(shapes):
|
||||
from scribe.services import shape_ledger
|
||||
from grimoire.services import shape_ledger
|
||||
|
||||
body = ("def push_update(apk_path: str) -> None:\n"
|
||||
" session = installer.open_session(installer.create_session())\n"
|
||||
@@ -557,7 +557,7 @@ async def test_the_proposer_offers_the_family_reference_as_the_top_hit(shapes):
|
||||
defs = [("tools/release.py", "sym", "push_update", "def push_update(apk_path: str) -> None:",
|
||||
"sha-push", body)]
|
||||
ref = type("Hit", (), {"id": shapes["kotlin"]})()
|
||||
with patch("scribe.services.embeddings.semantic_search_notes",
|
||||
with patch("grimoire.services.embeddings.semantic_search_notes",
|
||||
AsyncMock(return_value=[(0.73, ref)])):
|
||||
await shape_ledger.propose_for_repo(shapes["owner"], shapes["b"], REPO, defs)
|
||||
row = next(r for r in await shape_ledger.live_rows(shapes["b"]) if r.symbol == "push_update")
|
||||
@@ -568,7 +568,7 @@ async def test_the_proposer_offers_the_family_reference_as_the_top_hit(shapes):
|
||||
|
||||
|
||||
async def test_a_conflict_brings_the_losing_sides_variant_shapes_back_to_the_todo(shapes):
|
||||
from scribe.services.shape_ledger import live_rows
|
||||
from grimoire.services.shape_ledger import live_rows
|
||||
|
||||
await _classify(shapes, "b", "selfUpdate", "variant", idea_id=shapes["note"],
|
||||
reason="signs in CI with a throwaway key")
|
||||
|
||||
@@ -21,15 +21,15 @@ import pytest_asyncio
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.exc import IntegrityError
|
||||
|
||||
from scribe.models import async_session
|
||||
from scribe.models.family import (
|
||||
from grimoire.models import async_session
|
||||
from grimoire.models.family import (
|
||||
FamilyAdoption, FamilyDecision, FamilyIdea, FamilyIdeaPlatform,
|
||||
FamilyIdeaReference, Platform, ProjectPlatform,
|
||||
)
|
||||
from scribe.models.note import Note
|
||||
from scribe.models.project import Project
|
||||
from scribe.models.user import User
|
||||
from scribe.services import backup
|
||||
from grimoire.models.note import Note
|
||||
from grimoire.models.project import Project
|
||||
from grimoire.models.user import User
|
||||
from grimoire.services import backup
|
||||
from tests.helpers import ensure_user
|
||||
|
||||
pytestmark = [pytest.mark.integration, pytest.mark.usefixtures("_dispose_engine")]
|
||||
|
||||
@@ -18,16 +18,16 @@ import pytest
|
||||
import pytest_asyncio
|
||||
from sqlalchemy import select
|
||||
|
||||
from scribe.models import async_session
|
||||
from scribe.models.family import (
|
||||
from grimoire.models import async_session
|
||||
from grimoire.models.family import (
|
||||
FamilyAdoption, FamilyDecision, FamilyIdea, Platform, ProjectPlatform,
|
||||
)
|
||||
from scribe.models.milestone import Milestone
|
||||
from scribe.models.note import Note
|
||||
from scribe.models.project import Project
|
||||
from scribe.models.rulebook import Rule, Rulebook, RulebookTopic
|
||||
from scribe.models.user import User
|
||||
from scribe.services import family as family_svc
|
||||
from grimoire.models.milestone import Milestone
|
||||
from grimoire.models.note import Note
|
||||
from grimoire.models.project import Project
|
||||
from grimoire.models.rulebook import Rule, Rulebook, RulebookTopic
|
||||
from grimoire.models.user import User
|
||||
from grimoire.services import family as family_svc
|
||||
from tests.helpers import ensure_user
|
||||
|
||||
pytestmark = [pytest.mark.integration, pytest.mark.usefixtures("_dispose_engine")]
|
||||
@@ -62,7 +62,7 @@ async def _clean():
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def _no_meaning():
|
||||
with patch("scribe.services.embeddings.semantic_search_notes", AsyncMock(return_value=[])):
|
||||
with patch("grimoire.services.embeddings.semantic_search_notes", AsyncMock(return_value=[])):
|
||||
yield
|
||||
|
||||
|
||||
@@ -261,7 +261,7 @@ async def test_a_repeat_on_a_shared_platform_opens_an_evaluation(family):
|
||||
repeat = await _note_in(family["b"], family["owner"], "One keystore, two channels.")
|
||||
async with async_session() as s:
|
||||
original = await s.get(Note, family["note"])
|
||||
with patch("scribe.services.embeddings.semantic_search_notes",
|
||||
with patch("grimoire.services.embeddings.semantic_search_notes",
|
||||
AsyncMock(return_value=[(0.86, original)])):
|
||||
hint = await family_svc.repeat_trigger(family["owner"], repeat)
|
||||
assert hint and f"#{family['note']}" in hint
|
||||
@@ -272,7 +272,7 @@ async def test_a_repeat_with_no_shared_platform_is_silent(family):
|
||||
unrelated = await _note_in(family["c"], family["owner"], "One keystore, two channels.")
|
||||
async with async_session() as s:
|
||||
original = await s.get(Note, family["note"])
|
||||
with patch("scribe.services.embeddings.semantic_search_notes",
|
||||
with patch("grimoire.services.embeddings.semantic_search_notes",
|
||||
AsyncMock(return_value=[(0.86, original)])):
|
||||
assert await family_svc.repeat_trigger(family["owner"], unrelated) is None
|
||||
assert await _idea(family["note"]) is None
|
||||
@@ -292,7 +292,7 @@ async def test_a_repeat_across_kinds_is_silent(family):
|
||||
await s.refresh(snippet)
|
||||
original = await s.get(Note, family["note"])
|
||||
assert original.note_type == "note"
|
||||
with patch("scribe.services.embeddings.semantic_search_notes",
|
||||
with patch("grimoire.services.embeddings.semantic_search_notes",
|
||||
AsyncMock(return_value=[(0.86, original)])):
|
||||
assert await family_svc.repeat_trigger(family["owner"], snippet) is None
|
||||
assert await _idea(family["note"]) is None
|
||||
|
||||
@@ -18,16 +18,16 @@ import pytest
|
||||
import pytest_asyncio
|
||||
from sqlalchemy import select
|
||||
|
||||
from scribe.models import async_session
|
||||
from scribe.models.embedding import EMBEDDING_DIM, NoteEmbedding
|
||||
from scribe.models.family import (
|
||||
from grimoire.models import async_session
|
||||
from grimoire.models.embedding import EMBEDDING_DIM, NoteEmbedding
|
||||
from grimoire.models.family import (
|
||||
FamilyAdoption, FamilyDecision, FamilyIdea, FamilyIdeaPlatform, FamilyIdeaReference,
|
||||
Platform, ProjectPlatform,
|
||||
)
|
||||
from scribe.models.note import Note
|
||||
from scribe.models.project import Project
|
||||
from scribe.services import family_adoption as adoption_svc
|
||||
from scribe.services.embeddings import CHUNKER_VERSION, EMBEDDING_MODEL, semantic_search_notes
|
||||
from grimoire.models.note import Note
|
||||
from grimoire.models.project import Project
|
||||
from grimoire.services import family_adoption as adoption_svc
|
||||
from grimoire.services.embeddings import CHUNKER_VERSION, EMBEDDING_MODEL, semantic_search_notes
|
||||
from tests.helpers import ensure_user
|
||||
|
||||
pytestmark = [pytest.mark.integration, pytest.mark.usefixtures("_dispose_engine")]
|
||||
@@ -102,7 +102,7 @@ def _family(corpus) -> set[int]:
|
||||
|
||||
|
||||
async def _search(uid, **kw):
|
||||
with patch("scribe.services.embeddings.get_embedding", AsyncMock(return_value=QUERY_VEC)):
|
||||
with patch("grimoire.services.embeddings.get_embedding", AsyncMock(return_value=QUERY_VEC)):
|
||||
hits = await semantic_search_notes(uid, "how does the app update itself", limit=20, **kw)
|
||||
return {note.id for _score, note in hits}
|
||||
|
||||
|
||||
@@ -14,11 +14,11 @@ from unittest.mock import patch
|
||||
import pytest
|
||||
import pytest_asyncio
|
||||
|
||||
from scribe.config import Config
|
||||
from scribe.models import async_session
|
||||
from scribe.models.project import Project
|
||||
from scribe.services.forge import get_forges
|
||||
from scribe.services.forge_connections import (
|
||||
from grimoire.config import Config
|
||||
from grimoire.models import async_session
|
||||
from grimoire.models.project import Project
|
||||
from grimoire.services.forge import get_forges
|
||||
from grimoire.services.forge_connections import (
|
||||
create_connection,
|
||||
delete_connection,
|
||||
list_connections,
|
||||
@@ -178,7 +178,7 @@ async def test_the_token_is_stored_sealed_and_a_key_change_drops_the_row(seeded)
|
||||
rather than sending ciphertext to the forge as a token."""
|
||||
from sqlalchemy import text
|
||||
|
||||
from scribe.services import secrets
|
||||
from grimoire.services import secrets
|
||||
|
||||
owner = seeded["owner"]
|
||||
row = await create_connection(owner, kind="gitea", base_url=GITEA, token="plain-tok")
|
||||
|
||||
@@ -9,13 +9,13 @@ from datetime import datetime, timedelta, timezone
|
||||
import pytest
|
||||
import pytest_asyncio
|
||||
|
||||
from scribe.models import async_session
|
||||
from scribe.models.milestone import Milestone
|
||||
from scribe.models.note import Note
|
||||
from scribe.models.project import Project
|
||||
from scribe.models.task_log import TaskLog
|
||||
from scribe.services import milestones as milestones_svc
|
||||
from scribe.services import notes as notes_svc
|
||||
from grimoire.models import async_session
|
||||
from grimoire.models.milestone import Milestone
|
||||
from grimoire.models.note import Note
|
||||
from grimoire.models.project import Project
|
||||
from grimoire.models.task_log import TaskLog
|
||||
from grimoire.services import milestones as milestones_svc
|
||||
from grimoire.services import notes as notes_svc
|
||||
from tests.helpers import ensure_user
|
||||
|
||||
pytestmark = [pytest.mark.integration, pytest.mark.usefixtures("_dispose_engine", "_no_embedding")]
|
||||
|
||||
@@ -10,11 +10,11 @@ global and applies to every project, so there is nothing to subscribe to.
|
||||
import pytest
|
||||
import pytest_asyncio
|
||||
|
||||
from scribe.models import async_session
|
||||
from scribe.models.project import Project
|
||||
from scribe.services import inception as inception_svc
|
||||
from scribe.services import canonical_systems as canonical_svc
|
||||
from scribe.services import systems as systems_svc
|
||||
from grimoire.models import async_session
|
||||
from grimoire.models.project import Project
|
||||
from grimoire.services import inception as inception_svc
|
||||
from grimoire.services import canonical_systems as canonical_svc
|
||||
from grimoire.services import systems as systems_svc
|
||||
from tests.helpers import ensure_user
|
||||
|
||||
pytestmark = [pytest.mark.integration, pytest.mark.usefixtures("_dispose_engine")]
|
||||
|
||||
@@ -27,10 +27,10 @@ import pytest
|
||||
import pytest_asyncio
|
||||
from sqlalchemy import select
|
||||
|
||||
from scribe.models import async_session
|
||||
from scribe.models.note import Note
|
||||
from scribe.services import lessons as lessons_svc
|
||||
from scribe.services import notes as notes_svc
|
||||
from grimoire.models import async_session
|
||||
from grimoire.models.note import Note
|
||||
from grimoire.services import lessons as lessons_svc
|
||||
from grimoire.services import notes as notes_svc
|
||||
from tests.helpers import ensure_user
|
||||
|
||||
pytestmark = [pytest.mark.integration, pytest.mark.usefixtures("_dispose_engine")]
|
||||
|
||||
@@ -18,12 +18,12 @@ from unittest.mock import AsyncMock, patch
|
||||
import pytest
|
||||
import pytest_asyncio
|
||||
|
||||
from scribe.models import async_session
|
||||
from scribe.models.embedding import EMBEDDING_DIM, NoteEmbedding
|
||||
from scribe.models.note import Note
|
||||
from scribe.models.project import Project
|
||||
from scribe.services import lessons as lessons_svc
|
||||
from scribe.services.embeddings import CHUNKER_VERSION, EMBEDDING_MODEL, semantic_search_notes
|
||||
from grimoire.models import async_session
|
||||
from grimoire.models.embedding import EMBEDDING_DIM, NoteEmbedding
|
||||
from grimoire.models.note import Note
|
||||
from grimoire.models.project import Project
|
||||
from grimoire.services import lessons as lessons_svc
|
||||
from grimoire.services.embeddings import CHUNKER_VERSION, EMBEDDING_MODEL, semantic_search_notes
|
||||
from tests.helpers import ensure_user
|
||||
|
||||
pytestmark = [pytest.mark.integration, pytest.mark.usefixtures("_dispose_engine")]
|
||||
@@ -81,7 +81,7 @@ async def corpus():
|
||||
|
||||
async def _search(uid, **kw):
|
||||
with patch(
|
||||
"scribe.services.embeddings.get_embedding", AsyncMock(return_value=QUERY_VEC)
|
||||
"grimoire.services.embeddings.get_embedding", AsyncMock(return_value=QUERY_VEC)
|
||||
):
|
||||
hits = await semantic_search_notes(uid, "when does this apply", limit=20, **kw)
|
||||
return {note.id for _score, note in hits}
|
||||
|
||||
@@ -13,14 +13,14 @@ import pytest_asyncio
|
||||
from sqlalchemy import delete, select
|
||||
from sqlalchemy.exc import IntegrityError
|
||||
|
||||
from scribe.models import async_session
|
||||
from scribe.models.lesson_rule_link import LessonRuleLink
|
||||
from scribe.models.note import Note
|
||||
from scribe.models.project import Project
|
||||
from scribe.models.rulebook import Rule
|
||||
from scribe.services import lesson_rules as links_svc
|
||||
from scribe.services import lessons as lessons_svc
|
||||
from scribe.services import rulebooks as rulebooks_svc
|
||||
from grimoire.models import async_session
|
||||
from grimoire.models.lesson_rule_link import LessonRuleLink
|
||||
from grimoire.models.note import Note
|
||||
from grimoire.models.project import Project
|
||||
from grimoire.models.rulebook import Rule
|
||||
from grimoire.services import lesson_rules as links_svc
|
||||
from grimoire.services import lessons as lessons_svc
|
||||
from grimoire.services import rulebooks as rulebooks_svc
|
||||
from tests.helpers import ensure_user
|
||||
|
||||
pytestmark = [
|
||||
@@ -32,7 +32,7 @@ pytestmark = [
|
||||
@pytest.fixture(autouse=True)
|
||||
def _no_reindex():
|
||||
"""Rule writes detach an embedding refresh that outlives the test's loop."""
|
||||
with patch("scribe.services.rulebooks._refresh_rule_embedding", MagicMock()):
|
||||
with patch("grimoire.services.rulebooks._refresh_rule_embedding", MagicMock()):
|
||||
yield
|
||||
|
||||
|
||||
@@ -231,7 +231,7 @@ async def test_the_unjudged_list_is_scoped_to_the_reader(world):
|
||||
|
||||
|
||||
async def test_deleting_the_lesson_takes_its_answer_with_it(world):
|
||||
from scribe.models.lesson_rule_link import LessonNoRule
|
||||
from grimoire.models.lesson_rule_link import LessonNoRule
|
||||
|
||||
await links_svc.set_no_rule(world["owner"], world["lesson"], "alone")
|
||||
async with async_session() as s:
|
||||
|
||||
@@ -11,12 +11,12 @@ from unittest.mock import AsyncMock, patch
|
||||
import pytest
|
||||
import pytest_asyncio
|
||||
|
||||
from scribe.models import async_session
|
||||
from scribe.models.embedding import EMBEDDING_DIM, MilestoneEmbedding
|
||||
from scribe.models.milestone import Milestone
|
||||
from scribe.models.project import Project
|
||||
from scribe.services import dedup as dedup_svc
|
||||
from scribe.services.embeddings import CHUNKER_VERSION, EMBEDDING_MODEL, semantic_search_milestones
|
||||
from grimoire.models import async_session
|
||||
from grimoire.models.embedding import EMBEDDING_DIM, MilestoneEmbedding
|
||||
from grimoire.models.milestone import Milestone
|
||||
from grimoire.models.project import Project
|
||||
from grimoire.services import dedup as dedup_svc
|
||||
from grimoire.services.embeddings import CHUNKER_VERSION, EMBEDDING_MODEL, semantic_search_milestones
|
||||
from tests.helpers import ensure_user
|
||||
|
||||
pytestmark = [pytest.mark.integration, pytest.mark.usefixtures("_dispose_engine", "_no_embedding")]
|
||||
@@ -56,7 +56,7 @@ async def roadmap():
|
||||
|
||||
|
||||
async def _found(user_id, **kw) -> list[int]:
|
||||
with patch("scribe.services.embeddings.get_embedding", AsyncMock(return_value=NEAR)):
|
||||
with patch("grimoire.services.embeddings.get_embedding", AsyncMock(return_value=NEAR)):
|
||||
hits = await semantic_search_milestones(user_id, "book metadata and providers",
|
||||
threshold=0.5, limit=10, **kw)
|
||||
return [m.id for _s, m in hits]
|
||||
@@ -89,7 +89,7 @@ LONG = "Resolve works and editions against the metadata providers. " * 5
|
||||
|
||||
|
||||
async def _gate(roadmap, title, text="", project=None):
|
||||
with patch("scribe.services.embeddings.get_embedding", AsyncMock(return_value=NEAR)):
|
||||
with patch("grimoire.services.embeddings.get_embedding", AsyncMock(return_value=NEAR)):
|
||||
return await dedup_svc.plan_gate(
|
||||
roadmap["owner"], project or roadmap["mine"], title, text,
|
||||
)
|
||||
|
||||
@@ -10,9 +10,9 @@ import pytest
|
||||
import pytest_asyncio
|
||||
from sqlalchemy import delete, select
|
||||
|
||||
from scribe.models import async_session
|
||||
from scribe.models.moment_mapping import MomentMapping
|
||||
from scribe.services import moment_actions as ma
|
||||
from grimoire.models import async_session
|
||||
from grimoire.models.moment_mapping import MomentMapping
|
||||
from grimoire.services import moment_actions as ma
|
||||
from tests.helpers import ensure_user
|
||||
|
||||
pytestmark = [pytest.mark.integration, pytest.mark.usefixtures("_dispose_engine")]
|
||||
|
||||
@@ -16,11 +16,11 @@ import pytest
|
||||
import pytest_asyncio
|
||||
from sqlalchemy import delete
|
||||
|
||||
from scribe.models import async_session
|
||||
from scribe.models.embedding import EMBEDDING_DIM, NoteEmbedding
|
||||
from scribe.models.note import Note
|
||||
from scribe.models.user import User
|
||||
from scribe.services.embeddings import semantic_search_notes
|
||||
from grimoire.models import async_session
|
||||
from grimoire.models.embedding import EMBEDDING_DIM, NoteEmbedding
|
||||
from grimoire.models.note import Note
|
||||
from grimoire.models.user import User
|
||||
from grimoire.services.embeddings import semantic_search_notes
|
||||
|
||||
pytestmark = [pytest.mark.integration, pytest.mark.usefixtures("_dispose_engine")]
|
||||
|
||||
@@ -33,7 +33,7 @@ def _vec(*nonzero_first):
|
||||
|
||||
def _emb(note_id, user_id, chunk_index, vec):
|
||||
"""A chunk row at the current chunker version."""
|
||||
from scribe.services.embeddings import CHUNKER_VERSION, EMBEDDING_MODEL
|
||||
from grimoire.services.embeddings import CHUNKER_VERSION, EMBEDDING_MODEL
|
||||
|
||||
return NoteEmbedding(
|
||||
note_id=note_id,
|
||||
@@ -83,7 +83,7 @@ async def seeded():
|
||||
async def test_semantic_search_ranks_and_thresholds_via_pgvector(seeded):
|
||||
user_id, near_id, far_id = seeded
|
||||
with patch(
|
||||
"scribe.services.embeddings.get_embedding",
|
||||
"grimoire.services.embeddings.get_embedding",
|
||||
AsyncMock(return_value=_vec(1.0)),
|
||||
):
|
||||
results = await semantic_search_notes(user_id=user_id, query="anything", limit=10)
|
||||
@@ -105,7 +105,7 @@ async def test_semantic_search_ranks_and_thresholds_via_pgvector(seeded):
|
||||
async def test_low_threshold_lets_orthogonal_through(seeded):
|
||||
user_id, near_id, far_id = seeded
|
||||
with patch(
|
||||
"scribe.services.embeddings.get_embedding",
|
||||
"grimoire.services.embeddings.get_embedding",
|
||||
AsyncMock(return_value=_vec(1.0)),
|
||||
):
|
||||
results = await semantic_search_notes(
|
||||
|
||||
@@ -8,13 +8,13 @@ of steps they cannot open.
|
||||
import pytest
|
||||
import pytest_asyncio
|
||||
|
||||
from scribe.models import async_session
|
||||
from scribe.models.note import Note
|
||||
from scribe.models.project import Project
|
||||
from scribe.models.share import NoteShare, ProjectShare
|
||||
from scribe.services import notes as notes_svc
|
||||
from scribe.services.placement import task_placement
|
||||
from scribe.services.record_batch import BatchItem, BatchMilestone, create_batch
|
||||
from grimoire.models import async_session
|
||||
from grimoire.models.note import Note
|
||||
from grimoire.models.project import Project
|
||||
from grimoire.models.share import NoteShare, ProjectShare
|
||||
from grimoire.services import notes as notes_svc
|
||||
from grimoire.services.placement import task_placement
|
||||
from grimoire.services.record_batch import BatchItem, BatchMilestone, create_batch
|
||||
from tests.helpers import ensure_user
|
||||
|
||||
pytestmark = [pytest.mark.integration, pytest.mark.usefixtures("_dispose_engine", "_no_embedding")]
|
||||
|
||||
@@ -10,11 +10,11 @@ import pytest
|
||||
import pytest_asyncio
|
||||
from sqlalchemy import select
|
||||
|
||||
from scribe.models import async_session
|
||||
from scribe.models.family import Platform
|
||||
from scribe.models.project import Project
|
||||
from scribe.services import inception as inception_svc
|
||||
from scribe.services import platforms as platforms_svc
|
||||
from grimoire.models import async_session
|
||||
from grimoire.models.family import Platform
|
||||
from grimoire.models.project import Project
|
||||
from grimoire.services import inception as inception_svc
|
||||
from grimoire.services import platforms as platforms_svc
|
||||
from tests.helpers import ensure_user
|
||||
|
||||
pytestmark = [pytest.mark.integration, pytest.mark.usefixtures("_dispose_engine")]
|
||||
|
||||
@@ -28,11 +28,11 @@ import pytest
|
||||
import pytest_asyncio
|
||||
from sqlalchemy import select
|
||||
|
||||
from scribe.models import async_session
|
||||
from scribe.models.note import Note
|
||||
from scribe.models.rulebook import Rulebook
|
||||
from scribe.models.rule_version import RuleVersion
|
||||
from scribe.services import rulebooks as rulebooks_svc
|
||||
from grimoire.models import async_session
|
||||
from grimoire.models.note import Note
|
||||
from grimoire.models.rulebook import Rulebook
|
||||
from grimoire.models.rule_version import RuleVersion
|
||||
from grimoire.services import rulebooks as rulebooks_svc
|
||||
from tests.helpers import ensure_user
|
||||
|
||||
pytestmark = [pytest.mark.integration, pytest.mark.usefixtures("_dispose_engine")]
|
||||
|
||||
@@ -12,13 +12,13 @@ import pytest
|
||||
import pytest_asyncio
|
||||
from sqlalchemy import func, select
|
||||
|
||||
from scribe.models import async_session
|
||||
from scribe.models.milestone import Milestone
|
||||
from scribe.models.note import Note
|
||||
from scribe.models.project import Project
|
||||
from scribe.services import record_refs
|
||||
from scribe.services.planning import start_planning
|
||||
from scribe.services.record_batch import BatchItem, create_batch
|
||||
from grimoire.models import async_session
|
||||
from grimoire.models.milestone import Milestone
|
||||
from grimoire.models.note import Note
|
||||
from grimoire.models.project import Project
|
||||
from grimoire.services import record_refs
|
||||
from grimoire.services.planning import start_planning
|
||||
from grimoire.services.record_batch import BatchItem, create_batch
|
||||
from tests.helpers import ensure_user
|
||||
|
||||
pytestmark = [pytest.mark.integration, pytest.mark.usefixtures("_dispose_engine", "_no_embedding")]
|
||||
|
||||
@@ -24,9 +24,9 @@ import pytest_asyncio
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.exc import IntegrityError
|
||||
|
||||
from scribe.models import async_session
|
||||
from scribe.models.rulebook import Rule, Rulebook
|
||||
from scribe.services import rulebooks as rulebooks_svc
|
||||
from grimoire.models import async_session
|
||||
from grimoire.models.rulebook import Rule, Rulebook
|
||||
from grimoire.services import rulebooks as rulebooks_svc
|
||||
from tests.helpers import ensure_user
|
||||
|
||||
pytestmark = [pytest.mark.integration, pytest.mark.usefixtures("_dispose_engine")]
|
||||
|
||||
@@ -16,12 +16,12 @@ import pytest
|
||||
import pytest_asyncio
|
||||
from sqlalchemy import select
|
||||
|
||||
from scribe.models import async_session
|
||||
from scribe.models.rule_moment_judgment import RuleMomentJudgment
|
||||
from scribe.models.rulebook import Rulebook
|
||||
from scribe.models.user import User
|
||||
from scribe.services import rule_moment_judgments as judgments_svc
|
||||
from scribe.services import rulebooks as rulebooks_svc
|
||||
from grimoire.models import async_session
|
||||
from grimoire.models.rule_moment_judgment import RuleMomentJudgment
|
||||
from grimoire.models.rulebook import Rulebook
|
||||
from grimoire.models.user import User
|
||||
from grimoire.services import rule_moment_judgments as judgments_svc
|
||||
from grimoire.services import rulebooks as rulebooks_svc
|
||||
from tests.helpers import ensure_user
|
||||
|
||||
pytestmark = [pytest.mark.integration, pytest.mark.usefixtures("_dispose_engine")]
|
||||
@@ -207,7 +207,7 @@ def days(monkeypatch):
|
||||
queue: list[str] = []
|
||||
|
||||
def situation(_now):
|
||||
from scribe.services import lesson_rules
|
||||
from grimoire.services import lesson_rules
|
||||
return lesson_rules.situation_key("d", queue.pop(0))
|
||||
|
||||
monkeypatch.setattr(judgments_svc, "misfire_situation", situation)
|
||||
|
||||
@@ -14,14 +14,14 @@ import pytest
|
||||
import pytest_asyncio
|
||||
from sqlalchemy import select
|
||||
|
||||
from scribe.models import async_session
|
||||
from scribe.models.project import Project
|
||||
from scribe.models.rule_moment_judgment import RuleMomentJudgment
|
||||
from scribe.models.rulebook import Rule, Rulebook, RulebookTopic
|
||||
from scribe.models.user import User
|
||||
from scribe.services import backup
|
||||
from scribe.services import projects as projects_svc
|
||||
from scribe.services import rulebooks as rulebooks_svc
|
||||
from grimoire.models import async_session
|
||||
from grimoire.models.project import Project
|
||||
from grimoire.models.rule_moment_judgment import RuleMomentJudgment
|
||||
from grimoire.models.rulebook import Rule, Rulebook, RulebookTopic
|
||||
from grimoire.models.user import User
|
||||
from grimoire.services import backup
|
||||
from grimoire.services import projects as projects_svc
|
||||
from grimoire.services import rulebooks as rulebooks_svc
|
||||
from tests.helpers import ensure_user
|
||||
|
||||
pytestmark = [pytest.mark.integration, pytest.mark.usefixtures("_dispose_engine")]
|
||||
@@ -119,7 +119,7 @@ async def test_a_backup_carries_the_mounts_to_the_restored_rule(world):
|
||||
await rulebooks_svc.set_rule_moments(rule.id, uid, ["work.finish", "reply.report"])
|
||||
# A misfire report on one of them (v24) — the operator's evidence for
|
||||
# taking a mount off must not be lost to a restore.
|
||||
from scribe.services import rule_moment_judgments as judgments_svc
|
||||
from grimoire.services import rule_moment_judgments as judgments_svc
|
||||
reported = await judgments_svc.misfired(
|
||||
uid, rule.id, "reply.report", why="the reply only asked a question",
|
||||
reached_by="the reply that ends this turn",
|
||||
@@ -245,9 +245,9 @@ async def test_moment_usage_counts_deliveries_and_the_agent_opens_that_followed(
|
||||
|
||||
from sqlalchemy import delete
|
||||
|
||||
from scribe.models.rule_usage import PULLED, SURFACED, RuleUsageEvent
|
||||
from scribe.services.retrieval_pipeline import MOMENT_RULE_SOURCE
|
||||
from scribe.services.retrieval_telemetry import moment_usage
|
||||
from grimoire.models.rule_usage import PULLED, SURFACED, RuleUsageEvent
|
||||
from grimoire.services.retrieval_pipeline import MOMENT_RULE_SOURCE
|
||||
from grimoire.services.retrieval_telemetry import moment_usage
|
||||
|
||||
uid, sid, rule = world["uid"], world["sid"], world["rule"]
|
||||
other = await rulebooks_svc.create_rule(
|
||||
|
||||
@@ -14,12 +14,12 @@ from unittest.mock import MagicMock, patch
|
||||
import pytest
|
||||
import pytest_asyncio
|
||||
|
||||
from scribe.models import async_session
|
||||
from scribe.models.project import Project
|
||||
from scribe.models.rulebook import Rule
|
||||
from scribe.services import canonical_systems as canonical_svc
|
||||
from scribe.services import rule_versions as rv_svc
|
||||
from scribe.services import rulebooks as rulebooks_svc
|
||||
from grimoire.models import async_session
|
||||
from grimoire.models.project import Project
|
||||
from grimoire.models.rulebook import Rule
|
||||
from grimoire.services import canonical_systems as canonical_svc
|
||||
from grimoire.services import rule_versions as rv_svc
|
||||
from grimoire.services import rulebooks as rulebooks_svc
|
||||
from tests.helpers import ensure_user, rule_row
|
||||
|
||||
pytestmark = [pytest.mark.integration, pytest.mark.usefixtures("_dispose_engine")]
|
||||
@@ -29,7 +29,7 @@ pytestmark = [pytest.mark.integration, pytest.mark.usefixtures("_dispose_engine"
|
||||
def _no_reindex():
|
||||
"""Rule writes detach an embedding refresh that outlives the test's loop
|
||||
and races the next fixture; nothing here is about recall."""
|
||||
with patch("scribe.services.rulebooks._refresh_rule_embedding", MagicMock()):
|
||||
with patch("grimoire.services.rulebooks._refresh_rule_embedding", MagicMock()):
|
||||
yield
|
||||
|
||||
|
||||
|
||||
@@ -14,12 +14,12 @@ from unittest.mock import AsyncMock, MagicMock, patch
|
||||
import pytest
|
||||
import pytest_asyncio
|
||||
|
||||
from scribe.models import async_session
|
||||
from scribe.models.embedding import EMBEDDING_DIM, RuleEmbedding
|
||||
from scribe.models.project import Project
|
||||
from scribe.models.share import ProjectShare
|
||||
from scribe.services import rulebooks as rulebooks_svc
|
||||
from scribe.services.embeddings import CHUNKER_VERSION, EMBEDDING_MODEL, semantic_search_rules
|
||||
from grimoire.models import async_session
|
||||
from grimoire.models.embedding import EMBEDDING_DIM, RuleEmbedding
|
||||
from grimoire.models.project import Project
|
||||
from grimoire.models.share import ProjectShare
|
||||
from grimoire.services import rulebooks as rulebooks_svc
|
||||
from grimoire.services.embeddings import CHUNKER_VERSION, EMBEDDING_MODEL, semantic_search_rules
|
||||
from tests.helpers import ensure_user
|
||||
|
||||
pytestmark = [pytest.mark.integration, pytest.mark.usefixtures("_dispose_engine")]
|
||||
@@ -46,7 +46,7 @@ async def homes():
|
||||
ids = {"owner": owner.id, "collaborator": collaborator.id, "a": a.id, "b": b.id}
|
||||
await s.commit()
|
||||
|
||||
with patch("scribe.services.rulebooks._refresh_rule_embedding", MagicMock()):
|
||||
with patch("grimoire.services.rulebooks._refresh_rule_embedding", MagicMock()):
|
||||
book = await rulebooks_svc.create_rulebook(ids["owner"], "Scope house style")
|
||||
topic = await rulebooks_svc.create_topic(book.id, ids["owner"], "everywhere")
|
||||
glob = await rulebooks_svc.create_rule(
|
||||
@@ -81,9 +81,9 @@ async def _found(user_id: int, **scope) -> set[int]:
|
||||
same."""
|
||||
report: dict = {}
|
||||
raised: list[str] = []
|
||||
with patch("scribe.services.embeddings.get_embedding",
|
||||
with patch("grimoire.services.embeddings.get_embedding",
|
||||
AsyncMock(return_value=QUERY_VEC)), \
|
||||
patch("scribe.services.embeddings.logger") as log:
|
||||
patch("grimoire.services.embeddings.logger") as log:
|
||||
# Called inside the except block, so the traceback is still current.
|
||||
log.warning.side_effect = lambda *_a, **_k: raised.append(traceback.format_exc())
|
||||
hits = await semantic_search_rules(user_id, "anything", limit=10,
|
||||
@@ -139,7 +139,7 @@ async def test_an_in_scope_rule_is_found_however_many_closer_rules_are_out_of_sc
|
||||
mine_vec = [0.8, 0.6] + [0.0] * (EMBEDDING_DIM - 2) # cosine 0.8
|
||||
books = []
|
||||
try:
|
||||
with patch("scribe.services.rulebooks._refresh_rule_embedding", MagicMock()):
|
||||
with patch("grimoire.services.rulebooks._refresh_rule_embedding", MagicMock()):
|
||||
book = await rulebooks_svc.create_rulebook(reader_id, "Reader's rules")
|
||||
books.append((book.id, reader_id))
|
||||
topic = await rulebooks_svc.create_topic(book.id, reader_id, "mine")
|
||||
|
||||
@@ -17,11 +17,11 @@ planning read carries — and what mocks can't prove about it:
|
||||
import pytest
|
||||
import pytest_asyncio
|
||||
|
||||
from scribe.models import async_session
|
||||
from scribe.models.project import Project
|
||||
from scribe.services import canonical_systems as canonical_svc
|
||||
from scribe.services import rulebooks as rulebooks_svc
|
||||
from scribe.services import systems as systems_svc
|
||||
from grimoire.models import async_session
|
||||
from grimoire.models.project import Project
|
||||
from grimoire.services import canonical_systems as canonical_svc
|
||||
from grimoire.services import rulebooks as rulebooks_svc
|
||||
from grimoire.services import systems as systems_svc
|
||||
from tests.helpers import ensure_user
|
||||
|
||||
pytestmark = [pytest.mark.integration, pytest.mark.usefixtures("_dispose_engine")]
|
||||
|
||||
@@ -20,9 +20,9 @@ from datetime import datetime, timezone
|
||||
import pytest
|
||||
import pytest_asyncio
|
||||
|
||||
from scribe.models import async_session
|
||||
from scribe.models.rulebook import Rule
|
||||
from scribe.services import rulebooks as rulebooks_svc
|
||||
from grimoire.models import async_session
|
||||
from grimoire.models.rulebook import Rule
|
||||
from grimoire.services import rulebooks as rulebooks_svc
|
||||
from tests.helpers import ensure_user, rule_row
|
||||
|
||||
pytestmark = [pytest.mark.integration, pytest.mark.usefixtures("_dispose_engine")]
|
||||
|
||||
@@ -19,11 +19,11 @@ import pytest
|
||||
import pytest_asyncio
|
||||
from sqlalchemy import select
|
||||
|
||||
from scribe.models import async_session
|
||||
from scribe.models.rulebook import Rule, Rulebook
|
||||
from scribe.models.rule_version import RuleVersion
|
||||
from scribe.services import rule_versions as rv_svc
|
||||
from scribe.services import rulebooks as rulebooks_svc
|
||||
from grimoire.models import async_session
|
||||
from grimoire.models.rulebook import Rule, Rulebook
|
||||
from grimoire.models.rule_version import RuleVersion
|
||||
from grimoire.services import rule_versions as rv_svc
|
||||
from grimoire.services import rulebooks as rulebooks_svc
|
||||
from tests.helpers import ensure_user
|
||||
|
||||
pytestmark = [pytest.mark.integration, pytest.mark.usefixtures("_dispose_engine")]
|
||||
|
||||
@@ -20,17 +20,17 @@ from unittest.mock import AsyncMock, patch
|
||||
import pytest
|
||||
from sqlalchemy import delete
|
||||
|
||||
from scribe.models import async_session
|
||||
from scribe.models.embedding import (
|
||||
from grimoire.models import async_session
|
||||
from grimoire.models.embedding import (
|
||||
EMBEDDING_DIM, MilestoneEmbedding, NoteEmbedding, SystemEmbedding,
|
||||
)
|
||||
from scribe.models.milestone import Milestone
|
||||
from scribe.models.note import Note
|
||||
from scribe.models.project import Project
|
||||
from scribe.models.system import System
|
||||
from scribe.models.user import User
|
||||
from scribe.services import embeddings as emb
|
||||
from scribe.services.embeddings import CHUNKER_VERSION, EMBEDDING_MODEL
|
||||
from grimoire.models.milestone import Milestone
|
||||
from grimoire.models.note import Note
|
||||
from grimoire.models.project import Project
|
||||
from grimoire.models.system import System
|
||||
from grimoire.models.user import User
|
||||
from grimoire.services import embeddings as emb
|
||||
from grimoire.services.embeddings import CHUNKER_VERSION, EMBEDDING_MODEL
|
||||
from tests.helpers import ensure_user
|
||||
|
||||
pytestmark = [pytest.mark.integration, pytest.mark.usefixtures("_dispose_engine", "_no_embedding")]
|
||||
@@ -59,9 +59,9 @@ async def _found(search, user_id: int, **scope) -> set[int]:
|
||||
reads as an empty result — the swallowed traceback is captured and named
|
||||
in the failure instead."""
|
||||
raised: list[str] = []
|
||||
with patch("scribe.services.embeddings.get_embedding",
|
||||
with patch("grimoire.services.embeddings.get_embedding",
|
||||
AsyncMock(return_value=QUERY_VEC)), \
|
||||
patch("scribe.services.embeddings.logger") as log:
|
||||
patch("grimoire.services.embeddings.logger") as log:
|
||||
log.warning.side_effect = lambda *_a, **_k: raised.append(traceback.format_exc())
|
||||
hits = await search(user_id, "anything", limit=10, threshold=0.5, **scope)
|
||||
assert not raised, "the search raised:\n" + "\n".join(raised)
|
||||
|
||||
@@ -9,11 +9,11 @@ import pytest
|
||||
import pytest_asyncio
|
||||
from sqlalchemy import select
|
||||
|
||||
from scribe.models import async_session
|
||||
from scribe.models.code_shape import CodeShape
|
||||
from scribe.models.project import Project
|
||||
from scribe.models.user import User
|
||||
from scribe.services.shape_ledger import (
|
||||
from grimoire.models import async_session
|
||||
from grimoire.models.code_shape import CodeShape
|
||||
from grimoire.models.project import Project
|
||||
from grimoire.models.user import User
|
||||
from grimoire.services.shape_ledger import (
|
||||
classify_shapes,
|
||||
classify_shapes_where,
|
||||
list_project_shapes,
|
||||
@@ -36,7 +36,7 @@ SHAPES = [
|
||||
@pytest_asyncio.fixture
|
||||
async def seeded():
|
||||
"""Owner + outsider, a project with a synced 4-shape ledger, one snippet."""
|
||||
from scribe.services import snippets as snippets_svc
|
||||
from grimoire.services import snippets as snippets_svc
|
||||
|
||||
async with async_session() as s:
|
||||
owner = await ensure_user(s, "classify_owner")
|
||||
@@ -174,7 +174,7 @@ async def test_sync_stamps_scoped_rows_and_unstamps_when_they_become_reachable(s
|
||||
"""By-construction one-offs arrive `scoped` (mechanical), count as
|
||||
accounted, are reached by the sweep's default, and go back to the todo
|
||||
if a later tree makes them ordinary. A judgment overrides the stamp."""
|
||||
from scribe.services.coverage import ArchiveShape
|
||||
from grimoire.services.coverage import ArchiveShape
|
||||
owner, pid, sid = seeded["owner"], seeded["pid"], seeded["snippet"]
|
||||
scoped_shapes = [
|
||||
ArchiveShape("web/Card.vue", "css", "card", ".card {", "s1", ".card { x: 1 }", True),
|
||||
@@ -206,7 +206,7 @@ async def test_uses_edges_are_the_consumer_map(seeded):
|
||||
"""A shape keeps ONE snippet_id (what it is) and any number of
|
||||
uses edges (what it calls); the snippet's consumer map lists them,
|
||||
list_shapes(uses=N) finds them, and a sweep can write them."""
|
||||
from scribe.services import snippets as snippets_svc
|
||||
from grimoire.services import snippets as snippets_svc
|
||||
owner, pid, sid = seeded["owner"], seeded["pid"], seeded["snippet"]
|
||||
helper = await snippets_svc.create_snippet(
|
||||
owner, name="cls_hash_helper", code="def hash_token(raw):\n return raw\n",
|
||||
@@ -232,7 +232,7 @@ async def test_uses_edges_are_the_consumer_map(seeded):
|
||||
assert (await list_project_shapes(owner, pid, uses=hid))[1] == 2
|
||||
# The proposer writes uses edges for JUDGED rows too: Config (exempt)
|
||||
# names hash_token in its body → an edge, no proposal.
|
||||
from scribe.services.shape_ledger import propose_for_repo
|
||||
from grimoire.services.shape_ledger import propose_for_repo
|
||||
await classify_shapes(owner, pid, [
|
||||
{"path": "src/app.py", "symbol": "Config", "status": "exempt", "reason": "settings"},
|
||||
])
|
||||
@@ -270,8 +270,8 @@ async def test_list_filters_compose(seeded):
|
||||
|
||||
@pytest.mark.integration
|
||||
async def test_get_snippet_carries_the_structured_consumer_map(seeded):
|
||||
from scribe.mcp._context import _user_id_ctx
|
||||
from scribe.mcp.tools.snippets import get_snippet
|
||||
from grimoire.mcp._context import _user_id_ctx
|
||||
from grimoire.mcp.tools.snippets import get_snippet
|
||||
|
||||
owner, pid, sid = seeded["owner"], seeded["pid"], seeded["snippet"]
|
||||
await classify_shapes(owner, pid, [
|
||||
@@ -303,7 +303,7 @@ async def test_sync_refiles_rows_whose_snippet_was_purged(seeded):
|
||||
{"path": "src/app.py", "symbol": "make_app", "status": "instance",
|
||||
"snippet_id": sid},
|
||||
])
|
||||
from scribe.models.note import Note
|
||||
from grimoire.models.note import Note
|
||||
|
||||
async with async_session() as s:
|
||||
note = await s.get(Note, sid)
|
||||
@@ -331,7 +331,7 @@ async def test_write_path_evidence_is_a_proposal_never_a_verdict(seeded):
|
||||
edge — a fact about the code — is still recorded."""
|
||||
from datetime import datetime, timezone
|
||||
|
||||
from scribe.services.shape_ledger import suggest_write_path_instances
|
||||
from grimoire.services.shape_ledger import suggest_write_path_instances
|
||||
|
||||
owner, other, pid, sid = (
|
||||
seeded["owner"], seeded["other"], seeded["pid"], seeded["snippet"]
|
||||
@@ -396,7 +396,7 @@ async def test_a_brand_new_shape_gets_a_provisional_row_carrying_the_suggestion(
|
||||
carry a suggestion."""
|
||||
from datetime import datetime, timezone
|
||||
|
||||
from scribe.services.shape_ledger import suggest_write_path_instances, unjudged_shapes
|
||||
from grimoire.services.shape_ledger import suggest_write_path_instances, unjudged_shapes
|
||||
|
||||
owner, pid, sid = seeded["owner"], seeded["pid"], seeded["snippet"]
|
||||
pulled = {sid: datetime.now(timezone.utc)}
|
||||
@@ -446,8 +446,8 @@ async def test_recent_pulls_reads_the_usage_stream(seeded):
|
||||
window; a surfacing alone is not a pull."""
|
||||
from datetime import datetime, timedelta, timezone
|
||||
|
||||
from scribe.models.note_usage import PULLED, SURFACED, NoteUsageEvent
|
||||
from scribe.services.shape_ledger import recent_pulls
|
||||
from grimoire.models.note_usage import PULLED, SURFACED, NoteUsageEvent
|
||||
from grimoire.services.shape_ledger import recent_pulls
|
||||
|
||||
owner, sid = seeded["owner"], seeded["snippet"]
|
||||
now = datetime.now(timezone.utc)
|
||||
@@ -473,7 +473,7 @@ def _quiet_semantic():
|
||||
bases and the bookkeeping, so it answers "nothing" here."""
|
||||
from unittest.mock import AsyncMock, patch
|
||||
|
||||
from scribe.services import shape_ledger
|
||||
from grimoire.services import shape_ledger
|
||||
return patch.object(shape_ledger, "_semantic_canon", AsyncMock(return_value=None))
|
||||
|
||||
|
||||
@@ -494,7 +494,7 @@ async def test_proposer_proposes_and_confirm_classifies(seeded):
|
||||
nothing for the unrelated — skips rows whose content it already judged,
|
||||
and a scoped confirm turns proposals into agent instances while a
|
||||
classify on another retires its proposal."""
|
||||
from scribe.services.shape_ledger import (
|
||||
from grimoire.services.shape_ledger import (
|
||||
confirm_proposals, propose_for_repo,
|
||||
)
|
||||
|
||||
@@ -564,7 +564,7 @@ async def test_derive_groups_land_on_rows_and_in_the_summary(seeded):
|
||||
"""Shapes with no canon hit that repeat — identical bodies in two files,
|
||||
the same name in three — carry a derive proposal, and the readout ranks
|
||||
the families. A canon proposal keeps a row out of any derive group."""
|
||||
from scribe.services.shape_ledger import (
|
||||
from grimoire.services.shape_ledger import (
|
||||
apply_derive_groups, live_rows, propose_for_repo, proposal_summary,
|
||||
)
|
||||
|
||||
@@ -627,7 +627,7 @@ async def test_write_time_derive_names_the_family_or_the_canon_for_a_name(seeded
|
||||
count; for CSS a NAME family, never a body one); a
|
||||
name whose canonical row lives elsewhere → that canon; a judged row at
|
||||
the path, the canon's own file, or an unknown name → silence."""
|
||||
from scribe.services.shape_ledger import apply_derive_groups, write_time_derive
|
||||
from grimoire.services.shape_ledger import apply_derive_groups, write_time_derive
|
||||
|
||||
owner, pid, sid = seeded["owner"], seeded["pid"], seeded["snippet"]
|
||||
defs = _defs(
|
||||
@@ -672,7 +672,7 @@ async def test_derive_new_names_the_copy_that_joined_a_family_since_the_stamp(se
|
||||
newcomer — the drift since the last refresh, not the whole family."""
|
||||
from datetime import datetime, timezone
|
||||
|
||||
from scribe.services.shape_ledger import (
|
||||
from grimoire.services.shape_ledger import (
|
||||
apply_derive_groups, derive_new_summary, live_rows,
|
||||
)
|
||||
|
||||
@@ -704,8 +704,8 @@ async def test_transition_and_prefix_references_clear_the_unused_css_flag(seeded
|
||||
a transition `name=` and a concatenated name — travel from the real
|
||||
extractor through resolution into the flag, so rules they reach stop
|
||||
being reported as unused. Only a rule nothing can reach stays listed."""
|
||||
from scribe.services.coverage import class_references
|
||||
from scribe.services.shape_ledger import sync_repo_consumers, used_by_map, live_rows
|
||||
from grimoire.services.coverage import class_references
|
||||
from grimoire.services.shape_ledger import sync_repo_consumers, used_by_map, live_rows
|
||||
|
||||
pid, owner = seeded["pid"], seeded["owner"]
|
||||
defs = _defs(
|
||||
@@ -756,7 +756,7 @@ async def test_consumer_map_syncs_edges_from_template_references(seeded):
|
||||
resolution for a scoped class, fan-out to the shared sheet for a class a
|
||||
template does not define, counts refreshed and stale edges removed on
|
||||
the next sync, and a vanished row's edges gone with it."""
|
||||
from scribe.services.shape_ledger import consumers_of, live_rows, sync_repo_consumers
|
||||
from grimoire.services.shape_ledger import consumers_of, live_rows, sync_repo_consumers
|
||||
|
||||
pid = seeded["pid"]
|
||||
defs = _defs(
|
||||
@@ -791,7 +791,7 @@ async def test_consumer_map_syncs_edges_from_template_references(seeded):
|
||||
|
||||
# The readout side: used_by per css row, the unused-css flag, and the
|
||||
# family's consumers on the write-path check.
|
||||
from scribe.services.shape_ledger import (
|
||||
from grimoire.services.shape_ledger import (
|
||||
apply_derive_groups, used_by_map, write_time_derive,
|
||||
)
|
||||
owner = seeded["owner"]
|
||||
@@ -825,12 +825,12 @@ async def test_a_second_confirm_dialog_is_detected_and_named(seeded):
|
||||
judgment clears the flag; a shape proposed AS the canon is not flagged."""
|
||||
from datetime import datetime, timedelta, timezone
|
||||
|
||||
from scribe.services.shape_ledger import (
|
||||
from grimoire.services.shape_ledger import (
|
||||
divergence_summary, flag_divergence, live_rows, propose_for_repo,
|
||||
write_time_divergence,
|
||||
)
|
||||
|
||||
from scribe.services import snippets as snippets_svc
|
||||
from grimoire.services import snippets as snippets_svc
|
||||
|
||||
owner, pid = seeded["owner"], seeded["pid"]
|
||||
# The confirm helper is TS canon: a sym basis only proposes
|
||||
@@ -917,8 +917,8 @@ async def test_weak_stamps_elect_no_canon_and_their_flags_are_withdrawn(seeded):
|
||||
|
||||
from sqlalchemy import update
|
||||
|
||||
from scribe.services import snippets as snippets_svc
|
||||
from scribe.services.shape_ledger import (
|
||||
from grimoire.services import snippets as snippets_svc
|
||||
from grimoire.services.shape_ledger import (
|
||||
_RESEMBLE_REASON, flag_divergence, propose_for_repo, write_time_divergence,
|
||||
)
|
||||
|
||||
@@ -999,9 +999,9 @@ async def test_a_semantic_miss_does_not_silence_the_prompt(seeded):
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from unittest.mock import AsyncMock, patch
|
||||
|
||||
from scribe.services import shape_ledger
|
||||
from scribe.services import snippets as snippets_svc
|
||||
from scribe.services.shape_ledger import flag_divergence, live_rows, propose_for_repo
|
||||
from grimoire.services import shape_ledger
|
||||
from grimoire.services import snippets as snippets_svc
|
||||
from grimoire.services.shape_ledger import flag_divergence, live_rows, propose_for_repo
|
||||
|
||||
owner, pid = seeded["owner"], seeded["pid"]
|
||||
canon = await snippets_svc.create_snippet(
|
||||
@@ -1051,7 +1051,7 @@ async def _two_generations(seeded, tag: str):
|
||||
"""
|
||||
from datetime import datetime, timezone
|
||||
|
||||
from scribe.services import snippets as snippets_svc
|
||||
from grimoire.services import snippets as snippets_svc
|
||||
|
||||
owner, pid = seeded["owner"], seeded["pid"]
|
||||
canon = await snippets_svc.create_snippet(
|
||||
@@ -1092,8 +1092,8 @@ async def test_the_capped_semantic_pass_reaches_the_new_shape_first(seeded):
|
||||
ONE check, the new shape must be the one that gets it."""
|
||||
from unittest.mock import AsyncMock, patch
|
||||
|
||||
from scribe.services import shape_ledger
|
||||
from scribe.services.shape_ledger import live_rows, propose_for_repo
|
||||
from grimoire.services import shape_ledger
|
||||
from grimoire.services.shape_ledger import live_rows, propose_for_repo
|
||||
|
||||
owner, pid, later, _previous = await _two_generations(seeded, "cap")
|
||||
with patch.object(shape_ledger, "_semantic_canon", AsyncMock(return_value=None)):
|
||||
@@ -1108,7 +1108,7 @@ async def test_the_capped_semantic_pass_reaches_the_new_shape_first(seeded):
|
||||
|
||||
@pytest.mark.integration
|
||||
async def test_history_records_what_was_used_when_and_drift_asks_for_a_recheck(seeded):
|
||||
from scribe.services.shape_ledger import shape_history
|
||||
from grimoire.services.shape_ledger import shape_history
|
||||
|
||||
owner, other, pid, sid = (
|
||||
seeded["owner"], seeded["other"], seeded["pid"], seeded["snippet"]
|
||||
@@ -1163,7 +1163,7 @@ async def test_a_shape_written_this_turn_is_judged_before_the_sync_sees_it(seede
|
||||
then reads the shape keeps the judgment, and a sync that does NOT have it
|
||||
yet (the code has not reached the bound ref) keeps it through the vanish
|
||||
and the reappearance. Without `repo` the old contract holds."""
|
||||
from scribe.services import repo_bindings as repo_bindings_svc
|
||||
from grimoire.services import repo_bindings as repo_bindings_svc
|
||||
|
||||
owner, pid = seeded["owner"], seeded["pid"]
|
||||
await repo_bindings_svc.set_binding(owner, REPO, pid)
|
||||
@@ -1200,7 +1200,7 @@ async def test_unjudged_is_what_nobody_read(seeded):
|
||||
audit's verdict is not. Evidence travels; a verdict is never inferred."""
|
||||
from sqlalchemy import update
|
||||
|
||||
from scribe.services.shape_ledger import _RESEMBLE_REASON, unjudged_shapes
|
||||
from grimoire.services.shape_ledger import _RESEMBLE_REASON, unjudged_shapes
|
||||
|
||||
owner, pid, sid = seeded["owner"], seeded["pid"], seeded["snippet"]
|
||||
await classify_shapes(owner, pid, [
|
||||
@@ -1238,9 +1238,9 @@ async def test_a_whole_file_snippet_makes_its_file_row_canonical(seeded):
|
||||
symbol, is the canon for that file row — the claim a whole-file record
|
||||
makes and a definition row never could. It still covers no definition
|
||||
inside the file."""
|
||||
from scribe.services import snippets as snippets_svc
|
||||
from scribe.services.coverage import _recorded_locations
|
||||
from scribe.services.shape_ledger import mark_canonicals
|
||||
from grimoire.services import snippets as snippets_svc
|
||||
from grimoire.services.coverage import _recorded_locations
|
||||
from grimoire.services.shape_ledger import mark_canonicals
|
||||
|
||||
owner, pid = seeded["owner"], seeded["pid"]
|
||||
await sync_repo_shapes(pid, REPO, SHAPES + [
|
||||
|
||||
@@ -19,11 +19,11 @@ import pytest
|
||||
import pytest_asyncio
|
||||
from sqlalchemy import delete, func, select
|
||||
|
||||
from scribe.models import async_session
|
||||
from scribe.models.note import Note
|
||||
from scribe.models.user import User
|
||||
from scribe.services.knowledge import location_matches, location_parts
|
||||
from scribe.services.snippets import (
|
||||
from grimoire.models import async_session
|
||||
from grimoire.models.note import Note
|
||||
from grimoire.models.user import User
|
||||
from grimoire.services.knowledge import location_matches, location_parts
|
||||
from grimoire.services.snippets import (
|
||||
SNIPPET_NOTE_TYPE,
|
||||
backfill_snippet_data,
|
||||
compose_body,
|
||||
@@ -152,7 +152,7 @@ async def test_keyword_arm_applies_the_location_filter(seeded):
|
||||
keyword (ILIKE) arm, which is the second place the SQL clause has to appear."""
|
||||
user_id, ids = seeded
|
||||
with patch(
|
||||
"scribe.services.embeddings.semantic_search_notes",
|
||||
"grimoire.services.embeddings.semantic_search_notes",
|
||||
AsyncMock(return_value=[]),
|
||||
):
|
||||
assert await _ids_for(user_id, q="nested", repo="Grimoire") == {ids["nested"]}
|
||||
@@ -180,7 +180,7 @@ async def test_semantic_arm_applies_the_location_filter(seeded):
|
||||
candidates = [(0.9, n) for n in rows]
|
||||
|
||||
with patch(
|
||||
"scribe.services.embeddings.semantic_search_notes",
|
||||
"grimoire.services.embeddings.semantic_search_notes",
|
||||
AsyncMock(return_value=candidates),
|
||||
):
|
||||
# No keyword can match this, so every hit below came from the stub.
|
||||
|
||||
@@ -14,8 +14,8 @@ import pytest
|
||||
import pytest_asyncio
|
||||
from sqlalchemy.exc import IntegrityError
|
||||
|
||||
from scribe.models import async_session
|
||||
from scribe.models.note import Note
|
||||
from grimoire.models import async_session
|
||||
from grimoire.models.note import Note
|
||||
from tests.helpers import ensure_user
|
||||
|
||||
pytestmark = [pytest.mark.integration, pytest.mark.usefixtures("_dispose_engine")]
|
||||
@@ -81,7 +81,7 @@ async def test_a_kind_can_be_corrected_after_the_fact(owner_id):
|
||||
call succeeded would pass against a route that returns 200 while dropping
|
||||
the field, which is a failure nothing else reports.
|
||||
"""
|
||||
from scribe.services import notes as notes_svc
|
||||
from grimoire.services import notes as notes_svc
|
||||
|
||||
note_id = await _write(owner_id, "work")
|
||||
await notes_svc.update_note(owner_id, note_id, task_kind="spike")
|
||||
@@ -91,7 +91,7 @@ async def test_a_kind_can_be_corrected_after_the_fact(owner_id):
|
||||
|
||||
async def test_an_invalid_kind_is_refused_before_the_database(owner_id):
|
||||
"""A readable ValueError, not a CheckViolationError surfacing as a 500."""
|
||||
from scribe.services import notes as notes_svc
|
||||
from grimoire.services import notes as notes_svc
|
||||
|
||||
note_id = await _write(owner_id, "work")
|
||||
with pytest.raises(ValueError, match="Invalid kind"):
|
||||
|
||||
@@ -14,7 +14,7 @@ makes the trap possible, so it cannot come back by a different route.
|
||||
"""
|
||||
import pytest
|
||||
|
||||
from scribe.services.knowledge import (
|
||||
from grimoire.services.knowledge import (
|
||||
_FACETS,
|
||||
FACET_TYPES,
|
||||
NON_TASK_FACETS,
|
||||
@@ -28,7 +28,7 @@ from tests.helpers import compiled_sql, fake_note, fake_snippet, fake_task
|
||||
def _sql(note_type):
|
||||
from sqlalchemy import select
|
||||
|
||||
from scribe.models.note import Note
|
||||
from grimoire.models.note import Note
|
||||
|
||||
return compiled_sql(_apply_type_filter(select(Note.id), note_type))
|
||||
|
||||
|
||||
@@ -67,7 +67,7 @@ def _snippet_item(nid, title, user_id=1):
|
||||
# ── auto-inject ────────────────────────────────────────────────────────────
|
||||
|
||||
async def _autoinject(hits, exclude_ids, *, rec=None, surf=None):
|
||||
from scribe.services import plugin_context as pc
|
||||
from grimoire.services import plugin_context as pc
|
||||
search = AsyncMock(return_value=hits)
|
||||
with patch.object(pc, "get_autoinject_config", AsyncMock(
|
||||
return_value={"enabled": True, "threshold": 0.55, "top_k": 3})), \
|
||||
@@ -167,7 +167,7 @@ async def test_the_header_explains_the_seen_marker():
|
||||
# ── the write path ─────────────────────────────────────────────────────────
|
||||
|
||||
async def _write_path(hits, exclude_ids, *, here=(), rec=None, sync_exclude=()):
|
||||
from scribe.services import plugin_context as pc
|
||||
from grimoire.services import plugin_context as pc
|
||||
search = AsyncMock(return_value=hits)
|
||||
listing = AsyncMock(side_effect=[(list(here), len(here)), ([], 0)])
|
||||
with patch.object(pc, "get_writepath_config", AsyncMock(return_value=_wp_cfg())), \
|
||||
|
||||
@@ -13,10 +13,10 @@ from unittest.mock import AsyncMock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
from scribe.mcp._context import _user_id_ctx
|
||||
from scribe.mcp.tools.lessons import create_lesson, update_lesson
|
||||
from scribe.services import lessons as lessons_svc
|
||||
from scribe.services.lessons import WHAT_MAX_CHARS, claim_line, require_claim
|
||||
from grimoire.mcp._context import _user_id_ctx
|
||||
from grimoire.mcp.tools.lessons import create_lesson, update_lesson
|
||||
from grimoire.services import lessons as lessons_svc
|
||||
from grimoire.services.lessons import WHAT_MAX_CHARS, claim_line, require_claim
|
||||
|
||||
TRIGGER = "an existing call is about to double as a liveness signal"
|
||||
CLAIM = ("A check-in piggybacked on an existing call inherits every backoff "
|
||||
@@ -80,8 +80,8 @@ async def test_a_lesson_stored_with_a_long_name_can_still_be_edited():
|
||||
)
|
||||
updated = AsyncMock(return_value=note)
|
||||
with patch.object(lessons_svc, "update_lesson", updated), \
|
||||
patch("scribe.mcp.tools.lessons.systems_tools.attach_systems", AsyncMock()), \
|
||||
patch("scribe.mcp.tools.lessons.lesson_rules_svc.attach_lesson_rules", AsyncMock()):
|
||||
patch("grimoire.mcp.tools.lessons.systems_tools.attach_systems", AsyncMock()), \
|
||||
patch("grimoire.mcp.tools.lessons.lesson_rules_svc.attach_lesson_rules", AsyncMock()):
|
||||
await update_lesson(lesson_id=1, when_to_apply=TRIGGER)
|
||||
assert updated.await_args.kwargs["what"] is None
|
||||
|
||||
@@ -90,7 +90,7 @@ async def test_a_lesson_stored_with_a_long_name_can_still_be_edited():
|
||||
def test_the_rest_door_refuses_too(handler):
|
||||
"""The web form writes lessons as well, and a bound one door enforces is
|
||||
a bound the other door walks around."""
|
||||
from scribe.routes import lessons as routes
|
||||
from grimoire.routes import lessons as routes
|
||||
|
||||
src = inspect.getsource(getattr(routes, handler))
|
||||
write = "lessons_svc." + handler.removesuffix("_route") + "("
|
||||
@@ -118,7 +118,7 @@ def test_a_first_sentence_past_the_bound_is_cut_at_a_word():
|
||||
|
||||
|
||||
def test_the_menu_names_a_long_lesson_in_one_line():
|
||||
from scribe.services.plugin_context import _menu_name
|
||||
from grimoire.services.plugin_context import _menu_name
|
||||
|
||||
name = _menu_name(STORY, "lesson", {"what": STORY, "when_to_apply": TRIGGER}, "")
|
||||
assert name.endswith(" …")
|
||||
|
||||
@@ -13,10 +13,10 @@ from unittest.mock import AsyncMock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
from scribe.mcp._context import _user_id_ctx
|
||||
from scribe.mcp.tools import lessons as lesson_tools
|
||||
from scribe.services import lesson_rules as links_svc
|
||||
from scribe.services import lessons as lessons_svc
|
||||
from grimoire.mcp._context import _user_id_ctx
|
||||
from grimoire.mcp.tools import lessons as lesson_tools
|
||||
from grimoire.services import lesson_rules as links_svc
|
||||
from grimoire.services import lessons as lessons_svc
|
||||
|
||||
# Bound before conftest's autouse stub replaces the module attribute.
|
||||
_REAL = links_svc.convergence_for
|
||||
@@ -122,7 +122,7 @@ def _search_over(sim=_sim):
|
||||
def _run(no_rule, sim=_sim):
|
||||
return (
|
||||
patch.object(lessons_svc, "get_lesson", AsyncMock(return_value=_NOTES[1])),
|
||||
patch("scribe.services.embeddings.semantic_search_notes", _search_over(sim)),
|
||||
patch("grimoire.services.embeddings.semantic_search_notes", _search_over(sim)),
|
||||
patch.object(links_svc, "_no_rule_ids",
|
||||
AsyncMock(side_effect=lambda ids: {i for i in ids if i in no_rule})),
|
||||
)
|
||||
|
||||
@@ -26,8 +26,8 @@ regression.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
from scribe.services import lessons as lessons_svc
|
||||
from scribe.services.embeddings import chunk_document, document_title, embedding_text
|
||||
from grimoire.services import lessons as lessons_svc
|
||||
from grimoire.services.embeddings import chunk_document, document_title, embedding_text
|
||||
|
||||
TRIGGER = "a test fails on code you believe is correct"
|
||||
SUBJECT = "Suspect the guard before the code"
|
||||
|
||||
@@ -45,7 +45,7 @@ from __future__ import annotations
|
||||
|
||||
import pytest
|
||||
|
||||
from scribe.services import lessons as lessons_svc
|
||||
from grimoire.services import lessons as lessons_svc
|
||||
from tests.helpers import fake_lesson
|
||||
|
||||
WHAT = "A column written once at create time is a snapshot, not a field"
|
||||
|
||||
@@ -26,9 +26,9 @@ and goes red the day one is added without this value.
|
||||
"""
|
||||
from types import SimpleNamespace
|
||||
|
||||
from scribe.services import knowledge as knowledge_svc
|
||||
from scribe.services import lessons as lessons_svc
|
||||
from scribe.services.embeddings import trigger_title
|
||||
from grimoire.services import knowledge as knowledge_svc
|
||||
from grimoire.services import lessons as lessons_svc
|
||||
from grimoire.services.embeddings import trigger_title
|
||||
|
||||
|
||||
def _note(data=None, body=""):
|
||||
@@ -76,7 +76,7 @@ def test_one_join_builds_every_trigger_title():
|
||||
assert trigger_title(subject, trigger) == expected
|
||||
# The two note kinds reach it through the EMBEDDED title,
|
||||
# from their own mirror key — never a hand-rolled join.
|
||||
from scribe.services.embeddings import document_title
|
||||
from grimoire.services.embeddings import document_title
|
||||
assert document_title(subject, "lesson", {"when_to_apply": trigger}) == expected
|
||||
assert document_title(subject, "snippet", {"when_to_use": trigger}) == expected
|
||||
|
||||
@@ -107,7 +107,7 @@ def test_the_global_kinds_list_names_the_lesson_and_nothing_else():
|
||||
here instead — the one thing a literal costs is that it can drift, and
|
||||
this is what stops it.
|
||||
"""
|
||||
from scribe.services.embeddings import GLOBAL_NOTE_TYPES
|
||||
from grimoire.services.embeddings import GLOBAL_NOTE_TYPES
|
||||
|
||||
assert GLOBAL_NOTE_TYPES == (lessons_svc.LESSON_NOTE_TYPE,)
|
||||
|
||||
|
||||
@@ -43,8 +43,8 @@ import pytest
|
||||
def test_both_doors_share_one_serializer():
|
||||
"""A payload shape spelled once per door answers the two of them
|
||||
differently the first time a field is added."""
|
||||
from scribe.mcp.tools import lessons as mcp_lessons
|
||||
from scribe.services import lessons as lessons_svc
|
||||
from grimoire.mcp.tools import lessons as mcp_lessons
|
||||
from grimoire.services import lessons as lessons_svc
|
||||
|
||||
assert mcp_lessons._to_dict is lessons_svc.lesson_to_dict
|
||||
|
||||
@@ -53,7 +53,7 @@ def test_the_rest_door_composes_nothing_itself():
|
||||
"""Asserted on structure. The document is what ranks, so a door
|
||||
that built its own title would produce lessons that rank differently from
|
||||
the ones the agent writes."""
|
||||
from scribe.routes import lessons as routes
|
||||
from grimoire.routes import lessons as routes
|
||||
|
||||
src = inspect.getsource(routes)
|
||||
# It may CALL the service's composer (the dedup gate needs the document),
|
||||
@@ -67,15 +67,15 @@ def test_the_rest_door_composes_nothing_itself():
|
||||
|
||||
def test_the_door_is_registered():
|
||||
"""A blueprint nobody registers is a file, not a door."""
|
||||
from scribe import app as app_module
|
||||
from grimoire import app as app_module
|
||||
|
||||
src = inspect.getsource(app_module)
|
||||
assert "from scribe.routes.lessons import lessons_bp" in src
|
||||
assert "from grimoire.routes.lessons import lessons_bp" in src
|
||||
assert "app.register_blueprint(lessons_bp)" in src
|
||||
|
||||
|
||||
def test_the_blueprint_is_mounted_where_the_client_looks():
|
||||
from scribe.routes.lessons import lessons_bp
|
||||
from grimoire.routes.lessons import lessons_bp
|
||||
|
||||
assert lessons_bp.url_prefix == "/api/lessons"
|
||||
|
||||
@@ -84,7 +84,7 @@ def test_the_reverse_lookup_is_registered_before_the_id_route():
|
||||
"""Quart matches in registration order. The int converter protects
|
||||
`taught-by` today, but the ordering is what keeps that true if the
|
||||
converter is ever widened — the same care snippets' `/duplicates` takes."""
|
||||
from scribe.routes import lessons as routes
|
||||
from grimoire.routes import lessons as routes
|
||||
|
||||
src = inspect.getsource(routes)
|
||||
assert src.index('"/taught-by/<int:record_id>"') < src.index(
|
||||
@@ -98,7 +98,7 @@ def test_the_reverse_lookup_is_registered_before_the_id_route():
|
||||
def test_create_refuses_a_lesson_with_no_trigger():
|
||||
"""The kind-specific guard. A triggerless lesson saves and never surfaces,
|
||||
and nothing about the stored record shows it."""
|
||||
from scribe.routes import lessons as routes
|
||||
from grimoire.routes import lessons as routes
|
||||
|
||||
src = inspect.getsource(routes.create_lesson_route)
|
||||
assert "when_to_apply is required" in src
|
||||
@@ -111,14 +111,14 @@ def test_create_refuses_a_lesson_with_no_trigger():
|
||||
def test_update_refuses_to_clear_the_trigger():
|
||||
"""The other half. Creating without one is refused; emptying one later
|
||||
would reach the same broken state by a different path."""
|
||||
from scribe.routes import lessons as routes
|
||||
from grimoire.routes import lessons as routes
|
||||
|
||||
src = inspect.getsource(routes.update_lesson_route)
|
||||
assert "cannot be cleared" in src
|
||||
|
||||
|
||||
def test_the_subject_is_required_too():
|
||||
from scribe.routes import lessons as routes
|
||||
from grimoire.routes import lessons as routes
|
||||
|
||||
src = inspect.getsource(routes.create_lesson_route)
|
||||
assert "what is required" in src
|
||||
@@ -133,7 +133,7 @@ def test_the_subject_is_required_too():
|
||||
def test_writes_check_permission_and_act_as_the_owner(handler):
|
||||
"""Resolve share-aware, then write as the owner — otherwise a shared
|
||||
editor is rejected by the owner-scoped service."""
|
||||
from scribe.routes import lessons as routes
|
||||
from grimoire.routes import lessons as routes
|
||||
|
||||
src = inspect.getsource(getattr(routes, handler))
|
||||
assert "can_write_note" in src, f"{handler} does not check write permission"
|
||||
@@ -148,7 +148,7 @@ def test_no_handler_builds_its_own_owner_filter():
|
||||
`Note.user_id == uid` clause instead of going through the service and the
|
||||
access helpers. Passing `user_id=uid` INTO a service is the correct call
|
||||
and is not what this looks for."""
|
||||
from scribe.routes import lessons as routes
|
||||
from grimoire.routes import lessons as routes
|
||||
|
||||
src = inspect.getsource(routes)
|
||||
assert "Note.user_id" not in src
|
||||
@@ -159,7 +159,7 @@ def test_no_handler_builds_its_own_owner_filter():
|
||||
|
||||
def test_delete_trashes_recoverably():
|
||||
"""Every kind's delete is a trash, and the batch id is what restores it."""
|
||||
from scribe.routes import lessons as routes
|
||||
from grimoire.routes import lessons as routes
|
||||
|
||||
src = inspect.getsource(routes.delete_lesson_route)
|
||||
assert "trash_svc.delete" in src
|
||||
@@ -172,7 +172,7 @@ def test_delete_trashes_recoverably():
|
||||
def test_the_reverse_lookup_reads_the_indexed_mirror():
|
||||
"""Not by scanning bodies: `data[taught_by]` is JSONB with a GIN index
|
||||
(0070), which is the whole reason step 4 put the list there."""
|
||||
from scribe.services import lessons as lessons_svc
|
||||
from grimoire.services import lessons as lessons_svc
|
||||
|
||||
src = inspect.getsource(lessons_svc.lessons_taught_by)
|
||||
assert "path_exists" in src
|
||||
@@ -182,7 +182,7 @@ def test_the_reverse_lookup_reads_the_indexed_mirror():
|
||||
def test_the_reverse_lookup_is_share_aware():
|
||||
"""It renders beside a record the caller can already see, so a lesson
|
||||
someone shared with them belongs in the list exactly as their own does."""
|
||||
from scribe.services import lessons as lessons_svc
|
||||
from grimoire.services import lessons as lessons_svc
|
||||
|
||||
src = inspect.getsource(lessons_svc.lessons_taught_by)
|
||||
assert "readable_notes_clause" in src
|
||||
@@ -192,7 +192,7 @@ def test_the_reverse_lookup_is_share_aware():
|
||||
def test_the_reverse_lookup_refuses_a_nonsense_id_rather_than_interpolating():
|
||||
"""The jsonpath is built by formatting, so the id has to be an int before
|
||||
it gets near the expression."""
|
||||
from scribe.services import lessons as lessons_svc
|
||||
from grimoire.services import lessons as lessons_svc
|
||||
|
||||
src = inspect.getsource(lessons_svc.lessons_taught_by)
|
||||
assert "int(record_id)" in src
|
||||
@@ -200,7 +200,7 @@ def test_the_reverse_lookup_refuses_a_nonsense_id_rather_than_interpolating():
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_a_bad_id_returns_nothing_and_raises_nothing():
|
||||
from scribe.services.lessons import lessons_taught_by
|
||||
from grimoire.services.lessons import lessons_taught_by
|
||||
|
||||
assert await lessons_taught_by(1, 0) == []
|
||||
assert await lessons_taught_by(1, -3) == []
|
||||
@@ -214,7 +214,7 @@ def test_the_payload_reads_back_the_composed_fields():
|
||||
"""A caller that wrote `when_to_apply` reads `when_to_apply` back, not a
|
||||
body it has to parse."""
|
||||
from tests.helpers import fake_lesson
|
||||
from scribe.services.lessons import compose_body, lesson_to_dict
|
||||
from grimoire.services.lessons import compose_body, lesson_to_dict
|
||||
|
||||
what = "Read the job log before waiting longer"
|
||||
trigger = "a CI run has sat in_progress longer than its suite takes"
|
||||
@@ -239,7 +239,7 @@ def test_the_payload_reads_back_the_composed_fields():
|
||||
|
||||
def test_the_rest_guards_can_fail():
|
||||
"""Shown turning red once."""
|
||||
from scribe.routes import lessons as routes
|
||||
from grimoire.routes import lessons as routes
|
||||
|
||||
src = inspect.getsource(routes)
|
||||
assert "a phrase that is definitely not in this module" not in src
|
||||
|
||||
@@ -16,12 +16,12 @@ from unittest.mock import AsyncMock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
from scribe.mcp._context import _user_id_ctx
|
||||
from scribe.mcp.tools import lessons as lesson_tools
|
||||
from scribe.models.lesson_rule_link import LINK_STATES
|
||||
from scribe.services import backup
|
||||
from scribe.services import lesson_rules as links_svc
|
||||
from scribe.services import lessons as lessons_svc
|
||||
from grimoire.mcp._context import _user_id_ctx
|
||||
from grimoire.mcp.tools import lessons as lesson_tools
|
||||
from grimoire.models.lesson_rule_link import LINK_STATES
|
||||
from grimoire.services import backup
|
||||
from grimoire.services import lesson_rules as links_svc
|
||||
from grimoire.services import lessons as lessons_svc
|
||||
|
||||
TRIGGER = "a CI run is still in_progress far past its usual time"
|
||||
|
||||
@@ -39,9 +39,9 @@ def _stub_note(**kw):
|
||||
def _create_patches(created):
|
||||
return (
|
||||
patch.object(lessons_svc, "create_lesson", created),
|
||||
patch("scribe.mcp.tools.lessons.dedup_svc.find_duplicate_note",
|
||||
patch("grimoire.mcp.tools.lessons.dedup_svc.find_duplicate_note",
|
||||
AsyncMock(return_value=None)),
|
||||
patch("scribe.mcp.tools.lessons.systems_tools.attach_systems", AsyncMock()),
|
||||
patch("grimoire.mcp.tools.lessons.systems_tools.attach_systems", AsyncMock()),
|
||||
)
|
||||
|
||||
|
||||
@@ -237,8 +237,8 @@ async def test_list_unjudged_is_a_listing_and_routes_to_its_own_query():
|
||||
_user_id_ctx.set(7)
|
||||
listed = AsyncMock(return_value=([], 0))
|
||||
with patch.object(links_svc, "list_unjudged", listed), \
|
||||
patch("scribe.mcp.tools.lessons.access_svc.label_shared_items", AsyncMock(return_value=[])), \
|
||||
patch("scribe.mcp.tools.lessons.attach_usage", AsyncMock()):
|
||||
patch("grimoire.mcp.tools.lessons.access_svc.label_shared_items", AsyncMock(return_value=[])), \
|
||||
patch("grimoire.mcp.tools.lessons.attach_usage", AsyncMock()):
|
||||
out = await lesson_tools.list_lessons(unjudged=True, tag="ci", project_id=2)
|
||||
assert out == {"lessons": [], "total": 0}
|
||||
assert listed.await_args.kwargs["tag"] == "ci"
|
||||
|
||||
@@ -20,9 +20,9 @@ import inspect
|
||||
import re
|
||||
from pathlib import Path
|
||||
|
||||
from scribe.models.lesson_rule_link import LINK_STATES
|
||||
from scribe.services import lesson_rules as links_svc
|
||||
from scribe.services.access import PERMISSION_RANK
|
||||
from grimoire.models.lesson_rule_link import LINK_STATES
|
||||
from grimoire.services import lesson_rules as links_svc
|
||||
from grimoire.services.access import PERMISSION_RANK
|
||||
|
||||
FRONTEND = Path(__file__).resolve().parents[1] / "frontend" / "src"
|
||||
|
||||
@@ -133,7 +133,7 @@ def _payload_fn(view: str) -> str:
|
||||
def test_the_editor_sends_the_answer_under_the_names_both_routes_read():
|
||||
"""The contract check: a field the form sends under another name is
|
||||
dropped without an error, and the lesson saves unjudged."""
|
||||
from scribe.routes import lessons as routes
|
||||
from grimoire.routes import lessons as routes
|
||||
|
||||
body = _payload_fn(_read("views/LessonEditorView.vue"))
|
||||
assert "rule_ids:" in body and "no_rule:" in body
|
||||
@@ -157,7 +157,7 @@ def test_leaving_it_open_after_linking_clears_the_links_rather_than_nothing():
|
||||
|
||||
|
||||
def test_the_candidates_route_is_where_the_client_asks_and_above_the_id_route():
|
||||
from scribe.routes import lessons as routes
|
||||
from grimoire.routes import lessons as routes
|
||||
|
||||
src = inspect.getsource(routes)
|
||||
assert src.index('"/rule-candidates"') < src.index('"/<int:lesson_id>"')
|
||||
@@ -168,7 +168,7 @@ def test_the_candidates_route_keeps_unavailable_apart_from_none():
|
||||
"""The service returns None when the search could not run. The route has
|
||||
to pass that through as null, not coerce it to an empty list, because
|
||||
"nothing resembles it" is a claim the editor shows to the writer."""
|
||||
from scribe.routes import lessons as routes
|
||||
from grimoire.routes import lessons as routes
|
||||
|
||||
src = inspect.getsource(routes.lesson_rule_candidates_route)
|
||||
assert 'jsonify({"candidates": candidates})' in src
|
||||
|
||||
@@ -12,7 +12,7 @@ from __future__ import annotations
|
||||
import inspect
|
||||
from datetime import datetime, timedelta, timezone
|
||||
|
||||
from scribe.services import lesson_rules as links_svc
|
||||
from grimoire.services import lesson_rules as links_svc
|
||||
|
||||
NOW = datetime(2026, 10, 1, 12, 0, tzinfo=timezone.utc)
|
||||
|
||||
@@ -26,7 +26,7 @@ def test_the_same_ask_reworded_trivially_is_one_situation():
|
||||
def test_a_file_is_one_situation_however_its_code_differs():
|
||||
"""The write arm keys on the path, so a run of edits to one file — each
|
||||
with different code — is one situation, not one per edit."""
|
||||
path = "src/scribe/services/lesson_rules.py"
|
||||
path = "src/grimoire/services/lesson_rules.py"
|
||||
assert links_svc.situation_key("w", path) == links_svc.situation_key("w", path)
|
||||
assert links_svc.situation_key("w", path) != links_svc.situation_key("w", "src/other.py")
|
||||
|
||||
@@ -94,8 +94,8 @@ def test_both_hook_doors_count_the_pair_on_their_own_arm():
|
||||
"""The two responses that carry lessons and rules together. A door that
|
||||
renders both and does not record the pair is the miss this step exists
|
||||
to close, and it would produce no error anywhere — only an empty table."""
|
||||
from scribe.routes import plugin as plugin_routes
|
||||
from scribe.services import plugin_context
|
||||
from grimoire.routes import plugin as plugin_routes
|
||||
from grimoire.services import plugin_context
|
||||
|
||||
prompt_door = inspect.getsource(plugin_routes.autoinject_retrieve)
|
||||
assert "co_surfaced(" in prompt_door and 'arm="p"' in prompt_door
|
||||
|
||||
@@ -20,8 +20,8 @@ that these tests are shaped to catch rather than to describe:
|
||||
from unittest.mock import AsyncMock, MagicMock, patch
|
||||
|
||||
import pytest
|
||||
from scribe.services import plugin_context as pc
|
||||
from scribe.services.lessons import LESSON_NOTE_TYPE
|
||||
from grimoire.services import plugin_context as pc
|
||||
from grimoire.services.lessons import LESSON_NOTE_TYPE
|
||||
from tests.helpers import fake_lesson, fake_note, writepath_cfg
|
||||
|
||||
pytestmark = pytest.mark.usefixtures("_no_supersession")
|
||||
|
||||
@@ -21,9 +21,9 @@ from unittest.mock import AsyncMock, MagicMock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
from scribe.mcp.tools import lessons as lesson_tools
|
||||
from scribe.services import note_usage
|
||||
from scribe.services.note_usage import empty_usage
|
||||
from grimoire.mcp.tools import lessons as lesson_tools
|
||||
from grimoire.services import note_usage
|
||||
from grimoire.services.note_usage import empty_usage
|
||||
|
||||
|
||||
def _rows():
|
||||
@@ -128,7 +128,7 @@ async def test_get_lesson_reads_the_count_before_recording_its_own_pull():
|
||||
|
||||
|
||||
def _route_source(name: str) -> str:
|
||||
from scribe.routes import lessons as routes
|
||||
from grimoire.routes import lessons as routes
|
||||
|
||||
src = inspect.getsource(routes)
|
||||
start = src.index(f"async def {name}(")
|
||||
|
||||
@@ -22,9 +22,9 @@ from unittest.mock import AsyncMock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
from scribe.mcp._context import _user_id_ctx
|
||||
from scribe.mcp.tools.lessons import create_lesson, update_lesson
|
||||
from scribe.services import lessons as lessons_svc
|
||||
from grimoire.mcp._context import _user_id_ctx
|
||||
from grimoire.mcp.tools.lessons import create_lesson, update_lesson
|
||||
from grimoire.services import lessons as lessons_svc
|
||||
|
||||
TRIGGER = "a test fails on code you believe is correct"
|
||||
SUBJECT = "Suspect the guard before the code"
|
||||
@@ -73,9 +73,9 @@ async def test_a_lesson_keeps_every_incident_that_taught_it():
|
||||
_user_id_ctx.set(7)
|
||||
created = AsyncMock(return_value=_stub_note())
|
||||
with patch.object(lessons_svc, "create_lesson", created), \
|
||||
patch("scribe.mcp.tools.lessons.dedup_svc.find_duplicate_note",
|
||||
patch("grimoire.mcp.tools.lessons.dedup_svc.find_duplicate_note",
|
||||
AsyncMock(return_value=None)), \
|
||||
patch("scribe.mcp.tools.lessons.systems_tools.attach_systems", AsyncMock()):
|
||||
patch("grimoire.mcp.tools.lessons.systems_tools.attach_systems", AsyncMock()):
|
||||
await create_lesson(
|
||||
what=SUBJECT, when_to_apply=TRIGGER, learned_from=[11, 22, 33],
|
||||
)
|
||||
@@ -101,7 +101,7 @@ async def test_the_duplicate_gate_runs_before_anything_is_created():
|
||||
hit = SimpleNamespace(id=99, title="already recorded", similarity=0.97,
|
||||
reason="semantic")
|
||||
created = AsyncMock()
|
||||
with patch("scribe.mcp.tools.lessons.dedup_svc.find_duplicate_note",
|
||||
with patch("grimoire.mcp.tools.lessons.dedup_svc.find_duplicate_note",
|
||||
AsyncMock(return_value=hit)), \
|
||||
patch.object(lessons_svc, "create_lesson", created):
|
||||
out = await create_lesson(what=SUBJECT, when_to_apply=TRIGGER)
|
||||
@@ -122,10 +122,10 @@ async def test_the_gate_compares_the_composed_document_not_the_raw_fields():
|
||||
document, and would judge two lessons alike that rank nothing alike."""
|
||||
_user_id_ctx.set(7)
|
||||
gate = AsyncMock(return_value=None)
|
||||
with patch("scribe.mcp.tools.lessons.dedup_svc.find_duplicate_note", gate), \
|
||||
with patch("grimoire.mcp.tools.lessons.dedup_svc.find_duplicate_note", gate), \
|
||||
patch.object(lessons_svc, "create_lesson",
|
||||
AsyncMock(return_value=_stub_note())), \
|
||||
patch("scribe.mcp.tools.lessons.systems_tools.attach_systems", AsyncMock()):
|
||||
patch("grimoire.mcp.tools.lessons.systems_tools.attach_systems", AsyncMock()):
|
||||
await create_lesson(what=SUBJECT, when_to_apply=TRIGGER, insight="Look.")
|
||||
|
||||
title, body = gate.await_args.args[1], gate.await_args.args[2]
|
||||
@@ -140,7 +140,7 @@ def test_a_lesson_is_judged_at_the_trigger_dominated_bar():
|
||||
mostly prose ABOUT the thing. A lesson's document is that shape, so the
|
||||
bar sits above that band — otherwise two different lessons about one area
|
||||
block each other, which is the failure step 4 asked to check for."""
|
||||
from scribe.services.dedup import (
|
||||
from grimoire.services.dedup import (
|
||||
_LESSON_SEMANTIC_THRESHOLD,
|
||||
_SEMANTIC_THRESHOLD,
|
||||
_semantic_threshold,
|
||||
@@ -154,7 +154,7 @@ def test_a_lesson_is_judged_at_the_trigger_dominated_bar():
|
||||
assert _LESSON_SEMANTIC_THRESHOLD > _SEMANTIC_THRESHOLD
|
||||
# The carve-out is per kind, not a loosening of the gate: a lesson's bar is
|
||||
# its own, apart from the note's copy band.
|
||||
from scribe.services.dedup import _NOTE_COPY_THRESHOLD
|
||||
from grimoire.services.dedup import _NOTE_COPY_THRESHOLD
|
||||
|
||||
assert _semantic_threshold("note") == _NOTE_COPY_THRESHOLD
|
||||
assert _NOTE_COPY_THRESHOLD != _LESSON_SEMANTIC_THRESHOLD
|
||||
@@ -175,7 +175,7 @@ async def test_an_update_recomposes_both_halves_of_the_document():
|
||||
)
|
||||
updated = AsyncMock(return_value=_stub_note())
|
||||
with patch.object(lessons_svc, "get_lesson", AsyncMock(return_value=stored)), \
|
||||
patch("scribe.services.notes.update_note", updated):
|
||||
patch("grimoire.services.notes.update_note", updated):
|
||||
await lessons_svc.update_lesson(7, 1, when_to_apply="a guard goes red")
|
||||
|
||||
fields = updated.await_args.kwargs
|
||||
@@ -198,7 +198,7 @@ async def test_an_update_does_not_stack_the_composed_lines():
|
||||
)
|
||||
updated = AsyncMock(return_value=_stub_note())
|
||||
with patch.object(lessons_svc, "get_lesson", AsyncMock(return_value=stored)), \
|
||||
patch("scribe.services.notes.update_note", updated):
|
||||
patch("grimoire.services.notes.update_note", updated):
|
||||
await lessons_svc.update_lesson(7, 1, what="Suspect the guard")
|
||||
|
||||
body = updated.await_args.kwargs["body"]
|
||||
|
||||
@@ -13,10 +13,10 @@ from unittest.mock import AsyncMock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
from scribe.mcp.tools.notes import list_notes
|
||||
from scribe.mcp.tools.systems import list_system_records
|
||||
from scribe.mcp.tools.tasks import list_tasks
|
||||
from scribe.services.notes import brief_row
|
||||
from grimoire.mcp.tools.notes import list_notes
|
||||
from grimoire.mcp.tools.systems import list_system_records
|
||||
from grimoire.mcp.tools.tasks import list_tasks
|
||||
from grimoire.services.notes import brief_row
|
||||
|
||||
pytestmark = pytest.mark.usefixtures("_bind_user")
|
||||
|
||||
@@ -61,9 +61,9 @@ async def test_a_full_page_of_long_tasks_fits_inline():
|
||||
"""The ceiling. 100 rows (list_tasks' own limit cap) of ~5k-character step
|
||||
plans: as to_dict() rows this was over half a million characters."""
|
||||
rows = [_row(i) for i in range(100)]
|
||||
with patch("scribe.mcp.tools.tasks.notes_svc.list_notes",
|
||||
with patch("grimoire.mcp.tools.tasks.notes_svc.list_notes",
|
||||
AsyncMock(return_value=(rows, 100))), \
|
||||
patch("scribe.mcp.tools.tasks.milestones_svc.titles_for",
|
||||
patch("grimoire.mcp.tools.tasks.milestones_svc.titles_for",
|
||||
AsyncMock(return_value={415: "An existing plan is found"})) as titles:
|
||||
out = await list_tasks(project_id=2, limit=100)
|
||||
size = len(json.dumps(out))
|
||||
@@ -75,7 +75,7 @@ async def test_a_full_page_of_long_tasks_fits_inline():
|
||||
@pytest.mark.asyncio
|
||||
async def test_list_notes_rows_leave_the_body_to_get_note():
|
||||
rows = [_row(i, is_task=False) for i in range(3)]
|
||||
with patch("scribe.mcp.tools.notes.notes_svc.list_notes",
|
||||
with patch("grimoire.mcp.tools.notes.notes_svc.list_notes",
|
||||
AsyncMock(return_value=(rows, 3))):
|
||||
out = await list_notes(project_id=2)
|
||||
assert all("body" not in r for r in out["notes"])
|
||||
@@ -84,9 +84,9 @@ async def test_list_notes_rows_leave_the_body_to_get_note():
|
||||
@pytest.mark.asyncio
|
||||
async def test_list_system_records_rows_leave_the_body_to_the_getters():
|
||||
rows = [_row(1), _row(2, is_task=False)]
|
||||
with patch("scribe.mcp.tools.systems.systems_svc.list_records_for_system",
|
||||
with patch("grimoire.mcp.tools.systems.systems_svc.list_records_for_system",
|
||||
AsyncMock(return_value=rows)), \
|
||||
patch("scribe.mcp.tools.systems.milestones_svc.titles_for",
|
||||
patch("grimoire.mcp.tools.systems.milestones_svc.titles_for",
|
||||
AsyncMock(return_value={415: "Plan"})):
|
||||
out = await list_system_records(system_id=3)
|
||||
assert [r["id"] for r in out["records"]] == [1, 2]
|
||||
|
||||
@@ -14,8 +14,8 @@ from unittest.mock import AsyncMock, MagicMock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
from scribe.services import embeddings as emb
|
||||
from scribe.services.text import (
|
||||
from grimoire.services import embeddings as emb
|
||||
from grimoire.services.text import (
|
||||
BODY_OPENING,
|
||||
MATCHED_PASSAGE,
|
||||
excerpt_fields,
|
||||
|
||||
+10
-10
@@ -3,7 +3,7 @@ from unittest.mock import AsyncMock, MagicMock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
from scribe.mcp.auth import resolve_bearer
|
||||
from grimoire.mcp.auth import resolve_bearer
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@@ -22,7 +22,7 @@ async def test_resolve_bearer_malformed_header_returns_none():
|
||||
@pytest.mark.asyncio
|
||||
async def test_resolve_bearer_unknown_token_returns_none():
|
||||
with patch(
|
||||
"scribe.mcp.auth.lookup_key",
|
||||
"grimoire.mcp.auth.lookup_key",
|
||||
AsyncMock(return_value=None),
|
||||
):
|
||||
assert await resolve_bearer("Bearer fmcp_doesnotexist") is None
|
||||
@@ -34,7 +34,7 @@ async def test_resolve_bearer_valid_token_returns_user_id():
|
||||
fake_key.user_id = 42
|
||||
fake_key.scope = "write"
|
||||
with patch(
|
||||
"scribe.mcp.auth.lookup_key",
|
||||
"grimoire.mcp.auth.lookup_key",
|
||||
AsyncMock(return_value=fake_key),
|
||||
):
|
||||
uid, scope = await resolve_bearer("Bearer fmcp_validkey")
|
||||
@@ -48,7 +48,7 @@ async def test_resolve_bearer_calls_lookup_with_stripped_token():
|
||||
fake_key.user_id = 1
|
||||
fake_key.scope = "write"
|
||||
mock_lookup = AsyncMock(return_value=fake_key)
|
||||
with patch("scribe.mcp.auth.lookup_key", mock_lookup):
|
||||
with patch("grimoire.mcp.auth.lookup_key", mock_lookup):
|
||||
await resolve_bearer("Bearer fmcp_abc123 ")
|
||||
mock_lookup.assert_awaited_once_with("fmcp_abc123")
|
||||
|
||||
@@ -60,13 +60,13 @@ async def test_resolve_bearer_returns_user_id_and_scope():
|
||||
fake_key = MagicMock()
|
||||
fake_key.user_id = 9
|
||||
fake_key.scope = "read"
|
||||
with patch("scribe.mcp.auth.lookup_key", AsyncMock(return_value=fake_key)):
|
||||
with patch("grimoire.mcp.auth.lookup_key", AsyncMock(return_value=fake_key)):
|
||||
assert await resolve_bearer("Bearer fmcp_x") == (9, "read")
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_resolve_bearer_none_for_invalid():
|
||||
with patch("scribe.mcp.auth.lookup_key", AsyncMock(return_value=None)):
|
||||
with patch("grimoire.mcp.auth.lookup_key", AsyncMock(return_value=None)):
|
||||
assert await resolve_bearer("Bearer nope") is None
|
||||
assert await resolve_bearer(None) is None
|
||||
|
||||
@@ -75,7 +75,7 @@ async def test_resolve_bearer_none_for_invalid():
|
||||
|
||||
def test_body_calls_write_tool_classifies_correctly():
|
||||
import json
|
||||
from scribe.mcp.server import _body_calls_write_tool
|
||||
from grimoire.mcp.server import _body_calls_write_tool
|
||||
|
||||
def call(name):
|
||||
return json.dumps({"jsonrpc": "2.0", "id": 1, "method": "tools/call",
|
||||
@@ -99,7 +99,7 @@ def test_body_calls_write_tool_classifies_correctly():
|
||||
def _registered_tool_names() -> set[str]:
|
||||
"""What the server actually mounts — not a glob of function names, which
|
||||
would count helpers and miss anything registered another way."""
|
||||
from scribe.mcp.server import build_mcp_server
|
||||
from grimoire.mcp.server import build_mcp_server
|
||||
|
||||
return {tool.name for tool in build_mcp_server()._tool_manager.list_tools()}
|
||||
|
||||
@@ -118,7 +118,7 @@ def test_every_registered_tool_is_classified_exactly_once():
|
||||
not look like reads (`rules_due_for_verification`, `rule_history`). So the
|
||||
candidates are EVERY registered tool.
|
||||
"""
|
||||
from scribe.mcp.server import (
|
||||
from grimoire.mcp.server import (
|
||||
_DELIBERATELY_WRITE_SCOPED, _READ_ONLY_TOOLS, _WRITE_TOOLS,
|
||||
)
|
||||
|
||||
@@ -156,7 +156,7 @@ def test_every_registered_tool_is_classified_exactly_once():
|
||||
def test_the_completeness_check_can_fail():
|
||||
"""A guard that cannot fail is indistinguishable from one that is broken
|
||||
Drop a real tool from its set and the check must notice."""
|
||||
from scribe.mcp.server import _READ_ONLY_TOOLS, _WRITE_TOOLS
|
||||
from grimoire.mcp.server import _READ_ONLY_TOOLS, _WRITE_TOOLS
|
||||
|
||||
registered = _registered_tool_names()
|
||||
assert "rule_history" in _READ_ONLY_TOOLS
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"""Tests for MCP per-request user_id context."""
|
||||
import pytest
|
||||
|
||||
from scribe.mcp._context import current_user_id, _user_id_ctx
|
||||
from grimoire.mcp._context import current_user_id, _user_id_ctx
|
||||
|
||||
|
||||
def test_current_user_id_raises_when_unset():
|
||||
|
||||
@@ -15,7 +15,7 @@ from unittest.mock import AsyncMock, MagicMock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
from scribe.app import create_app
|
||||
from grimoire.app import create_app
|
||||
|
||||
|
||||
async def _send_request(
|
||||
@@ -73,7 +73,7 @@ async def test_mcp_endpoint_unauthenticated_returns_401():
|
||||
async def test_mcp_endpoint_invalid_token_returns_401():
|
||||
app = create_app()
|
||||
with patch(
|
||||
"scribe.mcp.auth.lookup_key",
|
||||
"grimoire.mcp.auth.lookup_key",
|
||||
AsyncMock(return_value=None),
|
||||
):
|
||||
status, _ = await _send_request(
|
||||
@@ -107,7 +107,7 @@ async def test_mcp_endpoint_valid_token_passes_auth():
|
||||
}).encode()
|
||||
async with app.mcp_instance.session_manager.run():
|
||||
with patch(
|
||||
"scribe.mcp.auth.lookup_key",
|
||||
"grimoire.mcp.auth.lookup_key",
|
||||
AsyncMock(return_value=fake_key),
|
||||
):
|
||||
status, _ = await _send_request(
|
||||
|
||||
@@ -28,7 +28,7 @@ so any future `list_*` could opt itself out by accident.
|
||||
import ast
|
||||
import pathlib
|
||||
|
||||
TOOLS_DIR = pathlib.Path(__file__).resolve().parents[1] / "src" / "scribe" / "mcp" / "tools"
|
||||
TOOLS_DIR = pathlib.Path(__file__).resolve().parents[1] / "src" / "grimoire" / "mcp" / "tools"
|
||||
|
||||
# `limit` here caps a RANKED top-N, not a page into a corpus, so there is no
|
||||
# "rest" to ask for — the 51st most-used tag is not what the caller wanted and
|
||||
|
||||
@@ -13,7 +13,7 @@ one is close. An error the caller sees once beats data half-written forever.
|
||||
import pytest
|
||||
from mcp.server.mcpserver.exceptions import ToolError, UnexpectedToolError
|
||||
|
||||
from scribe.mcp.server import StrictArgsMCPServer
|
||||
from grimoire.mcp.server import StrictArgsMCPServer
|
||||
|
||||
|
||||
def _echo_server() -> StrictArgsMCPServer:
|
||||
@@ -103,7 +103,7 @@ async def test_a_crash_stays_masked():
|
||||
async def test_a_real_tools_refusal_reaches_the_caller():
|
||||
"""Against the real server, on a refusal that needs no database: the
|
||||
message judge_menu writes for an empty verdict list."""
|
||||
from scribe.mcp.server import build_mcp_server
|
||||
from grimoire.mcp.server import build_mcp_server
|
||||
|
||||
with pytest.raises(ToolError) as exc:
|
||||
await build_mcp_server().call_tool("judge_menu", {"log_id": 1, "verdicts": []})
|
||||
@@ -114,7 +114,7 @@ async def test_the_real_server_refuses_content_on_create_note():
|
||||
"""Against the REAL server: `content=` on create_note
|
||||
must raise before dispatch — naming the bad argument and listing `body`
|
||||
among the accepted ones — instead of creating a body-less note."""
|
||||
from scribe.mcp.server import build_mcp_server
|
||||
from grimoire.mcp.server import build_mcp_server
|
||||
|
||||
mcp = build_mcp_server()
|
||||
assert isinstance(mcp, StrictArgsMCPServer) # the guard is actually mounted
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -9,7 +9,7 @@ from unittest.mock import AsyncMock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
from scribe.services.design_systems import DesignSystemCycle
|
||||
from grimoire.services.design_systems import DesignSystemCycle
|
||||
from tests.helpers import design_token_stub, fake_record
|
||||
|
||||
|
||||
@@ -31,27 +31,27 @@ async def test_creating_without_a_parent_passes_none_not_zero():
|
||||
"""0 is the "omitted" sentinel, and it must not reach the service as a
|
||||
system id — there is no system 0, so the create would fail an ACL check for
|
||||
a record that cannot exist."""
|
||||
with patch("scribe.mcp.tools.design_systems.ds_svc") as svc:
|
||||
with patch("grimoire.mcp.tools.design_systems.ds_svc") as svc:
|
||||
svc.create_design_system = AsyncMock(return_value=_fake_design_system())
|
||||
from scribe.mcp.tools.design_systems import create_design_system
|
||||
from grimoire.mcp.tools.design_systems import create_design_system
|
||||
await create_design_system(title="FabledSword")
|
||||
assert svc.create_design_system.await_args.kwargs["parent_id"] is None
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_creating_with_a_parent_passes_it_through():
|
||||
with patch("scribe.mcp.tools.design_systems.ds_svc") as svc:
|
||||
with patch("grimoire.mcp.tools.design_systems.ds_svc") as svc:
|
||||
svc.create_design_system = AsyncMock(return_value=_fake_design_system())
|
||||
from scribe.mcp.tools.design_systems import create_design_system
|
||||
from grimoire.mcp.tools.design_systems import create_design_system
|
||||
await create_design_system(title="Grimoire", parent_id=4)
|
||||
assert svc.create_design_system.await_args.kwargs["parent_id"] == 4
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_create_raises_when_the_parent_is_not_writable():
|
||||
with patch("scribe.mcp.tools.design_systems.ds_svc") as svc:
|
||||
with patch("grimoire.mcp.tools.design_systems.ds_svc") as svc:
|
||||
svc.create_design_system = AsyncMock(return_value=None)
|
||||
from scribe.mcp.tools.design_systems import create_design_system
|
||||
from grimoire.mcp.tools.design_systems import create_design_system
|
||||
with pytest.raises(ValueError):
|
||||
await create_design_system(title="Grimoire", parent_id=4)
|
||||
|
||||
@@ -61,9 +61,9 @@ async def test_create_raises_when_the_parent_is_not_writable():
|
||||
@pytest.mark.asyncio
|
||||
async def test_update_with_parent_id_zero_leaves_the_parent_alone():
|
||||
"""The common case — renaming a system must not silently re-root it."""
|
||||
with patch("scribe.mcp.tools.design_systems.ds_svc") as svc:
|
||||
with patch("grimoire.mcp.tools.design_systems.ds_svc") as svc:
|
||||
svc.update_design_system = AsyncMock(return_value=_fake_design_system())
|
||||
from scribe.mcp.tools.design_systems import update_design_system
|
||||
from grimoire.mcp.tools.design_systems import update_design_system
|
||||
await update_design_system(design_system_id=1, title="Renamed")
|
||||
fields = svc.update_design_system.await_args.kwargs
|
||||
assert "parent_id" not in fields
|
||||
@@ -75,18 +75,18 @@ async def test_update_with_parent_id_minus_one_clears_it():
|
||||
"""-1 means "make this a family system". It has to arrive at the service as
|
||||
None, which is the value the service reads as "become a root" — where
|
||||
omitting the key means "leave alone"."""
|
||||
with patch("scribe.mcp.tools.design_systems.ds_svc") as svc:
|
||||
with patch("grimoire.mcp.tools.design_systems.ds_svc") as svc:
|
||||
svc.update_design_system = AsyncMock(return_value=_fake_design_system())
|
||||
from scribe.mcp.tools.design_systems import update_design_system
|
||||
from grimoire.mcp.tools.design_systems import update_design_system
|
||||
await update_design_system(design_system_id=1, parent_id=-1)
|
||||
assert svc.update_design_system.await_args.kwargs["parent_id"] is None
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_update_with_a_positive_parent_id_sets_it():
|
||||
with patch("scribe.mcp.tools.design_systems.ds_svc") as svc:
|
||||
with patch("grimoire.mcp.tools.design_systems.ds_svc") as svc:
|
||||
svc.update_design_system = AsyncMock(return_value=_fake_design_system())
|
||||
from scribe.mcp.tools.design_systems import update_design_system
|
||||
from grimoire.mcp.tools.design_systems import update_design_system
|
||||
await update_design_system(design_system_id=1, parent_id=4)
|
||||
assert svc.update_design_system.await_args.kwargs["parent_id"] == 4
|
||||
|
||||
@@ -95,11 +95,11 @@ async def test_update_with_a_positive_parent_id_sets_it():
|
||||
async def test_a_cycle_surfaces_as_a_usable_error_not_a_not_found():
|
||||
"""The service raises so this layer can keep the two apart. An agent told
|
||||
"not found" would retry the same call; one told what the loop is can fix it."""
|
||||
with patch("scribe.mcp.tools.design_systems.ds_svc") as svc:
|
||||
with patch("grimoire.mcp.tools.design_systems.ds_svc") as svc:
|
||||
svc.update_design_system = AsyncMock(
|
||||
side_effect=DesignSystemCycle("2 already inherits from 1")
|
||||
)
|
||||
from scribe.mcp.tools.design_systems import update_design_system
|
||||
from grimoire.mcp.tools.design_systems import update_design_system
|
||||
with pytest.raises(ValueError, match="already inherits"):
|
||||
await update_design_system(design_system_id=1, parent_id=2)
|
||||
|
||||
@@ -110,9 +110,9 @@ async def test_a_cycle_surfaces_as_a_usable_error_not_a_not_found():
|
||||
async def test_update_token_treats_order_index_minus_one_as_unchanged():
|
||||
"""0 is a VALID order_index, so it cannot double as the omitted sentinel —
|
||||
the same reason the systems tools use -1."""
|
||||
with patch("scribe.mcp.tools.design_systems.ds_svc") as svc:
|
||||
with patch("grimoire.mcp.tools.design_systems.ds_svc") as svc:
|
||||
svc.update_token = AsyncMock(return_value=_fake_token())
|
||||
from scribe.mcp.tools.design_systems import update_design_token
|
||||
from grimoire.mcp.tools.design_systems import update_design_token
|
||||
await update_design_token(token_id=9, purpose="page bg")
|
||||
fields = svc.update_token.await_args.kwargs
|
||||
assert "order_index" not in fields
|
||||
@@ -121,9 +121,9 @@ async def test_update_token_treats_order_index_minus_one_as_unchanged():
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_update_token_accepts_order_index_zero():
|
||||
with patch("scribe.mcp.tools.design_systems.ds_svc") as svc:
|
||||
with patch("grimoire.mcp.tools.design_systems.ds_svc") as svc:
|
||||
svc.update_token = AsyncMock(return_value=_fake_token())
|
||||
from scribe.mcp.tools.design_systems import update_design_token
|
||||
from grimoire.mcp.tools.design_systems import update_design_token
|
||||
await update_design_token(token_id=9, order_index=0)
|
||||
assert svc.update_token.await_args.kwargs["order_index"] == 0
|
||||
|
||||
@@ -132,9 +132,9 @@ async def test_update_token_accepts_order_index_zero():
|
||||
async def test_update_token_can_set_an_empty_value_map():
|
||||
"""`value_by_mode={}` is meaningful — it strips every mode from a token. The
|
||||
guard is `is not None`, not truthiness, or that edit would be unreachable."""
|
||||
with patch("scribe.mcp.tools.design_systems.ds_svc") as svc:
|
||||
with patch("grimoire.mcp.tools.design_systems.ds_svc") as svc:
|
||||
svc.update_token = AsyncMock(return_value=_fake_token())
|
||||
from scribe.mcp.tools.design_systems import update_design_token
|
||||
from grimoire.mcp.tools.design_systems import update_design_token
|
||||
await update_design_token(token_id=9, value_by_mode={})
|
||||
assert svc.update_token.await_args.kwargs["value_by_mode"] == {}
|
||||
|
||||
@@ -143,9 +143,9 @@ async def test_update_token_can_set_an_empty_value_map():
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_clearing_a_projects_design_system_passes_none():
|
||||
with patch("scribe.mcp.tools.design_systems.ds_svc") as svc:
|
||||
with patch("grimoire.mcp.tools.design_systems.ds_svc") as svc:
|
||||
svc.set_project_design_system = AsyncMock(return_value=True)
|
||||
from scribe.mcp.tools.design_systems import set_project_design_system
|
||||
from grimoire.mcp.tools.design_systems import set_project_design_system
|
||||
result = await set_project_design_system(project_id=2, design_system_id=-1)
|
||||
assert svc.set_project_design_system.await_args.args[2] is None
|
||||
assert result["design_system_id"] is None
|
||||
@@ -156,7 +156,7 @@ async def test_resolve_returns_serialised_tokens_with_their_provenance():
|
||||
"""The payload has to carry the shadowed entries, not just the winner —
|
||||
dropping them at the serialisation boundary would discard the one thing
|
||||
resolution was built to preserve."""
|
||||
from scribe.services.design_cascade import resolve_tokens
|
||||
from grimoire.services.design_cascade import resolve_tokens
|
||||
|
||||
|
||||
resolved = resolve_tokens(
|
||||
@@ -164,9 +164,9 @@ async def test_resolve_returns_serialised_tokens_with_their_provenance():
|
||||
{1: [design_token_stub("--fs-accent", {"base": "#6b2118"})],
|
||||
2: [design_token_stub("--fs-accent", {"base": "#5b4a8a"})]},
|
||||
)
|
||||
with patch("scribe.mcp.tools.design_systems.ds_svc") as svc:
|
||||
with patch("grimoire.mcp.tools.design_systems.ds_svc") as svc:
|
||||
svc.resolve_design_system = AsyncMock(return_value=resolved)
|
||||
from scribe.mcp.tools.design_systems import resolve_design_system
|
||||
from grimoire.mcp.tools.design_systems import resolve_design_system
|
||||
payload = await resolve_design_system(design_system_id=2)
|
||||
|
||||
token = payload["tokens"][0]
|
||||
@@ -183,18 +183,18 @@ async def test_update_token_can_clear_supersedes_with_an_empty_list():
|
||||
"""`[]` means "this token replaces nothing after all" — a real edit. Guarded
|
||||
on `is not None` so it isn't swallowed as "unchanged", the same trap
|
||||
update_snippet guards against."""
|
||||
with patch("scribe.mcp.tools.design_systems.ds_svc") as svc:
|
||||
with patch("grimoire.mcp.tools.design_systems.ds_svc") as svc:
|
||||
svc.update_token = AsyncMock(return_value=_fake_token())
|
||||
from scribe.mcp.tools.design_systems import update_design_token
|
||||
from grimoire.mcp.tools.design_systems import update_design_token
|
||||
await update_design_token(token_id=9, supersedes=[])
|
||||
assert svc.update_token.await_args.kwargs["supersedes"] == []
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_update_token_leaves_supersedes_alone_when_omitted():
|
||||
with patch("scribe.mcp.tools.design_systems.ds_svc") as svc:
|
||||
with patch("grimoire.mcp.tools.design_systems.ds_svc") as svc:
|
||||
svc.update_token = AsyncMock(return_value=_fake_token())
|
||||
from scribe.mcp.tools.design_systems import update_design_token
|
||||
from grimoire.mcp.tools.design_systems import update_design_token
|
||||
await update_design_token(token_id=9, purpose="text on action surfaces")
|
||||
assert "supersedes" not in svc.update_token.await_args.kwargs
|
||||
|
||||
@@ -206,15 +206,15 @@ async def test_get_design_system_carries_the_chain_merged_guidance():
|
||||
"""enter_project carries only a summary and points here. The
|
||||
system's own `guidance` field is only its departures; a session building
|
||||
UI needs the house style too, so the merged form rides alongside."""
|
||||
from scribe.mcp.tools.design_systems import get_design_system
|
||||
from grimoire.mcp.tools.design_systems import get_design_system
|
||||
|
||||
merged = [{"design_system_id": 1, "title": "House", "guidance": "house style"},
|
||||
{"design_system_id": 2, "title": "App", "guidance": "departures"}]
|
||||
with patch("scribe.mcp.tools.design_systems.ds_svc.get_design_system",
|
||||
with patch("grimoire.mcp.tools.design_systems.ds_svc.get_design_system",
|
||||
AsyncMock(return_value=_fake_design_system())), \
|
||||
patch("scribe.mcp.tools.design_systems.ds_svc.list_tokens",
|
||||
patch("grimoire.mcp.tools.design_systems.ds_svc.list_tokens",
|
||||
AsyncMock(return_value=[])), \
|
||||
patch("scribe.mcp.tools.design_systems.ds_svc.design_context",
|
||||
patch("grimoire.mcp.tools.design_systems.ds_svc.design_context",
|
||||
AsyncMock(return_value={"guidance": merged})):
|
||||
out = await get_design_system(2)
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ from unittest.mock import AsyncMock, MagicMock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
from scribe.mcp.tools.milestones import (
|
||||
from grimoire.mcp.tools.milestones import (
|
||||
list_milestones, get_milestone, create_milestone, update_milestone,
|
||||
)
|
||||
from tests.helpers import fake_milestone, fake_task
|
||||
@@ -16,7 +16,7 @@ pytestmark = pytest.mark.usefixtures("_bind_user")
|
||||
def _no_plan_gate():
|
||||
"""The plan gate reads the database; these tests are about what reaches
|
||||
the service. The gate's own tests re-patch it."""
|
||||
with patch("scribe.mcp.tools.milestones.dedup_svc.plan_gate",
|
||||
with patch("grimoire.mcp.tools.milestones.dedup_svc.plan_gate",
|
||||
AsyncMock(return_value=None)):
|
||||
yield
|
||||
|
||||
@@ -25,7 +25,7 @@ def _no_plan_gate():
|
||||
async def test_list_milestones_returns_dict_with_progress():
|
||||
rows = [{"id": 1, "title": "MS1", "status": "active", "total": 2}]
|
||||
with patch(
|
||||
"scribe.mcp.tools.milestones.milestones_svc.get_project_milestone_summary",
|
||||
"grimoire.mcp.tools.milestones.milestones_svc.get_project_milestone_summary",
|
||||
AsyncMock(return_value=rows),
|
||||
):
|
||||
out = await list_milestones(project_id=1)
|
||||
@@ -36,7 +36,7 @@ async def test_list_milestones_returns_dict_with_progress():
|
||||
async def test_create_milestone_passes_through():
|
||||
m = fake_milestone(id=5)
|
||||
mock = AsyncMock(return_value=m)
|
||||
with patch("scribe.mcp.tools.milestones.milestones_svc.create_milestone", mock):
|
||||
with patch("grimoire.mcp.tools.milestones.milestones_svc.create_milestone", mock):
|
||||
out = await create_milestone(project_id=1, title="new", description="d")
|
||||
assert out["id"] == 5
|
||||
assert mock.call_args.kwargs["project_id"] == 1
|
||||
@@ -48,7 +48,7 @@ async def test_create_milestone_passes_through():
|
||||
async def test_create_milestone_empty_description_becomes_none():
|
||||
m = fake_milestone()
|
||||
mock = AsyncMock(return_value=m)
|
||||
with patch("scribe.mcp.tools.milestones.milestones_svc.create_milestone", mock):
|
||||
with patch("grimoire.mcp.tools.milestones.milestones_svc.create_milestone", mock):
|
||||
await create_milestone(project_id=1, title="t", description="")
|
||||
assert mock.call_args.kwargs["description"] is None
|
||||
|
||||
@@ -58,7 +58,7 @@ async def test_create_milestone_passes_body_through():
|
||||
"""The milestone-as-plan body is forwarded to the service."""
|
||||
m = fake_milestone(id=5)
|
||||
mock = AsyncMock(return_value=m)
|
||||
with patch("scribe.mcp.tools.milestones.milestones_svc.create_milestone", mock):
|
||||
with patch("grimoire.mcp.tools.milestones.milestones_svc.create_milestone", mock):
|
||||
await create_milestone(project_id=1, title="t", body="## Goal\n\nship")
|
||||
assert mock.call_args.kwargs["body"] == "## Goal\n\nship"
|
||||
|
||||
@@ -67,7 +67,7 @@ async def test_create_milestone_passes_body_through():
|
||||
async def test_create_milestone_empty_body_becomes_none():
|
||||
m = fake_milestone()
|
||||
mock = AsyncMock(return_value=m)
|
||||
with patch("scribe.mcp.tools.milestones.milestones_svc.create_milestone", mock):
|
||||
with patch("grimoire.mcp.tools.milestones.milestones_svc.create_milestone", mock):
|
||||
await create_milestone(project_id=1, title="t", body="")
|
||||
assert mock.call_args.kwargs["body"] is None
|
||||
|
||||
@@ -76,9 +76,9 @@ async def test_create_milestone_empty_body_becomes_none():
|
||||
async def test_create_milestone_returns_the_active_plan_that_already_covers_it():
|
||||
match = {"duplicate": True, "existing_id": 4}
|
||||
create = AsyncMock()
|
||||
with patch("scribe.mcp.tools.milestones.dedup_svc.plan_gate",
|
||||
with patch("grimoire.mcp.tools.milestones.dedup_svc.plan_gate",
|
||||
AsyncMock(return_value=match)) as gate, \
|
||||
patch("scribe.mcp.tools.milestones.milestones_svc.create_milestone", create):
|
||||
patch("grimoire.mcp.tools.milestones.milestones_svc.create_milestone", create):
|
||||
out = await create_milestone(project_id=1, title="t", description="d", body="b")
|
||||
assert out is match
|
||||
create.assert_not_awaited()
|
||||
@@ -92,8 +92,8 @@ async def test_create_milestone_skips_the_gate_when_forced_or_done():
|
||||
plan competing with an open one."""
|
||||
gate = AsyncMock(return_value={"duplicate": True})
|
||||
create = AsyncMock(return_value=fake_milestone(id=6))
|
||||
with patch("scribe.mcp.tools.milestones.dedup_svc.plan_gate", gate), \
|
||||
patch("scribe.mcp.tools.milestones.milestones_svc.create_milestone", create):
|
||||
with patch("grimoire.mcp.tools.milestones.dedup_svc.plan_gate", gate), \
|
||||
patch("grimoire.mcp.tools.milestones.milestones_svc.create_milestone", create):
|
||||
assert (await create_milestone(project_id=1, title="t", force=True))["id"] == 6
|
||||
assert (await create_milestone(project_id=1, title="t", status="done"))["id"] == 6
|
||||
gate.assert_not_awaited()
|
||||
@@ -103,7 +103,7 @@ async def test_create_milestone_skips_the_gate_when_forced_or_done():
|
||||
async def test_update_milestone_sends_body():
|
||||
m = fake_milestone()
|
||||
mock = AsyncMock(return_value=m)
|
||||
with patch("scribe.mcp.tools.milestones.milestones_svc.update_milestone", mock):
|
||||
with patch("grimoire.mcp.tools.milestones.milestones_svc.update_milestone", mock):
|
||||
await update_milestone(project_id=1, milestone_id=5, body="new plan")
|
||||
assert mock.call_args.kwargs == {"body": "new plan"}
|
||||
|
||||
@@ -114,13 +114,13 @@ async def test_get_milestone_returns_body_steps_and_rules():
|
||||
step = fake_task(id=9, title="step 1", milestone_id=5, body="a long step body")
|
||||
applicable = {"rules": [{"id": 1, "title": "r"}], "truncated": False,
|
||||
"project_rules": [{"id": 3, "title": "own"}]}
|
||||
with patch("scribe.mcp.tools.milestones.milestones_svc.get_milestone",
|
||||
with patch("grimoire.mcp.tools.milestones.milestones_svc.get_milestone",
|
||||
AsyncMock(return_value=m)), \
|
||||
patch("scribe.mcp.tools.milestones.milestones_svc.get_milestone_progress",
|
||||
patch("grimoire.mcp.tools.milestones.milestones_svc.get_milestone_progress",
|
||||
AsyncMock(return_value={"total": 1, "completed": 0, "pct": 0.0})), \
|
||||
patch("scribe.mcp.tools.milestones.notes_svc.list_notes",
|
||||
patch("grimoire.mcp.tools.milestones.notes_svc.list_notes",
|
||||
AsyncMock(return_value=([step], 1))), \
|
||||
patch("scribe.mcp.tools.milestones.rulebooks_svc.get_applicable_rules",
|
||||
patch("grimoire.mcp.tools.milestones.rulebooks_svc.get_applicable_rules",
|
||||
AsyncMock(return_value=applicable)):
|
||||
out = await get_milestone(milestone_id=5)
|
||||
assert out["milestone"]["body"] == "## Goal"
|
||||
@@ -134,7 +134,7 @@ async def test_get_milestone_returns_body_steps_and_rules():
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_milestone_raises_when_not_found():
|
||||
with patch("scribe.mcp.tools.milestones.milestones_svc.get_milestone",
|
||||
with patch("grimoire.mcp.tools.milestones.milestones_svc.get_milestone",
|
||||
AsyncMock(return_value=None)):
|
||||
with pytest.raises(ValueError, match="milestone 999 not found"):
|
||||
await get_milestone(milestone_id=999)
|
||||
@@ -144,7 +144,7 @@ async def test_get_milestone_raises_when_not_found():
|
||||
async def test_update_milestone_only_sends_non_default_fields():
|
||||
m = fake_milestone()
|
||||
mock = AsyncMock(return_value=m)
|
||||
with patch("scribe.mcp.tools.milestones.milestones_svc.update_milestone", mock):
|
||||
with patch("grimoire.mcp.tools.milestones.milestones_svc.update_milestone", mock):
|
||||
await update_milestone(project_id=1, milestone_id=5, status="done")
|
||||
args, kwargs = mock.call_args
|
||||
assert args == (7, 5)
|
||||
@@ -156,7 +156,7 @@ async def test_update_milestone_order_index_negative_is_omitted():
|
||||
"""order_index=-1 sentinel means leave unchanged."""
|
||||
m = fake_milestone()
|
||||
mock = AsyncMock(return_value=m)
|
||||
with patch("scribe.mcp.tools.milestones.milestones_svc.update_milestone", mock):
|
||||
with patch("grimoire.mcp.tools.milestones.milestones_svc.update_milestone", mock):
|
||||
await update_milestone(project_id=1, milestone_id=5, order_index=-1)
|
||||
assert "order_index" not in mock.call_args.kwargs
|
||||
|
||||
@@ -166,7 +166,7 @@ async def test_update_milestone_order_index_zero_is_explicit():
|
||||
"""order_index=0 is a real value (top of list), not a sentinel."""
|
||||
m = fake_milestone()
|
||||
mock = AsyncMock(return_value=m)
|
||||
with patch("scribe.mcp.tools.milestones.milestones_svc.update_milestone", mock):
|
||||
with patch("grimoire.mcp.tools.milestones.milestones_svc.update_milestone", mock):
|
||||
await update_milestone(project_id=1, milestone_id=5, order_index=0)
|
||||
assert mock.call_args.kwargs["order_index"] == 0
|
||||
|
||||
@@ -174,7 +174,7 @@ async def test_update_milestone_order_index_zero_is_explicit():
|
||||
@pytest.mark.asyncio
|
||||
async def test_update_milestone_raises_when_not_found():
|
||||
with patch(
|
||||
"scribe.mcp.tools.milestones.milestones_svc.update_milestone",
|
||||
"grimoire.mcp.tools.milestones.milestones_svc.update_milestone",
|
||||
AsyncMock(return_value=None),
|
||||
):
|
||||
with pytest.raises(ValueError, match="milestone 999 not found"):
|
||||
|
||||
@@ -3,7 +3,7 @@ from unittest.mock import AsyncMock, MagicMock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
from scribe.mcp.tools.notes import (
|
||||
from grimoire.mcp.tools.notes import (
|
||||
list_notes, get_note, create_note,
|
||||
update_note, delete_note,
|
||||
)
|
||||
@@ -23,19 +23,19 @@ def _no_supersession():
|
||||
relation's own behaviour is covered in test_services_supersession.py, and
|
||||
the attachment is covered explicitly below.
|
||||
"""
|
||||
with patch("scribe.mcp.tools.notes.supersession_svc.get_relations",
|
||||
with patch("grimoire.mcp.tools.notes.supersession_svc.get_relations",
|
||||
AsyncMock(return_value={"supersedes": [], "superseded_by": []})):
|
||||
yield
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_create_note_blocked_by_duplicate_gate():
|
||||
from scribe.services.dedup import DuplicateMatch
|
||||
from grimoire.services.dedup import DuplicateMatch
|
||||
dup = DuplicateMatch(id=88, title="Embeddings notes", similarity=0.94, reason="semantic")
|
||||
create_mock = AsyncMock()
|
||||
with patch("scribe.mcp.tools.notes.dedup_svc.find_duplicate_note",
|
||||
with patch("grimoire.mcp.tools.notes.dedup_svc.find_duplicate_note",
|
||||
AsyncMock(return_value=dup)), \
|
||||
patch("scribe.mcp.tools.notes.notes_svc.create_note", create_mock):
|
||||
patch("grimoire.mcp.tools.notes.notes_svc.create_note", create_mock):
|
||||
out = await create_note(title="embeddings", body="notes about embeddings")
|
||||
assert out["duplicate"] is True
|
||||
assert out["existing_id"] == 88
|
||||
@@ -46,8 +46,8 @@ async def test_create_note_blocked_by_duplicate_gate():
|
||||
@pytest.mark.asyncio
|
||||
async def test_create_note_force_bypasses_duplicate_gate():
|
||||
find_mock = AsyncMock()
|
||||
with patch("scribe.mcp.tools.notes.dedup_svc.find_duplicate_note", find_mock), \
|
||||
patch("scribe.mcp.tools.notes.notes_svc.create_note",
|
||||
with patch("grimoire.mcp.tools.notes.dedup_svc.find_duplicate_note", find_mock), \
|
||||
patch("grimoire.mcp.tools.notes.notes_svc.create_note",
|
||||
AsyncMock(return_value=fake_note(id=3))):
|
||||
out = await create_note(title="dup", force=True)
|
||||
assert out["id"] == 3
|
||||
@@ -58,7 +58,7 @@ async def test_create_note_force_bypasses_duplicate_gate():
|
||||
async def test_list_notes_repackages_tuple_into_dict():
|
||||
rows = [fake_note(id=1), fake_note(id=2)]
|
||||
with patch(
|
||||
"scribe.mcp.tools.notes.notes_svc.list_notes",
|
||||
"grimoire.mcp.tools.notes.notes_svc.list_notes",
|
||||
AsyncMock(return_value=(rows, 2)),
|
||||
):
|
||||
out = await list_notes()
|
||||
@@ -69,7 +69,7 @@ async def test_list_notes_repackages_tuple_into_dict():
|
||||
@pytest.mark.asyncio
|
||||
async def test_list_notes_passes_is_task_false():
|
||||
mock = AsyncMock(return_value=([], 0))
|
||||
with patch("scribe.mcp.tools.notes.notes_svc.list_notes", mock):
|
||||
with patch("grimoire.mcp.tools.notes.notes_svc.list_notes", mock):
|
||||
await list_notes()
|
||||
assert mock.call_args.kwargs["is_task"] is False
|
||||
|
||||
@@ -78,7 +78,7 @@ async def test_list_notes_passes_is_task_false():
|
||||
async def test_list_notes_tag_filter_maps_to_list():
|
||||
"""The single-tag string param maps to a one-element list at the service layer."""
|
||||
mock = AsyncMock(return_value=([], 0))
|
||||
with patch("scribe.mcp.tools.notes.notes_svc.list_notes", mock):
|
||||
with patch("grimoire.mcp.tools.notes.notes_svc.list_notes", mock):
|
||||
await list_notes(tag="ops")
|
||||
assert mock.call_args.kwargs["tags"] == ["ops"]
|
||||
|
||||
@@ -86,7 +86,7 @@ async def test_list_notes_tag_filter_maps_to_list():
|
||||
@pytest.mark.asyncio
|
||||
async def test_list_notes_empty_tag_means_no_filter():
|
||||
mock = AsyncMock(return_value=([], 0))
|
||||
with patch("scribe.mcp.tools.notes.notes_svc.list_notes", mock):
|
||||
with patch("grimoire.mcp.tools.notes.notes_svc.list_notes", mock):
|
||||
await list_notes(tag="")
|
||||
assert mock.call_args.kwargs["tags"] is None
|
||||
|
||||
@@ -94,7 +94,7 @@ async def test_list_notes_empty_tag_means_no_filter():
|
||||
@pytest.mark.asyncio
|
||||
async def test_list_notes_search_text_maps_to_q():
|
||||
mock = AsyncMock(return_value=([], 0))
|
||||
with patch("scribe.mcp.tools.notes.notes_svc.list_notes", mock):
|
||||
with patch("grimoire.mcp.tools.notes.notes_svc.list_notes", mock):
|
||||
await list_notes(search_text="kafka")
|
||||
assert mock.call_args.kwargs["q"] == "kafka"
|
||||
|
||||
@@ -102,7 +102,7 @@ async def test_list_notes_search_text_maps_to_q():
|
||||
@pytest.mark.asyncio
|
||||
async def test_list_notes_limit_clamped():
|
||||
mock = AsyncMock(return_value=([], 0))
|
||||
with patch("scribe.mcp.tools.notes.notes_svc.list_notes", mock):
|
||||
with patch("grimoire.mcp.tools.notes.notes_svc.list_notes", mock):
|
||||
await list_notes(limit=9999)
|
||||
assert mock.call_args.kwargs["limit"] == 100
|
||||
|
||||
@@ -111,7 +111,7 @@ async def test_list_notes_limit_clamped():
|
||||
async def test_get_note_returns_dict():
|
||||
fake = fake_note(id=5, title="found")
|
||||
with patch(
|
||||
"scribe.mcp.tools.notes.notes_svc.get_note_for_user",
|
||||
"grimoire.mcp.tools.notes.notes_svc.get_note_for_user",
|
||||
AsyncMock(return_value=(fake, "owner")),
|
||||
):
|
||||
out = await get_note(note_id=5)
|
||||
@@ -135,9 +135,9 @@ async def test_get_note_warns_in_words_when_a_later_note_overtook_it():
|
||||
acts on it confidently either way.
|
||||
"""
|
||||
fake = fake_note(id=5, title="June's answer")
|
||||
with patch("scribe.mcp.tools.notes.notes_svc.get_note_for_user",
|
||||
with patch("grimoire.mcp.tools.notes.notes_svc.get_note_for_user",
|
||||
AsyncMock(return_value=(fake, "owner"))), \
|
||||
patch("scribe.mcp.tools.notes.supersession_svc.get_relations",
|
||||
patch("grimoire.mcp.tools.notes.supersession_svc.get_relations",
|
||||
AsyncMock(return_value={"supersedes": [], "superseded_by": [9]})):
|
||||
out = await get_note(note_id=5)
|
||||
assert out["superseded_by"] == [9]
|
||||
@@ -153,10 +153,10 @@ async def test_get_note_opens_a_shared_record_and_says_whose_it_is():
|
||||
handed — and the web UI opens the same note fine."""
|
||||
theirs = fake_note(id=5, title="Their note", user_id=9)
|
||||
with patch(
|
||||
"scribe.mcp.tools.notes.notes_svc.get_note_for_user",
|
||||
"grimoire.mcp.tools.notes.notes_svc.get_note_for_user",
|
||||
AsyncMock(return_value=(theirs, "viewer")),
|
||||
), patch(
|
||||
"scribe.services.access.describe_provenance",
|
||||
"grimoire.services.access.describe_provenance",
|
||||
AsyncMock(return_value={"shared": True, "owner": "alex",
|
||||
"permission": "viewer"}),
|
||||
):
|
||||
@@ -169,7 +169,7 @@ async def test_get_note_opens_a_shared_record_and_says_whose_it_is():
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_note_raises_when_not_found():
|
||||
with patch(
|
||||
"scribe.mcp.tools.notes.notes_svc.get_note_for_user",
|
||||
"grimoire.mcp.tools.notes.notes_svc.get_note_for_user",
|
||||
AsyncMock(return_value=None),
|
||||
):
|
||||
with pytest.raises(ValueError, match="note 999 not found"):
|
||||
@@ -183,7 +183,7 @@ async def test_get_note_treats_a_trashed_note_as_missing():
|
||||
trashed = fake_note(id=5)
|
||||
trashed.deleted_at = "2026-07-01T00:00:00Z"
|
||||
with patch(
|
||||
"scribe.mcp.tools.notes.notes_svc.get_note_for_user",
|
||||
"grimoire.mcp.tools.notes.notes_svc.get_note_for_user",
|
||||
AsyncMock(return_value=(trashed, "owner")),
|
||||
):
|
||||
with pytest.raises(ValueError, match="note 5 not found"):
|
||||
@@ -194,7 +194,7 @@ async def test_get_note_treats_a_trashed_note_as_missing():
|
||||
async def test_create_note_passes_through():
|
||||
fake = fake_note(id=10, title="new")
|
||||
mock = AsyncMock(return_value=fake)
|
||||
with patch("scribe.mcp.tools.notes.notes_svc.create_note", mock):
|
||||
with patch("grimoire.mcp.tools.notes.notes_svc.create_note", mock):
|
||||
out = await create_note(title="new", body="x", tags=["a"], project_id=5)
|
||||
assert out["id"] == 10
|
||||
assert mock.call_args.kwargs["title"] == "new"
|
||||
@@ -206,7 +206,7 @@ async def test_create_note_project_zero_becomes_none():
|
||||
"""project_id=0 sentinel must become None at the service layer (orphan note)."""
|
||||
fake = fake_note()
|
||||
mock = AsyncMock(return_value=fake)
|
||||
with patch("scribe.mcp.tools.notes.notes_svc.create_note", mock):
|
||||
with patch("grimoire.mcp.tools.notes.notes_svc.create_note", mock):
|
||||
await create_note(title="t", project_id=0)
|
||||
assert mock.call_args.kwargs["project_id"] is None
|
||||
|
||||
@@ -221,7 +221,7 @@ async def test_update_note_only_sends_non_default_fields():
|
||||
door takes, and the same test."""
|
||||
fake = fake_note()
|
||||
mock = AsyncMock(return_value=fake)
|
||||
with patch("scribe.mcp.tools.notes.notes_svc.update_note", mock):
|
||||
with patch("grimoire.mcp.tools.notes.notes_svc.update_note", mock):
|
||||
await update_note(note_id=1, title="new title")
|
||||
args, kwargs = mock.call_args
|
||||
assert args == (7, 1)
|
||||
@@ -234,11 +234,11 @@ async def test_update_note_forwards_a_check_but_not_an_empty_one():
|
||||
""""" means "leave this alone" at this door — an agent updating a body must
|
||||
not wipe a check it was never asked about."""
|
||||
mock = AsyncMock(return_value=fake_note())
|
||||
with patch("scribe.mcp.tools.notes.notes_svc.update_note", mock):
|
||||
with patch("grimoire.mcp.tools.notes.notes_svc.update_note", mock):
|
||||
await update_note(note_id=1, verify_with="curl the docs")
|
||||
assert mock.call_args.kwargs["verify_with"] == "curl the docs"
|
||||
|
||||
with patch("scribe.mcp.tools.notes.notes_svc.update_note", mock):
|
||||
with patch("grimoire.mcp.tools.notes.notes_svc.update_note", mock):
|
||||
await update_note(note_id=1, title="t")
|
||||
assert "verify_with" not in mock.call_args.kwargs
|
||||
|
||||
@@ -247,7 +247,7 @@ async def test_update_note_forwards_a_check_but_not_an_empty_one():
|
||||
async def test_update_note_forwards_clear_so_a_check_can_be_removed():
|
||||
"""The only way to unset a field at a door where "" means "leave alone"."""
|
||||
mock = AsyncMock(return_value=fake_note())
|
||||
with patch("scribe.mcp.tools.notes.notes_svc.update_note", mock):
|
||||
with patch("grimoire.mcp.tools.notes.notes_svc.update_note", mock):
|
||||
await update_note(note_id=1, clear=["verify_with"])
|
||||
assert mock.call_args.kwargs["clear"] == ["verify_with"]
|
||||
|
||||
@@ -255,7 +255,7 @@ async def test_update_note_forwards_clear_so_a_check_can_be_removed():
|
||||
@pytest.mark.asyncio
|
||||
async def test_create_note_forwards_the_check_fields():
|
||||
mock = AsyncMock(return_value=fake_note())
|
||||
with patch("scribe.mcp.tools.notes.notes_svc.create_note", mock):
|
||||
with patch("grimoire.mcp.tools.notes.notes_svc.create_note", mock):
|
||||
await create_note(title="t", verify_with="curl", expires_when="AMO changes")
|
||||
assert mock.call_args.kwargs["verify_with"] == "curl"
|
||||
assert mock.call_args.kwargs["expires_when"] == "AMO changes"
|
||||
@@ -266,7 +266,7 @@ async def test_update_note_empty_tags_clears_explicitly():
|
||||
"""tags=[] is an explicit clear, distinct from tags=None (omit)."""
|
||||
fake = fake_note()
|
||||
mock = AsyncMock(return_value=fake)
|
||||
with patch("scribe.mcp.tools.notes.notes_svc.update_note", mock):
|
||||
with patch("grimoire.mcp.tools.notes.notes_svc.update_note", mock):
|
||||
await update_note(note_id=1, tags=[])
|
||||
kwargs = dict(mock.call_args.kwargs)
|
||||
kwargs.pop("clear")
|
||||
@@ -277,7 +277,7 @@ async def test_update_note_empty_tags_clears_explicitly():
|
||||
async def test_update_note_tags_none_means_omit():
|
||||
fake = fake_note()
|
||||
mock = AsyncMock(return_value=fake)
|
||||
with patch("scribe.mcp.tools.notes.notes_svc.update_note", mock):
|
||||
with patch("grimoire.mcp.tools.notes.notes_svc.update_note", mock):
|
||||
await update_note(note_id=1, tags=None)
|
||||
assert "tags" not in mock.call_args.kwargs
|
||||
|
||||
@@ -285,7 +285,7 @@ async def test_update_note_tags_none_means_omit():
|
||||
@pytest.mark.asyncio
|
||||
async def test_update_note_raises_when_not_found():
|
||||
with patch(
|
||||
"scribe.mcp.tools.notes.notes_svc.update_note",
|
||||
"grimoire.mcp.tools.notes.notes_svc.update_note",
|
||||
AsyncMock(return_value=None),
|
||||
):
|
||||
with pytest.raises(ValueError, match="note 999 not found"):
|
||||
@@ -297,10 +297,10 @@ async def test_delete_note_soft_deletes_and_returns_batch():
|
||||
doomed = MagicMock()
|
||||
doomed.title = "the note that went"
|
||||
with patch(
|
||||
"scribe.mcp.tools.notes.trash_svc.delete",
|
||||
"grimoire.mcp.tools.notes.trash_svc.delete",
|
||||
AsyncMock(return_value="batch-1"),
|
||||
), patch(
|
||||
"scribe.mcp.tools.notes.notes_svc.get_note_for_user",
|
||||
"grimoire.mcp.tools.notes.notes_svc.get_note_for_user",
|
||||
AsyncMock(return_value=(doomed, "owner")),
|
||||
):
|
||||
result = await delete_note(note_id=7)
|
||||
@@ -316,10 +316,10 @@ async def test_delete_note_still_deletes_when_the_title_lookup_fails():
|
||||
"""The title is a courtesy on top of the delete, never a precondition for
|
||||
it. A lookup that errors costs the name, not the operation."""
|
||||
with patch(
|
||||
"scribe.mcp.tools.notes.trash_svc.delete",
|
||||
"grimoire.mcp.tools.notes.trash_svc.delete",
|
||||
AsyncMock(return_value="batch-1"),
|
||||
), patch(
|
||||
"scribe.mcp.tools.notes.notes_svc.get_note_for_user",
|
||||
"grimoire.mcp.tools.notes.notes_svc.get_note_for_user",
|
||||
AsyncMock(side_effect=RuntimeError("database down")),
|
||||
):
|
||||
result = await delete_note(note_id=7)
|
||||
@@ -330,10 +330,10 @@ async def test_delete_note_still_deletes_when_the_title_lookup_fails():
|
||||
@pytest.mark.asyncio
|
||||
async def test_delete_note_raises_when_not_found():
|
||||
with patch(
|
||||
"scribe.mcp.tools.notes.trash_svc.delete",
|
||||
"grimoire.mcp.tools.notes.trash_svc.delete",
|
||||
AsyncMock(return_value=None),
|
||||
), patch(
|
||||
"scribe.mcp.tools.notes.notes_svc.get_note_for_user",
|
||||
"grimoire.mcp.tools.notes.notes_svc.get_note_for_user",
|
||||
AsyncMock(return_value=None),
|
||||
):
|
||||
with pytest.raises(ValueError, match="note 999 not found"):
|
||||
|
||||
@@ -10,7 +10,7 @@ pytestmark = pytest.mark.usefixtures("_bind_user")
|
||||
@pytest.fixture(autouse=True)
|
||||
def _no_plan_gate():
|
||||
"""The plan gate reads the database; the tests that are about it re-patch it."""
|
||||
with patch("scribe.mcp.tools.tasks.dedup_svc.plan_gate", AsyncMock(return_value=None)):
|
||||
with patch("grimoire.mcp.tools.tasks.dedup_svc.plan_gate", AsyncMock(return_value=None)):
|
||||
yield
|
||||
|
||||
|
||||
@@ -18,9 +18,9 @@ def _no_plan_gate():
|
||||
async def test_start_planning_tool_delegates_to_service():
|
||||
payload = {"milestone": {"id": 5}, "applicable_rules": [], "project_rules": [],
|
||||
"applicable_rules_truncated": False, "project_goal": "", "open_task_count": 0}
|
||||
with patch("scribe.mcp.tools.tasks.planning_svc.start_planning",
|
||||
with patch("grimoire.mcp.tools.tasks.planning_svc.start_planning",
|
||||
AsyncMock(return_value=payload)) as mock:
|
||||
from scribe.mcp.tools.tasks import start_planning
|
||||
from grimoire.mcp.tools.tasks import start_planning
|
||||
out = await start_planning(project_id=3, title="Plan it")
|
||||
assert out["milestone"]["id"] == 5
|
||||
# No body and no steps reach the service as None, so it seeds the template
|
||||
@@ -36,11 +36,11 @@ async def test_start_planning_returns_the_plan_that_already_covers_it():
|
||||
handed the first instead, and nothing is created."""
|
||||
match = {"duplicate": True, "existing_id": 12}
|
||||
svc = AsyncMock()
|
||||
with patch("scribe.mcp.tools.tasks.dedup_svc.plan_gate",
|
||||
with patch("grimoire.mcp.tools.tasks.dedup_svc.plan_gate",
|
||||
AsyncMock(return_value=match)) as gate, \
|
||||
patch("scribe.mcp.tools.tasks.dedup_svc.find_duplicate_note", AsyncMock()) as steps, \
|
||||
patch("scribe.mcp.tools.tasks.planning_svc.start_planning", svc):
|
||||
from scribe.mcp.tools.tasks import start_planning
|
||||
patch("grimoire.mcp.tools.tasks.dedup_svc.find_duplicate_note", AsyncMock()) as steps, \
|
||||
patch("grimoire.mcp.tools.tasks.planning_svc.start_planning", svc):
|
||||
from grimoire.mcp.tools.tasks import start_planning
|
||||
out = await start_planning(project_id=3, title="Metadata", body="design",
|
||||
steps=[{"title": "Resolve editions", "body": "via providers"}])
|
||||
assert out is match
|
||||
@@ -55,10 +55,10 @@ async def test_start_planning_returns_the_plan_that_already_covers_it():
|
||||
@pytest.mark.asyncio
|
||||
async def test_force_creates_the_plan_without_asking_the_gate():
|
||||
gate = AsyncMock(return_value={"duplicate": True})
|
||||
with patch("scribe.mcp.tools.tasks.dedup_svc.plan_gate", gate), \
|
||||
patch("scribe.mcp.tools.tasks.planning_svc.start_planning",
|
||||
with patch("grimoire.mcp.tools.tasks.dedup_svc.plan_gate", gate), \
|
||||
patch("grimoire.mcp.tools.tasks.planning_svc.start_planning",
|
||||
AsyncMock(return_value={"milestone": {"id": 5}})) as svc:
|
||||
from scribe.mcp.tools.tasks import start_planning
|
||||
from grimoire.mcp.tools.tasks import start_planning
|
||||
out = await start_planning(project_id=3, title="Metadata", force=True)
|
||||
assert out["milestone"]["id"] == 5
|
||||
svc.assert_awaited_once()
|
||||
@@ -69,11 +69,11 @@ async def test_force_creates_the_plan_without_asking_the_gate():
|
||||
async def test_get_task_augments_plan_with_rules():
|
||||
applicable = {"rules": [{"id": 1, "title": "r"}], "truncated": False,
|
||||
"project_rules": [{"id": 3, "title": "own"}]}
|
||||
with patch("scribe.mcp.tools.tasks.notes_svc.get_note_for_user",
|
||||
with patch("grimoire.mcp.tools.tasks.notes_svc.get_note_for_user",
|
||||
AsyncMock(return_value=(fake_task(task_kind="plan", id=9, project_id=3), "owner"))), \
|
||||
patch("scribe.mcp.tools.tasks.rulebooks_svc.get_applicable_rules",
|
||||
patch("grimoire.mcp.tools.tasks.rulebooks_svc.get_applicable_rules",
|
||||
AsyncMock(return_value=applicable)):
|
||||
from scribe.mcp.tools.tasks import get_task
|
||||
from grimoire.mcp.tools.tasks import get_task
|
||||
out = await get_task(task_id=9)
|
||||
assert out["applicable_rules"] == [{"id": 1, "title": "r"}]
|
||||
assert out["project_rules"] == [{"id": 3, "title": "own"}]
|
||||
@@ -83,11 +83,11 @@ async def test_get_task_augments_plan_with_rules():
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_task_work_kind_has_no_rules():
|
||||
with patch("scribe.mcp.tools.tasks.notes_svc.get_note_for_user",
|
||||
with patch("grimoire.mcp.tools.tasks.notes_svc.get_note_for_user",
|
||||
AsyncMock(return_value=(fake_task(task_kind="work", id=9, project_id=3), "owner"))), \
|
||||
patch("scribe.mcp.tools.tasks.rulebooks_svc.get_applicable_rules",
|
||||
patch("grimoire.mcp.tools.tasks.rulebooks_svc.get_applicable_rules",
|
||||
AsyncMock()) as mock_rules:
|
||||
from scribe.mcp.tools.tasks import get_task
|
||||
from grimoire.mcp.tools.tasks import get_task
|
||||
out = await get_task(task_id=9)
|
||||
assert "applicable_rules" not in out
|
||||
assert not mock_rules.called
|
||||
|
||||
@@ -10,7 +10,7 @@ pytestmark = pytest.mark.usefixtures("_bind_user")
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_create_process_requires_title_and_body():
|
||||
from scribe.mcp.tools.processes import create_process
|
||||
from grimoire.mcp.tools.processes import create_process
|
||||
with pytest.raises(ValueError):
|
||||
await create_process(title="", body="something")
|
||||
with pytest.raises(ValueError):
|
||||
@@ -20,11 +20,11 @@ async def test_create_process_requires_title_and_body():
|
||||
@pytest.mark.asyncio
|
||||
async def test_create_process_sets_note_type():
|
||||
created = fake_note(title="Drift Audit", note_type="process")
|
||||
with patch("scribe.mcp.tools.processes.dedup_svc.find_duplicate_note",
|
||||
with patch("grimoire.mcp.tools.processes.dedup_svc.find_duplicate_note",
|
||||
AsyncMock(return_value=None)), \
|
||||
patch("scribe.services.notes.create_note",
|
||||
patch("grimoire.services.notes.create_note",
|
||||
AsyncMock(return_value=created)) as mock_create:
|
||||
from scribe.mcp.tools.processes import create_process
|
||||
from grimoire.mcp.tools.processes import create_process
|
||||
out = await create_process(title="Drift Audit", body="the prompt", tags=["audit"])
|
||||
assert out["note_type"] == "process"
|
||||
# the service was asked to create a process
|
||||
@@ -37,16 +37,16 @@ async def test_create_process_blocks_a_near_duplicate():
|
||||
"""The gate matters more for processes than for other kinds: each one becomes
|
||||
a skill file that auto-surfaces, so two near-identical procedures don't just
|
||||
bloat the corpus — they compete to be followed."""
|
||||
from scribe.services.dedup import DuplicateMatch
|
||||
from grimoire.services.dedup import DuplicateMatch
|
||||
|
||||
# The real dataclass, not a MagicMock: a mock answers every attribute, so it
|
||||
# would pass whatever field names this test happened to guess and prove
|
||||
# nothing about the payload the tool actually returns.
|
||||
match = DuplicateMatch(id=42, title="Drift Audit", similarity=0.94, reason="semantic")
|
||||
with patch("scribe.mcp.tools.processes.dedup_svc.find_duplicate_note",
|
||||
with patch("grimoire.mcp.tools.processes.dedup_svc.find_duplicate_note",
|
||||
AsyncMock(return_value=match)), \
|
||||
patch("scribe.services.notes.create_note", AsyncMock()) as mock_create:
|
||||
from scribe.mcp.tools.processes import create_process
|
||||
patch("grimoire.services.notes.create_note", AsyncMock()) as mock_create:
|
||||
from grimoire.mcp.tools.processes import create_process
|
||||
out = await create_process(title="Drift Audit", body="the prompt")
|
||||
assert out["duplicate"] is True
|
||||
assert out["existing_id"] == 42
|
||||
@@ -56,11 +56,11 @@ async def test_create_process_blocks_a_near_duplicate():
|
||||
@pytest.mark.asyncio
|
||||
async def test_create_process_force_bypasses_the_gate():
|
||||
created = fake_note(title="Drift Audit", note_type="process")
|
||||
with patch("scribe.mcp.tools.processes.dedup_svc.find_duplicate_note",
|
||||
with patch("grimoire.mcp.tools.processes.dedup_svc.find_duplicate_note",
|
||||
AsyncMock()) as find_mock, \
|
||||
patch("scribe.services.notes.create_note",
|
||||
patch("grimoire.services.notes.create_note",
|
||||
AsyncMock(return_value=created)):
|
||||
from scribe.mcp.tools.processes import create_process
|
||||
from grimoire.mcp.tools.processes import create_process
|
||||
await create_process(title="Drift Audit", body="the prompt", force=True)
|
||||
find_mock.assert_not_awaited()
|
||||
|
||||
@@ -68,9 +68,9 @@ async def test_create_process_force_bypasses_the_gate():
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_process_returns_body_and_candidates():
|
||||
note = fake_note(id=7, title="Drift Audit", note_type="process")
|
||||
with patch("scribe.services.notes.resolve_process",
|
||||
with patch("grimoire.services.notes.resolve_process",
|
||||
AsyncMock(return_value=(note, [{"id": 9, "title": "Drift Audit Notes"}]))):
|
||||
from scribe.mcp.tools.processes import get_process
|
||||
from grimoire.mcp.tools.processes import get_process
|
||||
out = await get_process("drift")
|
||||
assert out["id"] == 7
|
||||
assert out["other_matches"] == [{"id": 9, "title": "Drift Audit Notes"}]
|
||||
@@ -85,12 +85,12 @@ async def test_get_process_flags_another_users_process():
|
||||
the returned body', so an unlabelled one would put someone else's procedure
|
||||
in charge of the session."""
|
||||
note = fake_note(id=7, title="Drift Audit", user_id=9, note_type="process")
|
||||
with patch("scribe.services.notes.resolve_process",
|
||||
with patch("grimoire.services.notes.resolve_process",
|
||||
AsyncMock(return_value=(note, []))), \
|
||||
patch("scribe.services.access.describe_provenance",
|
||||
patch("grimoire.services.access.describe_provenance",
|
||||
AsyncMock(return_value={"shared": True, "owner": "alex",
|
||||
"permission": "viewer"})):
|
||||
from scribe.mcp.tools.processes import get_process
|
||||
from grimoire.mcp.tools.processes import get_process
|
||||
out = await get_process("deploy")
|
||||
assert out["shared"] is True
|
||||
assert out["owner"] == "alex"
|
||||
@@ -98,9 +98,9 @@ async def test_get_process_flags_another_users_process():
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_process_not_found_raises():
|
||||
with patch("scribe.services.notes.resolve_process",
|
||||
with patch("grimoire.services.notes.resolve_process",
|
||||
AsyncMock(return_value=(None, []))):
|
||||
from scribe.mcp.tools.processes import get_process
|
||||
from grimoire.mcp.tools.processes import get_process
|
||||
with pytest.raises(ValueError):
|
||||
await get_process("missing")
|
||||
|
||||
@@ -111,9 +111,9 @@ async def test_update_process_rejects_non_process_note():
|
||||
# returns (note, permission).
|
||||
plain = fake_note(id=3, title="Drift Audit", note_type="note")
|
||||
plain.deleted_at = None
|
||||
with patch("scribe.services.notes.get_note_for_user",
|
||||
with patch("grimoire.services.notes.get_note_for_user",
|
||||
AsyncMock(return_value=(plain, "owner"))):
|
||||
from scribe.mcp.tools.processes import update_process
|
||||
from grimoire.mcp.tools.processes import update_process
|
||||
with pytest.raises(ValueError):
|
||||
await update_process(process_id=3, title="x")
|
||||
|
||||
@@ -125,11 +125,11 @@ async def test_update_process_refuses_a_read_only_share_with_the_reason():
|
||||
open would just send them looking for a missing id."""
|
||||
theirs = fake_note(id=5, title="Drift Audit", user_id=9, note_type="process")
|
||||
theirs.deleted_at = None
|
||||
with patch("scribe.services.notes.get_note_for_user",
|
||||
with patch("grimoire.services.notes.get_note_for_user",
|
||||
AsyncMock(return_value=(theirs, "viewer"))), \
|
||||
patch("scribe.services.access.can_write_note", AsyncMock(return_value=False)), \
|
||||
patch("scribe.services.notes.update_note", AsyncMock()) as mock_update:
|
||||
from scribe.mcp.tools.processes import update_process
|
||||
patch("grimoire.services.access.can_write_note", AsyncMock(return_value=False)), \
|
||||
patch("grimoire.services.notes.update_note", AsyncMock()) as mock_update:
|
||||
from grimoire.mcp.tools.processes import update_process
|
||||
with pytest.raises(ValueError, match="read-only"):
|
||||
await update_process(process_id=5, title="x")
|
||||
mock_update.assert_not_awaited()
|
||||
@@ -139,11 +139,11 @@ async def test_update_process_refuses_a_read_only_share_with_the_reason():
|
||||
async def test_delete_process_trashes_it_recoverably():
|
||||
proc = fake_note(id=4, title="Drift Audit", note_type="process")
|
||||
proc.deleted_at = None
|
||||
with patch("scribe.services.notes.get_note_for_user",
|
||||
with patch("grimoire.services.notes.get_note_for_user",
|
||||
AsyncMock(return_value=(proc, "owner"))), \
|
||||
patch("scribe.mcp.tools.processes.trash_svc.delete",
|
||||
patch("grimoire.mcp.tools.processes.trash_svc.delete",
|
||||
AsyncMock(return_value="batch-1")) as mock_delete:
|
||||
from scribe.mcp.tools.processes import delete_process
|
||||
from grimoire.mcp.tools.processes import delete_process
|
||||
out = await delete_process(process_id=4)
|
||||
assert out["deleted_batch_id"] == "batch-1"
|
||||
# Through the trash, not a hard delete — restorable like every other kind.
|
||||
@@ -157,10 +157,10 @@ async def test_delete_process_refuses_a_plain_note():
|
||||
mistyped argument."""
|
||||
plain = fake_note(id=3, title="Drift Audit", note_type="note")
|
||||
plain.deleted_at = None
|
||||
with patch("scribe.services.notes.get_note_for_user",
|
||||
with patch("grimoire.services.notes.get_note_for_user",
|
||||
AsyncMock(return_value=(plain, "owner"))), \
|
||||
patch("scribe.mcp.tools.processes.trash_svc.delete", AsyncMock()) as mock_delete:
|
||||
from scribe.mcp.tools.processes import delete_process
|
||||
patch("grimoire.mcp.tools.processes.trash_svc.delete", AsyncMock()) as mock_delete:
|
||||
from grimoire.mcp.tools.processes import delete_process
|
||||
with pytest.raises(ValueError):
|
||||
await delete_process(process_id=3)
|
||||
mock_delete.assert_not_awaited()
|
||||
@@ -172,11 +172,11 @@ async def test_delete_process_refuses_a_plain_note():
|
||||
async def test_create_process_stores_the_moments_it_is_for():
|
||||
created = fake_note(title="Release", note_type="process",
|
||||
data={"moments": ["work.deliver"]})
|
||||
with patch("scribe.mcp.tools.processes.dedup_svc.find_duplicate_note",
|
||||
with patch("grimoire.mcp.tools.processes.dedup_svc.find_duplicate_note",
|
||||
AsyncMock(return_value=None)), \
|
||||
patch("scribe.services.notes.create_note",
|
||||
patch("grimoire.services.notes.create_note",
|
||||
AsyncMock(return_value=created)) as mock_create:
|
||||
from scribe.mcp.tools.processes import create_process
|
||||
from grimoire.mcp.tools.processes import create_process
|
||||
out = await create_process(title="Release", body="steps",
|
||||
moments=["Work.Deliver", "work.deliver"])
|
||||
assert mock_create.await_args.kwargs["data"] == {"moments": ["work.deliver"]}
|
||||
@@ -187,10 +187,10 @@ async def test_create_process_stores_the_moments_it_is_for():
|
||||
async def test_create_process_refuses_an_unknown_moment_before_writing():
|
||||
"""A typo'd moment would be stored and never fire — a procedure that looks
|
||||
attached and is not."""
|
||||
with patch("scribe.mcp.tools.processes.dedup_svc.find_duplicate_note",
|
||||
with patch("grimoire.mcp.tools.processes.dedup_svc.find_duplicate_note",
|
||||
AsyncMock(return_value=None)), \
|
||||
patch("scribe.services.notes.create_note", AsyncMock()) as mock_create:
|
||||
from scribe.mcp.tools.processes import create_process
|
||||
patch("grimoire.services.notes.create_note", AsyncMock()) as mock_create:
|
||||
from grimoire.mcp.tools.processes import create_process
|
||||
with pytest.raises(ValueError, match="unknown moment"):
|
||||
await create_process(title="Release", body="steps", moments=["work.shipped"])
|
||||
mock_create.assert_not_awaited()
|
||||
@@ -199,11 +199,11 @@ async def test_create_process_refuses_an_unknown_moment_before_writing():
|
||||
@pytest.mark.asyncio
|
||||
async def test_create_process_without_moments_stores_no_data():
|
||||
created = fake_note(title="Release", note_type="process")
|
||||
with patch("scribe.mcp.tools.processes.dedup_svc.find_duplicate_note",
|
||||
with patch("grimoire.mcp.tools.processes.dedup_svc.find_duplicate_note",
|
||||
AsyncMock(return_value=None)), \
|
||||
patch("scribe.services.notes.create_note",
|
||||
patch("grimoire.services.notes.create_note",
|
||||
AsyncMock(return_value=created)) as mock_create:
|
||||
from scribe.mcp.tools.processes import create_process
|
||||
from grimoire.mcp.tools.processes import create_process
|
||||
out = await create_process(title="Release", body="steps")
|
||||
assert mock_create.await_args.kwargs["data"] is None
|
||||
assert out["moments"] == []
|
||||
@@ -211,12 +211,12 @@ async def test_create_process_without_moments_stores_no_data():
|
||||
|
||||
async def _update(moments, data):
|
||||
proc = fake_note(id=5, title="Release", note_type="process", data=data)
|
||||
with patch("scribe.services.notes.get_note_for_user",
|
||||
with patch("grimoire.services.notes.get_note_for_user",
|
||||
AsyncMock(return_value=(proc, "owner"))), \
|
||||
patch("scribe.services.access.can_write_note", AsyncMock(return_value=True)), \
|
||||
patch("scribe.services.notes.update_note",
|
||||
patch("grimoire.services.access.can_write_note", AsyncMock(return_value=True)), \
|
||||
patch("grimoire.services.notes.update_note",
|
||||
AsyncMock(return_value=proc)) as mock_update:
|
||||
from scribe.mcp.tools.processes import update_process
|
||||
from grimoire.mcp.tools.processes import update_process
|
||||
await update_process(process_id=5, moments=moments)
|
||||
return mock_update.await_args.kwargs
|
||||
|
||||
@@ -242,8 +242,8 @@ async def test_update_process_without_moments_leaves_data_alone():
|
||||
async def test_get_process_says_which_moments_it_is_for():
|
||||
note = fake_note(id=7, title="Release", note_type="process",
|
||||
data={"moments": ["work.deliver"]})
|
||||
with patch("scribe.services.notes.resolve_process", AsyncMock(return_value=(note, []))):
|
||||
from scribe.mcp.tools.processes import get_process
|
||||
with patch("grimoire.services.notes.resolve_process", AsyncMock(return_value=(note, []))):
|
||||
from grimoire.mcp.tools.processes import get_process
|
||||
out = await get_process("release")
|
||||
assert out["moments"] == ["work.deliver"]
|
||||
|
||||
@@ -253,7 +253,7 @@ def test_register_attaches_every_tool_in_the_module():
|
||||
registered is invisible to an agent, and nothing else would notice."""
|
||||
import inspect
|
||||
|
||||
from scribe.mcp.tools import processes
|
||||
from grimoire.mcp.tools import processes
|
||||
mcp = FakeMCP()
|
||||
|
||||
processes.register(mcp)
|
||||
|
||||
@@ -4,7 +4,7 @@ from unittest.mock import AsyncMock, MagicMock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
from scribe.mcp.tools.projects import (
|
||||
from grimoire.mcp.tools.projects import (
|
||||
list_projects, get_project, create_project,
|
||||
update_project, enter_project,
|
||||
)
|
||||
@@ -21,7 +21,7 @@ def _no_systems():
|
||||
the lookup is stubbed to the common case — a project with none. The
|
||||
populated shape is asserted in its own test below.
|
||||
"""
|
||||
with patch("scribe.mcp.tools.projects.systems_svc.list_systems",
|
||||
with patch("grimoire.mcp.tools.projects.systems_svc.list_systems",
|
||||
AsyncMock(return_value=[])):
|
||||
yield
|
||||
|
||||
@@ -33,7 +33,7 @@ def _no_platforms():
|
||||
case — a project with none yet. The populated shape is asserted in its
|
||||
own test.
|
||||
"""
|
||||
with patch("scribe.mcp.tools.projects.platforms_svc.project_platforms",
|
||||
with patch("grimoire.mcp.tools.projects.platforms_svc.project_platforms",
|
||||
AsyncMock(return_value=[])):
|
||||
yield
|
||||
|
||||
@@ -44,7 +44,7 @@ def _no_coverage():
|
||||
deal: no database here, stub the common case (nothing computed). The
|
||||
populated line is asserted in tests/test_pattern_coverage.py.
|
||||
"""
|
||||
with patch("scribe.mcp.tools.projects.coverage_svc.cached_coverage",
|
||||
with patch("grimoire.mcp.tools.projects.coverage_svc.cached_coverage",
|
||||
AsyncMock(return_value=None)):
|
||||
yield
|
||||
|
||||
@@ -54,7 +54,7 @@ def _no_family():
|
||||
"""enter_project reads the family-canon readout —
|
||||
no database here, so the common case is stubbed: nothing to answer. The
|
||||
populated key is asserted in its own test below."""
|
||||
with patch("scribe.mcp.tools.projects.family_adoption_svc.family_readout",
|
||||
with patch("grimoire.mcp.tools.projects.family_adoption_svc.family_readout",
|
||||
AsyncMock(return_value=None)) as mock:
|
||||
yield mock
|
||||
|
||||
@@ -64,7 +64,7 @@ def _no_background_seed():
|
||||
"""enter_project fire-and-forgets a coverage self-seed. These
|
||||
are no-database unit tests, so the spawn is stubbed out; the firing shape
|
||||
has its own test below."""
|
||||
with patch("scribe.mcp.tools.projects.spawn") as mock:
|
||||
with patch("grimoire.mcp.tools.projects.spawn") as mock:
|
||||
yield mock
|
||||
|
||||
|
||||
@@ -75,7 +75,7 @@ def _no_bootstrap():
|
||||
records — a database read. Stub the common case (young project, no ask);
|
||||
the firing shape has its own test below.
|
||||
"""
|
||||
with patch("scribe.mcp.tools.projects.systems_tools.bootstrap_systems_ask",
|
||||
with patch("grimoire.mcp.tools.projects.systems_tools.bootstrap_systems_ask",
|
||||
AsyncMock(return_value=None)) as mock:
|
||||
yield mock
|
||||
|
||||
@@ -84,7 +84,7 @@ def _no_bootstrap():
|
||||
async def test_list_projects_wraps_in_dict():
|
||||
rows = [fake_project(id=1), fake_project(id=2)]
|
||||
with patch(
|
||||
"scribe.mcp.tools.projects.projects_svc.list_projects",
|
||||
"grimoire.mcp.tools.projects.projects_svc.list_projects",
|
||||
AsyncMock(return_value=rows),
|
||||
):
|
||||
out = await list_projects()
|
||||
@@ -99,13 +99,13 @@ async def test_get_project_enriches_with_milestone_summary():
|
||||
"rules": [], "truncated": False,
|
||||
}
|
||||
with patch(
|
||||
"scribe.mcp.tools.projects.projects_svc.get_project",
|
||||
"grimoire.mcp.tools.projects.projects_svc.get_project",
|
||||
AsyncMock(return_value=p),
|
||||
), patch(
|
||||
"scribe.mcp.tools.projects.milestones_svc.get_project_milestone_summary",
|
||||
"grimoire.mcp.tools.projects.milestones_svc.get_project_milestone_summary",
|
||||
AsyncMock(return_value=milestone_summary),
|
||||
), patch(
|
||||
"scribe.mcp.tools.projects.rulebooks_svc.get_applicable_rules",
|
||||
"grimoire.mcp.tools.projects.rulebooks_svc.get_applicable_rules",
|
||||
AsyncMock(return_value=applicable_payload),
|
||||
):
|
||||
out = await get_project(project_id=5)
|
||||
@@ -131,13 +131,13 @@ async def test_get_project_includes_applicable_rules_and_project_rules():
|
||||
"truncated": False,
|
||||
}
|
||||
with patch(
|
||||
"scribe.mcp.tools.projects.projects_svc.get_project",
|
||||
"grimoire.mcp.tools.projects.projects_svc.get_project",
|
||||
AsyncMock(return_value=p),
|
||||
), patch(
|
||||
"scribe.mcp.tools.projects.milestones_svc.get_project_milestone_summary",
|
||||
"grimoire.mcp.tools.projects.milestones_svc.get_project_milestone_summary",
|
||||
AsyncMock(return_value=milestone_summary),
|
||||
), patch(
|
||||
"scribe.mcp.tools.projects.rulebooks_svc.get_applicable_rules",
|
||||
"grimoire.mcp.tools.projects.rulebooks_svc.get_applicable_rules",
|
||||
AsyncMock(return_value=applicable_payload),
|
||||
):
|
||||
out = await get_project(project_id=3)
|
||||
@@ -151,7 +151,7 @@ async def test_get_project_includes_applicable_rules_and_project_rules():
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_project_raises_when_not_found():
|
||||
with patch(
|
||||
"scribe.mcp.tools.projects.projects_svc.get_project",
|
||||
"grimoire.mcp.tools.projects.projects_svc.get_project",
|
||||
AsyncMock(return_value=None),
|
||||
):
|
||||
with pytest.raises(ValueError, match="project 999 not found"):
|
||||
@@ -162,7 +162,7 @@ async def test_get_project_raises_when_not_found():
|
||||
async def test_create_project_passes_color_empty_as_none():
|
||||
p = fake_project()
|
||||
mock = AsyncMock(return_value=p)
|
||||
with patch("scribe.mcp.tools.projects.projects_svc.create_project", mock):
|
||||
with patch("grimoire.mcp.tools.projects.projects_svc.create_project", mock):
|
||||
await create_project(title="P", color="")
|
||||
assert mock.call_args.kwargs["color"] is None
|
||||
|
||||
@@ -171,7 +171,7 @@ async def test_create_project_passes_color_empty_as_none():
|
||||
async def test_update_project_only_sends_non_default_fields():
|
||||
p = fake_project()
|
||||
mock = AsyncMock(return_value=p)
|
||||
with patch("scribe.mcp.tools.projects.projects_svc.update_project", mock):
|
||||
with patch("grimoire.mcp.tools.projects.projects_svc.update_project", mock):
|
||||
await update_project(project_id=1, status="archived")
|
||||
args, kwargs = mock.call_args
|
||||
assert args == (7, 1)
|
||||
@@ -181,7 +181,7 @@ async def test_update_project_only_sends_non_default_fields():
|
||||
@pytest.mark.asyncio
|
||||
async def test_update_project_raises_when_not_found():
|
||||
with patch(
|
||||
"scribe.mcp.tools.projects.projects_svc.update_project",
|
||||
"grimoire.mcp.tools.projects.projects_svc.update_project",
|
||||
AsyncMock(return_value=None),
|
||||
):
|
||||
with pytest.raises(ValueError, match="project 999 not found"):
|
||||
@@ -209,16 +209,16 @@ async def test_enter_project_composes_full_context():
|
||||
note1.updated_at = None # avoids datetime mocking
|
||||
|
||||
with patch(
|
||||
"scribe.mcp.tools.projects.projects_svc.get_project",
|
||||
"grimoire.mcp.tools.projects.projects_svc.get_project",
|
||||
AsyncMock(return_value=p),
|
||||
), patch(
|
||||
"scribe.mcp.tools.projects.rulebooks_svc.get_applicable_rules",
|
||||
"grimoire.mcp.tools.projects.rulebooks_svc.get_applicable_rules",
|
||||
AsyncMock(return_value=applicable_payload),
|
||||
), patch(
|
||||
"scribe.mcp.tools.projects.milestones_svc.get_project_milestone_summary",
|
||||
"grimoire.mcp.tools.projects.milestones_svc.get_project_milestone_summary",
|
||||
AsyncMock(return_value=milestone_summary),
|
||||
), patch(
|
||||
"scribe.mcp.tools.projects.notes_svc.list_notes",
|
||||
"grimoire.mcp.tools.projects.notes_svc.list_notes",
|
||||
AsyncMock(side_effect=[([task1], 1), ([note1], 1)]),
|
||||
):
|
||||
out = await enter_project(project_id=5)
|
||||
@@ -260,19 +260,19 @@ async def test_enter_project_surfaces_the_systems_vocabulary():
|
||||
sys1.description = "Embeddings, ranking, auto-inject.\nLong detail below."
|
||||
|
||||
with patch(
|
||||
"scribe.mcp.tools.projects.projects_svc.get_project",
|
||||
"grimoire.mcp.tools.projects.projects_svc.get_project",
|
||||
AsyncMock(return_value=p),
|
||||
), patch(
|
||||
"scribe.mcp.tools.projects.rulebooks_svc.get_applicable_rules",
|
||||
"grimoire.mcp.tools.projects.rulebooks_svc.get_applicable_rules",
|
||||
AsyncMock(return_value={"rules": [], "truncated": False}),
|
||||
), patch(
|
||||
"scribe.mcp.tools.projects.milestones_svc.get_project_milestone_summary",
|
||||
"grimoire.mcp.tools.projects.milestones_svc.get_project_milestone_summary",
|
||||
AsyncMock(return_value=[]),
|
||||
), patch(
|
||||
"scribe.mcp.tools.projects.notes_svc.list_notes",
|
||||
"grimoire.mcp.tools.projects.notes_svc.list_notes",
|
||||
AsyncMock(side_effect=[([], 0), ([], 0)]),
|
||||
), patch(
|
||||
"scribe.mcp.tools.projects.systems_svc.list_systems",
|
||||
"grimoire.mcp.tools.projects.systems_svc.list_systems",
|
||||
AsyncMock(return_value=[sys1]),
|
||||
):
|
||||
out = await enter_project(project_id=5)
|
||||
@@ -283,13 +283,13 @@ async def test_enter_project_surfaces_the_systems_vocabulary():
|
||||
def _enter_project_stubs(p):
|
||||
"""The four patches every enter_project test repeats, as one context list."""
|
||||
return [
|
||||
patch("scribe.mcp.tools.projects.projects_svc.get_project",
|
||||
patch("grimoire.mcp.tools.projects.projects_svc.get_project",
|
||||
AsyncMock(return_value=p)),
|
||||
patch("scribe.mcp.tools.projects.rulebooks_svc.get_applicable_rules",
|
||||
patch("grimoire.mcp.tools.projects.rulebooks_svc.get_applicable_rules",
|
||||
AsyncMock(return_value={"rules": [], "truncated": False})),
|
||||
patch("scribe.mcp.tools.projects.milestones_svc.get_project_milestone_summary",
|
||||
patch("grimoire.mcp.tools.projects.milestones_svc.get_project_milestone_summary",
|
||||
AsyncMock(return_value=[])),
|
||||
patch("scribe.mcp.tools.projects.notes_svc.list_notes",
|
||||
patch("grimoire.mcp.tools.projects.notes_svc.list_notes",
|
||||
AsyncMock(side_effect=[([], 0), ([], 0)])),
|
||||
]
|
||||
|
||||
@@ -306,7 +306,7 @@ async def test_enter_project_carries_the_bootstrap_ask_when_it_fires():
|
||||
for cm in _enter_project_stubs(fake_project(id=5)):
|
||||
stack.enter_context(cm)
|
||||
stack.enter_context(patch(
|
||||
"scribe.mcp.tools.projects.systems_tools.bootstrap_systems_ask",
|
||||
"grimoire.mcp.tools.projects.systems_tools.bootstrap_systems_ask",
|
||||
AsyncMock(return_value=ask),
|
||||
))
|
||||
out = await enter_project(project_id=5)
|
||||
@@ -327,7 +327,7 @@ async def test_enter_project_never_asks_bootstrap_once_a_vocabulary_exists(
|
||||
for cm in _enter_project_stubs(fake_project(id=5)):
|
||||
stack.enter_context(cm)
|
||||
stack.enter_context(patch(
|
||||
"scribe.mcp.tools.projects.systems_svc.list_systems",
|
||||
"grimoire.mcp.tools.projects.systems_svc.list_systems",
|
||||
AsyncMock(return_value=[sys1]),
|
||||
))
|
||||
out = await enter_project(project_id=5)
|
||||
@@ -351,7 +351,7 @@ async def test_enter_project_fires_the_coverage_seed_on_the_owner(
|
||||
for cm in _enter_project_stubs(project):
|
||||
stack.enter_context(cm)
|
||||
seed = stack.enter_context(patch(
|
||||
"scribe.mcp.tools.projects.coverage_svc.refresh_if_stale",
|
||||
"grimoire.mcp.tools.projects.coverage_svc.refresh_if_stale",
|
||||
MagicMock(return_value=object()),
|
||||
))
|
||||
await enter_project(project_id=5)
|
||||
@@ -372,19 +372,19 @@ async def test_enter_project_hands_back_the_design_system_when_the_project_has_o
|
||||
"inherits_from": ["House"], "description": ""}
|
||||
|
||||
with patch(
|
||||
"scribe.mcp.tools.projects.projects_svc.get_project",
|
||||
"grimoire.mcp.tools.projects.projects_svc.get_project",
|
||||
AsyncMock(return_value=p),
|
||||
), patch(
|
||||
"scribe.mcp.tools.projects.rulebooks_svc.get_applicable_rules",
|
||||
"grimoire.mcp.tools.projects.rulebooks_svc.get_applicable_rules",
|
||||
AsyncMock(return_value={"rules": [], "truncated": False}),
|
||||
), patch(
|
||||
"scribe.mcp.tools.projects.milestones_svc.get_project_milestone_summary",
|
||||
"grimoire.mcp.tools.projects.milestones_svc.get_project_milestone_summary",
|
||||
AsyncMock(return_value=[]),
|
||||
), patch(
|
||||
"scribe.mcp.tools.projects.notes_svc.list_notes",
|
||||
"grimoire.mcp.tools.projects.notes_svc.list_notes",
|
||||
AsyncMock(side_effect=[([], 0), ([], 0)]),
|
||||
), patch(
|
||||
"scribe.mcp.tools.projects.design_systems_svc.design_context",
|
||||
"grimoire.mcp.tools.projects.design_systems_svc.design_context",
|
||||
AsyncMock(return_value=design),
|
||||
) as ctx:
|
||||
out = await enter_project(project_id=5)
|
||||
@@ -400,7 +400,7 @@ async def test_enter_project_hands_back_the_design_system_when_the_project_has_o
|
||||
@pytest.mark.asyncio
|
||||
async def test_enter_project_raises_when_project_not_found():
|
||||
with patch(
|
||||
"scribe.mcp.tools.projects.projects_svc.get_project",
|
||||
"grimoire.mcp.tools.projects.projects_svc.get_project",
|
||||
AsyncMock(return_value=None),
|
||||
):
|
||||
with pytest.raises(ValueError, match="project 999 not found"):
|
||||
@@ -409,7 +409,7 @@ async def test_enter_project_raises_when_project_not_found():
|
||||
|
||||
def test_enter_project_registered_in_register():
|
||||
"""register(mcp) registers enter_project alongside the existing tools."""
|
||||
from scribe.mcp.tools.projects import register
|
||||
from grimoire.mcp.tools.projects import register
|
||||
mcp = FakeMCP()
|
||||
|
||||
register(mcp)
|
||||
@@ -422,8 +422,8 @@ def test_enter_project_registered_in_register():
|
||||
@pytest.mark.asyncio
|
||||
async def test_create_project_without_inception_args_stays_undecided():
|
||||
p = fake_project(id=5, title="P", inception=None)
|
||||
with patch("scribe.mcp.tools.projects.projects_svc.create_project", AsyncMock(return_value=p)), \
|
||||
patch("scribe.mcp.tools.projects.inception_svc.decide", AsyncMock()) as decide:
|
||||
with patch("grimoire.mcp.tools.projects.projects_svc.create_project", AsyncMock(return_value=p)), \
|
||||
patch("grimoire.mcp.tools.projects.inception_svc.decide", AsyncMock()) as decide:
|
||||
out = await create_project(title="P")
|
||||
decide.assert_not_awaited()
|
||||
assert "inception_hint" in out and "inception_effects" not in out
|
||||
@@ -433,8 +433,8 @@ async def test_create_project_without_inception_args_stays_undecided():
|
||||
async def test_create_project_with_inception_args_decides_via_mcp():
|
||||
p = fake_project(id=5, title="P", inception=None)
|
||||
decided = {"inception": {"via": "mcp", "choices": {}}, "effects": {"systems_seeded": []}}
|
||||
with patch("scribe.mcp.tools.projects.projects_svc.create_project", AsyncMock(return_value=p)), \
|
||||
patch("scribe.mcp.tools.projects.inception_svc.decide", AsyncMock(return_value=decided)) as decide:
|
||||
with patch("grimoire.mcp.tools.projects.projects_svc.create_project", AsyncMock(return_value=p)), \
|
||||
patch("grimoire.mcp.tools.projects.inception_svc.decide", AsyncMock(return_value=decided)) as decide:
|
||||
out = await create_project(title="P", design_system_id=-1, seed_systems=True)
|
||||
kw = decide.await_args.kwargs
|
||||
assert decide.await_args.args[1] == 5 and kw["via"] == "mcp"
|
||||
@@ -444,9 +444,9 @@ async def test_create_project_with_inception_args_decides_via_mcp():
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_decide_project_inception_tool_records_an_inherit_all_decision_when_given_nothing():
|
||||
from scribe.mcp.tools.projects import decide_project_inception
|
||||
from grimoire.mcp.tools.projects import decide_project_inception
|
||||
decided = {"inception": {"via": "mcp"}, "effects": {}}
|
||||
with patch("scribe.mcp.tools.projects.inception_svc.decide", AsyncMock(return_value=decided)) as decide:
|
||||
with patch("grimoire.mcp.tools.projects.inception_svc.decide", AsyncMock(return_value=decided)) as decide:
|
||||
out = await decide_project_inception(project_id=5)
|
||||
assert decide.await_args.kwargs["choices"] == {}
|
||||
assert out["project_id"] == 5 and out["inception"]["via"] == "mcp"
|
||||
@@ -458,13 +458,13 @@ async def test_enter_project_carries_the_inception_ask_only_for_an_undecided_own
|
||||
ask = {"defaults": {}, "ask": "decide", "call": "decide_project_inception(...)"}
|
||||
|
||||
async def run(project):
|
||||
with patch("scribe.mcp.tools.projects.projects_svc.get_project", AsyncMock(return_value=project)), \
|
||||
patch("scribe.mcp.tools.projects.rulebooks_svc.get_applicable_rules", AsyncMock(return_value=applicable)), \
|
||||
patch("scribe.mcp.tools.projects.milestones_svc.get_project_milestone_summary", AsyncMock(return_value=[])), \
|
||||
patch("scribe.mcp.tools.projects.notes_svc.list_notes", AsyncMock(side_effect=[([], 0), ([], 0)])), \
|
||||
patch("scribe.mcp.tools.projects.systems_svc.list_systems", AsyncMock(return_value=[])), \
|
||||
patch("scribe.mcp.tools.projects.systems_tools.bootstrap_systems_ask", AsyncMock(return_value=None)), \
|
||||
patch("scribe.mcp.tools.projects.inception_svc.inception_ask", AsyncMock(return_value=ask)) as asked:
|
||||
with patch("grimoire.mcp.tools.projects.projects_svc.get_project", AsyncMock(return_value=project)), \
|
||||
patch("grimoire.mcp.tools.projects.rulebooks_svc.get_applicable_rules", AsyncMock(return_value=applicable)), \
|
||||
patch("grimoire.mcp.tools.projects.milestones_svc.get_project_milestone_summary", AsyncMock(return_value=[])), \
|
||||
patch("grimoire.mcp.tools.projects.notes_svc.list_notes", AsyncMock(side_effect=[([], 0), ([], 0)])), \
|
||||
patch("grimoire.mcp.tools.projects.systems_svc.list_systems", AsyncMock(return_value=[])), \
|
||||
patch("grimoire.mcp.tools.projects.systems_tools.bootstrap_systems_ask", AsyncMock(return_value=None)), \
|
||||
patch("grimoire.mcp.tools.projects.inception_svc.inception_ask", AsyncMock(return_value=ask)) as asked:
|
||||
return await enter_project(project_id=5), asked
|
||||
|
||||
# Own + undecided → the ask rides along.
|
||||
@@ -479,8 +479,8 @@ async def test_enter_project_carries_the_inception_ask_only_for_an_undecided_own
|
||||
|
||||
|
||||
def test_inception_routes_and_tool_are_registered():
|
||||
from scribe.app import create_app
|
||||
from scribe.mcp.server import build_mcp_server
|
||||
from grimoire.app import create_app
|
||||
from grimoire.mcp.server import build_mcp_server
|
||||
rules = {r.rule for r in create_app().url_map.iter_rules()}
|
||||
assert "/api/projects/<int:project_id>/inception" in rules
|
||||
assert "/api/projects/<int:project_id>/inception/defaults" in rules
|
||||
|
||||
@@ -16,14 +16,14 @@ pytestmark = pytest.mark.usefixtures("_bind_user")
|
||||
|
||||
|
||||
async def _update(owed=None, **kwargs):
|
||||
from scribe.mcp.tools.tasks import update_task
|
||||
from grimoire.mcp.tools.tasks import update_task
|
||||
|
||||
note = MagicMock(id=5, user_id=7, project_id=3, started_at="2026-10-06T09:00:00+00:00")
|
||||
note.to_dict.return_value = {"id": 5}
|
||||
with patch("scribe.mcp.tools.tasks.notes_svc.update_note", AsyncMock(return_value=note)), \
|
||||
patch("scribe.mcp.tools.tasks.systems_tools.attach_systems", AsyncMock()), \
|
||||
patch("scribe.mcp.tools.tasks.placement_svc.attach_placement", AsyncMock()), \
|
||||
patch("scribe.mcp.tools.tasks.family_adoption_svc.owed_since",
|
||||
with patch("grimoire.mcp.tools.tasks.notes_svc.update_note", AsyncMock(return_value=note)), \
|
||||
patch("grimoire.mcp.tools.tasks.systems_tools.attach_systems", AsyncMock()), \
|
||||
patch("grimoire.mcp.tools.tasks.placement_svc.attach_placement", AsyncMock()), \
|
||||
patch("grimoire.mcp.tools.tasks.family_adoption_svc.owed_since",
|
||||
AsyncMock(return_value=list(owed or []))):
|
||||
return await update_task(task_id=5, **kwargs)
|
||||
|
||||
@@ -42,7 +42,7 @@ async def test_other_updates_do_not(kwargs):
|
||||
|
||||
async def test_closing_carries_the_plain_cue_and_no_preference_key_of_its_own():
|
||||
"""The retired key does not come back beside the moment delivery."""
|
||||
from scribe.mcp.tools.tasks import REPORT_BACK_CUE
|
||||
from grimoire.mcp.tools.tasks import REPORT_BACK_CUE
|
||||
|
||||
out = await _update(status="done")
|
||||
assert "reply_preferences" not in out
|
||||
@@ -56,7 +56,7 @@ _OWED = {"idea_id": 9, "idea_title": "Signed APK lane", "project_id": 4,
|
||||
async def test_closing_names_the_owed_adoptions_filed_while_the_task_was_open():
|
||||
"""The finishing moment of family canon: owed family work filed
|
||||
into a project during this task comes back for the report to name."""
|
||||
from scribe.services.family_adoption import OWED_CUE
|
||||
from grimoire.services.family_adoption import OWED_CUE
|
||||
|
||||
out = await _update(owed=[_OWED], status="done")
|
||||
assert out["family_owed"] == [_OWED]
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -5,8 +5,8 @@ from unittest.mock import AsyncMock, MagicMock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
from scribe.mcp._context import _user_id_ctx
|
||||
from scribe.mcp.tools.search import search
|
||||
from grimoire.mcp._context import _user_id_ctx
|
||||
from grimoire.mcp.tools.search import search
|
||||
from tests.helpers import fake_note
|
||||
|
||||
|
||||
@@ -29,7 +29,7 @@ async def test_fable_search_returns_repackaged_results():
|
||||
_user_id_ctx.set(7)
|
||||
fake = fake_note(id=1, title="kafka rebalance", is_task=False, body="HPA details", tags=["ops"])
|
||||
with patch(
|
||||
"scribe.mcp.tools.search.semantic_search_notes",
|
||||
"grimoire.mcp.tools.search.semantic_search_notes",
|
||||
AsyncMock(return_value=[(0.93, fake)]),
|
||||
):
|
||||
out = await search(q="kafka")
|
||||
@@ -65,7 +65,7 @@ async def test_search_shows_the_passage_that_matched_not_the_opening():
|
||||
}
|
||||
return [(0.5, fake)]
|
||||
|
||||
with patch("scribe.mcp.tools.search.semantic_search_notes", _search):
|
||||
with patch("grimoire.mcp.tools.search.semantic_search_notes", _search):
|
||||
out = await search(q="answer")
|
||||
r = out["results"][0]
|
||||
assert r["excerpt"] == "THE ANSWER IS 04775c3."
|
||||
@@ -83,7 +83,7 @@ async def test_a_result_says_whether_its_excerpt_is_the_match_or_the_opening():
|
||||
_user_id_ctx.set(7)
|
||||
fake = fake_note(id=1, title="t", body="short body")
|
||||
with patch(
|
||||
"scribe.mcp.tools.search.semantic_search_notes",
|
||||
"grimoire.mcp.tools.search.semantic_search_notes",
|
||||
AsyncMock(return_value=[(0.5, fake)]),
|
||||
):
|
||||
out = await search(q="x")
|
||||
@@ -98,7 +98,7 @@ async def test_a_long_excerpt_keeps_both_ends_and_says_how_much_went():
|
||||
body = "OPENING. " + ("m" * 3000) + " CLOSING."
|
||||
fake = fake_note(id=1, title="t", body=body)
|
||||
with patch(
|
||||
"scribe.mcp.tools.search.semantic_search_notes",
|
||||
"grimoire.mcp.tools.search.semantic_search_notes",
|
||||
AsyncMock(return_value=[(0.5, fake)]),
|
||||
):
|
||||
out = await search(q="x")
|
||||
@@ -115,7 +115,7 @@ async def test_a_short_record_arrives_whole_and_unmarked():
|
||||
_user_id_ctx.set(7)
|
||||
fake = fake_note(id=1, title="t", body="all of it")
|
||||
with patch(
|
||||
"scribe.mcp.tools.search.semantic_search_notes",
|
||||
"grimoire.mcp.tools.search.semantic_search_notes",
|
||||
AsyncMock(return_value=[(0.5, fake)]),
|
||||
):
|
||||
out = await search(q="x")
|
||||
@@ -134,7 +134,7 @@ async def test_fable_search_content_type_filters_at_service_layer():
|
||||
that already asks this way."""
|
||||
_user_id_ctx.set(7)
|
||||
mock_search = AsyncMock(return_value=[])
|
||||
with patch("scribe.mcp.tools.search.semantic_search_notes", mock_search):
|
||||
with patch("grimoire.mcp.tools.search.semantic_search_notes", mock_search):
|
||||
await search(q="x", content_type="task")
|
||||
assert mock_search.call_args.kwargs["is_task"] is True
|
||||
assert mock_search.call_args.kwargs.get("task_kind") is None
|
||||
@@ -173,7 +173,7 @@ async def test_each_specific_kind_reaches_the_engine_filter_it_names(
|
||||
for a snippet must narrow to snippets, not merely to non-tasks."""
|
||||
_user_id_ctx.set(7)
|
||||
mock_search = AsyncMock(return_value=[])
|
||||
with patch("scribe.mcp.tools.search.semantic_search_notes", mock_search):
|
||||
with patch("grimoire.mcp.tools.search.semantic_search_notes", mock_search):
|
||||
await search(q="x", content_type=content_type)
|
||||
kwargs = mock_search.call_args.kwargs
|
||||
for key, value in expected.items():
|
||||
@@ -183,7 +183,7 @@ async def test_each_specific_kind_reaches_the_engine_filter_it_names(
|
||||
def test_the_vocabulary_is_derived_from_the_facet_table_not_recopied():
|
||||
"""Adding a kind to `_FACETS` is one edit, at this door too. A hand-kept
|
||||
list here lets the agent's search offer fewer kinds than the web's."""
|
||||
from scribe.services.knowledge import FACET_TYPES, content_type_filters
|
||||
from grimoire.services.knowledge import FACET_TYPES, content_type_filters
|
||||
|
||||
for facet in FACET_TYPES:
|
||||
filters = content_type_filters(facet) # raises if a kind is unreachable
|
||||
@@ -203,7 +203,7 @@ async def test_an_unknown_content_type_is_refused_rather_than_returning_nothing(
|
||||
instead of falling through to a filter that matches no row."""
|
||||
_user_id_ctx.set(7)
|
||||
mock_search = AsyncMock(return_value=[])
|
||||
with patch("scribe.mcp.tools.search.semantic_search_notes", mock_search):
|
||||
with patch("grimoire.mcp.tools.search.semantic_search_notes", mock_search):
|
||||
with pytest.raises(ValueError) as err:
|
||||
await search(q="x", content_type="snippets") # plural typo
|
||||
mock_search.assert_not_awaited()
|
||||
@@ -216,8 +216,8 @@ async def test_an_unknown_content_type_is_refused_rather_than_returning_nothing(
|
||||
async def test_the_docstring_names_every_kind_the_tool_accepts():
|
||||
"""The docstring IS the agent-facing contract — a filter an agent
|
||||
has not been told about is unreachable however well it is wired."""
|
||||
from scribe.mcp.tools.search import search as search_tool
|
||||
from scribe.services.knowledge import FACET_TYPES
|
||||
from grimoire.mcp.tools.search import search as search_tool
|
||||
from grimoire.services.knowledge import FACET_TYPES
|
||||
|
||||
doc = search_tool.__doc__ or ""
|
||||
for facet in FACET_TYPES:
|
||||
@@ -242,7 +242,7 @@ async def test_an_explicit_search_reaches_a_lesson_from_any_project():
|
||||
"""
|
||||
_user_id_ctx.set(7)
|
||||
mock_search = AsyncMock(return_value=[])
|
||||
with patch("scribe.mcp.tools.search.semantic_search_notes", mock_search):
|
||||
with patch("grimoire.mcp.tools.search.semantic_search_notes", mock_search):
|
||||
await search(q="x", project_id=3)
|
||||
|
||||
assert mock_search.call_args.kwargs["include_global_kinds"] is True
|
||||
@@ -252,7 +252,7 @@ async def test_an_explicit_search_reaches_a_lesson_from_any_project():
|
||||
async def test_fable_search_limit_is_clamped():
|
||||
_user_id_ctx.set(7)
|
||||
mock_search = AsyncMock(return_value=[])
|
||||
with patch("scribe.mcp.tools.search.semantic_search_notes", mock_search):
|
||||
with patch("grimoire.mcp.tools.search.semantic_search_notes", mock_search):
|
||||
await search(q="x", limit=999)
|
||||
assert mock_search.call_args.kwargs["limit"] == 50
|
||||
|
||||
@@ -273,7 +273,7 @@ async def test_rule_search_scopes_to_the_project_it_is_given(project_id, scope):
|
||||
is asking the whole rulebook — unlike a hook, which speaks unasked."""
|
||||
_user_id_ctx.set(7)
|
||||
found = AsyncMock(return_value=[])
|
||||
with patch("scribe.mcp.tools.search.semantic_search_rules", found):
|
||||
with patch("grimoire.mcp.tools.search.semantic_search_rules", found):
|
||||
await search(q="release tagging", content_type="rule", project_id=project_id)
|
||||
kwargs = found.await_args.kwargs
|
||||
assert {k: kwargs[k] for k in scope} == scope
|
||||
@@ -281,7 +281,7 @@ async def test_rule_search_scopes_to_the_project_it_is_given(project_id, scope):
|
||||
|
||||
|
||||
def test_a_milestone_is_embedded_by_what_it_is_for_then_its_plan():
|
||||
from scribe.services.embeddings import milestone_document
|
||||
from grimoire.services.embeddings import milestone_document
|
||||
|
||||
assert milestone_document("M3", "metadata providers", "## Goal\nx") == (
|
||||
"M3 — metadata providers", "metadata providers\n\n## Goal\nx")
|
||||
@@ -313,9 +313,9 @@ async def test_milestone_search_is_its_own_shape_and_scopes_to_the_project():
|
||||
}
|
||||
return [(0.81, ms)]
|
||||
|
||||
with patch("scribe.mcp.tools.search.semantic_search_milestones",
|
||||
with patch("grimoire.mcp.tools.search.semantic_search_milestones",
|
||||
_milestone_search), \
|
||||
patch("scribe.services.milestones.get_project_milestone_summary", summary):
|
||||
patch("grimoire.services.milestones.get_project_milestone_summary", summary):
|
||||
out = await search(q="book metadata", content_type="milestone", project_id=30)
|
||||
assert seen["project_id"] == 30
|
||||
assert out["results"] == [{
|
||||
@@ -356,7 +356,7 @@ async def test_system_search_is_its_own_shape_and_carries_the_matched_passage():
|
||||
}
|
||||
return [(0.81, fake)]
|
||||
|
||||
with patch("scribe.mcp.tools.search.semantic_search_systems", _system_search):
|
||||
with patch("grimoire.mcp.tools.search.semantic_search_systems", _system_search):
|
||||
out = await search(q="where does retrieval telemetry go?",
|
||||
content_type="system", project_id=2)
|
||||
|
||||
@@ -383,12 +383,12 @@ async def test_system_search_scopes_to_the_project_it_was_given():
|
||||
answer to "where does this belong here?"."""
|
||||
_user_id_ctx.set(7)
|
||||
mock = AsyncMock(return_value=[])
|
||||
with patch("scribe.mcp.tools.search.semantic_search_systems", mock):
|
||||
with patch("grimoire.mcp.tools.search.semantic_search_systems", mock):
|
||||
await search(q="x", content_type="system", project_id=30)
|
||||
assert mock.call_args.kwargs["project_id"] == 30
|
||||
|
||||
mock.reset_mock()
|
||||
with patch("scribe.mcp.tools.search.semantic_search_systems", mock):
|
||||
with patch("grimoire.mcp.tools.search.semantic_search_systems", mock):
|
||||
await search(q="x", content_type="system")
|
||||
assert mock.call_args.kwargs["project_id"] is None
|
||||
|
||||
@@ -400,8 +400,8 @@ async def test_a_system_search_does_not_go_through_the_note_search():
|
||||
_user_id_ctx.set(7)
|
||||
notes = AsyncMock(return_value=[])
|
||||
with (
|
||||
patch("scribe.mcp.tools.search.semantic_search_notes", notes),
|
||||
patch("scribe.mcp.tools.search.semantic_search_systems", AsyncMock(return_value=[])),
|
||||
patch("grimoire.mcp.tools.search.semantic_search_notes", notes),
|
||||
patch("grimoire.mcp.tools.search.semantic_search_systems", AsyncMock(return_value=[])),
|
||||
):
|
||||
await search(q="x", content_type="system")
|
||||
notes.assert_not_awaited()
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -13,11 +13,11 @@ _NO_MATCH = {"duplicate": None, "canonical": None}
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_create_system_returns_dict():
|
||||
with patch("scribe.mcp.tools.systems.current_user_id", return_value=1), \
|
||||
patch("scribe.mcp.tools.systems.systems_svc") as svc:
|
||||
with patch("grimoire.mcp.tools.systems.current_user_id", return_value=1), \
|
||||
patch("grimoire.mcp.tools.systems.systems_svc") as svc:
|
||||
svc.assess_system_name = AsyncMock(return_value=_NO_MATCH)
|
||||
svc.create_system = AsyncMock(return_value=fake_system(name="Reader"))
|
||||
from scribe.mcp.tools.systems import create_system
|
||||
from grimoire.mcp.tools.systems import create_system
|
||||
result = await create_system(project_id=5, name="Reader", description="pdf reader")
|
||||
assert result["name"] == "Reader"
|
||||
# An unmatched name is a project-specific area: created, no offer, no fuss.
|
||||
@@ -26,11 +26,11 @@ async def test_create_system_returns_dict():
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_create_system_no_access_raises():
|
||||
with patch("scribe.mcp.tools.systems.current_user_id", return_value=1), \
|
||||
patch("scribe.mcp.tools.systems.systems_svc") as svc:
|
||||
with patch("grimoire.mcp.tools.systems.current_user_id", return_value=1), \
|
||||
patch("grimoire.mcp.tools.systems.systems_svc") as svc:
|
||||
svc.assess_system_name = AsyncMock(return_value=_NO_MATCH)
|
||||
svc.create_system = AsyncMock(return_value=None)
|
||||
from scribe.mcp.tools.systems import create_system
|
||||
from grimoire.mcp.tools.systems import create_system
|
||||
with pytest.raises(ValueError):
|
||||
await create_system(project_id=5, name="Reader")
|
||||
|
||||
@@ -44,10 +44,10 @@ async def test_create_system_applies_an_exact_area_and_offers_a_similar_one():
|
||||
judgment call, so it is only OFFERED: applying it silently is how a
|
||||
cross-project rule ends up surfacing in the wrong project.
|
||||
"""
|
||||
from scribe.mcp.tools.systems import create_system
|
||||
from grimoire.mcp.tools.systems import create_system
|
||||
|
||||
with patch("scribe.mcp.tools.systems.current_user_id", return_value=1), \
|
||||
patch("scribe.mcp.tools.systems.systems_svc") as svc:
|
||||
with patch("grimoire.mcp.tools.systems.current_user_id", return_value=1), \
|
||||
patch("grimoire.mcp.tools.systems.systems_svc") as svc:
|
||||
svc.assess_system_name = AsyncMock(return_value={
|
||||
"duplicate": None,
|
||||
"canonical": {"id": 3, "name": "CI & Release", "basis": "exact"},
|
||||
@@ -57,8 +57,8 @@ async def test_create_system_applies_an_exact_area_and_offers_a_similar_one():
|
||||
assert svc.create_system.await_args.kwargs["canonical_id"] == 3
|
||||
assert "canonical_note" in exact and "canonical_suggestion" not in exact
|
||||
|
||||
with patch("scribe.mcp.tools.systems.current_user_id", return_value=1), \
|
||||
patch("scribe.mcp.tools.systems.systems_svc") as svc:
|
||||
with patch("grimoire.mcp.tools.systems.current_user_id", return_value=1), \
|
||||
patch("grimoire.mcp.tools.systems.systems_svc") as svc:
|
||||
svc.assess_system_name = AsyncMock(return_value={
|
||||
"duplicate": None,
|
||||
"canonical": {"id": 3, "name": "CI & Release", "basis": "overlap", "score": 0.33},
|
||||
@@ -72,9 +72,9 @@ async def test_create_system_applies_an_exact_area_and_offers_a_similar_one():
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_create_system_duplicate_names_the_existing_one_and_creates_nothing():
|
||||
from scribe.mcp.tools.systems import create_system
|
||||
with patch("scribe.mcp.tools.systems.current_user_id", return_value=1), \
|
||||
patch("scribe.mcp.tools.systems.systems_svc") as svc:
|
||||
from grimoire.mcp.tools.systems import create_system
|
||||
with patch("grimoire.mcp.tools.systems.current_user_id", return_value=1), \
|
||||
patch("grimoire.mcp.tools.systems.systems_svc") as svc:
|
||||
svc.assess_system_name = AsyncMock(return_value={
|
||||
"duplicate": {"id": 4, "name": "Reader"}, "canonical": None,
|
||||
})
|
||||
@@ -90,11 +90,11 @@ async def test_get_system_splits_records_by_kind():
|
||||
issue = fake_task(id=10, task_kind="issue", milestone_id=None)
|
||||
work = fake_task(id=11, milestone_id=None)
|
||||
note = fake_note(id=12, status=None, milestone_id=None)
|
||||
with patch("scribe.mcp.tools.systems.current_user_id", return_value=1), \
|
||||
patch("scribe.mcp.tools.systems.systems_svc") as svc:
|
||||
with patch("grimoire.mcp.tools.systems.current_user_id", return_value=1), \
|
||||
patch("grimoire.mcp.tools.systems.systems_svc") as svc:
|
||||
svc.get_system = AsyncMock(return_value=fake_system(id=3))
|
||||
svc.list_records_for_system = AsyncMock(return_value=[issue, work, note])
|
||||
from scribe.mcp.tools.systems import get_system
|
||||
from grimoire.mcp.tools.systems import get_system
|
||||
result = await get_system(system_id=3)
|
||||
assert [r["id"] for r in result["issues"]] == [10]
|
||||
assert [r["id"] for r in result["tasks"]] == [11]
|
||||
@@ -104,12 +104,12 @@ async def test_get_system_splits_records_by_kind():
|
||||
@pytest.mark.asyncio
|
||||
async def test_path_patterns_reach_the_service_from_both_tools():
|
||||
"""Omitted means unchanged; an empty list is a value — it clears them."""
|
||||
with patch("scribe.mcp.tools.systems.current_user_id", return_value=1), \
|
||||
patch("scribe.mcp.tools.systems.systems_svc") as svc:
|
||||
with patch("grimoire.mcp.tools.systems.current_user_id", return_value=1), \
|
||||
patch("grimoire.mcp.tools.systems.systems_svc") as svc:
|
||||
svc.assess_system_name = AsyncMock(return_value=_NO_MATCH)
|
||||
svc.create_system = AsyncMock(return_value=fake_system(name="Billing"))
|
||||
svc.update_system = AsyncMock(return_value=fake_system(name="Billing"))
|
||||
from scribe.mcp.tools.systems import create_system, update_system
|
||||
from grimoire.mcp.tools.systems import create_system, update_system
|
||||
await create_system(project_id=5, name="Billing", path_patterns=["src/billing"])
|
||||
assert svc.create_system.await_args.kwargs["path_patterns"] == ["src/billing"]
|
||||
await update_system(system_id=1, name="Billing")
|
||||
@@ -120,10 +120,10 @@ async def test_path_patterns_reach_the_service_from_both_tools():
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_update_system_not_found_raises():
|
||||
with patch("scribe.mcp.tools.systems.current_user_id", return_value=1), \
|
||||
patch("scribe.mcp.tools.systems.systems_svc") as svc:
|
||||
with patch("grimoire.mcp.tools.systems.current_user_id", return_value=1), \
|
||||
patch("grimoire.mcp.tools.systems.systems_svc") as svc:
|
||||
svc.update_system = AsyncMock(return_value=None)
|
||||
from scribe.mcp.tools.systems import update_system
|
||||
from grimoire.mcp.tools.systems import update_system
|
||||
with pytest.raises(ValueError):
|
||||
await update_system(system_id=99, name="x")
|
||||
|
||||
@@ -131,13 +131,13 @@ async def test_update_system_not_found_raises():
|
||||
@pytest.mark.asyncio
|
||||
async def test_create_task_issue_sets_kind_provenance_and_systems():
|
||||
note = MagicMock(); note.id = 50; note.to_dict.return_value = {"id": 50, "task_kind": "issue"}
|
||||
with patch("scribe.mcp.tools.tasks.current_user_id", return_value=1), \
|
||||
patch("scribe.mcp.tools.tasks.notes_svc") as notes_svc, \
|
||||
patch("scribe.mcp.tools.tasks.systems_svc") as systems_svc:
|
||||
with patch("grimoire.mcp.tools.tasks.current_user_id", return_value=1), \
|
||||
patch("grimoire.mcp.tools.tasks.notes_svc") as notes_svc, \
|
||||
patch("grimoire.mcp.tools.tasks.systems_svc") as systems_svc:
|
||||
notes_svc.create_note = AsyncMock(return_value=note)
|
||||
systems_svc.set_record_systems = AsyncMock(return_value=[2, 3])
|
||||
systems_svc.list_record_systems = AsyncMock(return_value=[])
|
||||
from scribe.mcp.tools.tasks import create_task
|
||||
from grimoire.mcp.tools.tasks import create_task
|
||||
result = await create_task(title="bug", kind="issue", system_ids=[2, 3], arose_from_id=9)
|
||||
_, kwargs = notes_svc.create_note.call_args
|
||||
assert kwargs["task_kind"] == "issue"
|
||||
@@ -150,9 +150,9 @@ async def test_create_task_issue_sets_kind_provenance_and_systems():
|
||||
async def test_untagged_hint_names_the_projects_systems():
|
||||
sys_a = MagicMock(); sys_a.id = 1; sys_a.name = "workers"
|
||||
sys_b = MagicMock(); sys_b.id = 2; sys_b.name = "scrape-pipeline"
|
||||
with patch("scribe.mcp.tools.systems.systems_svc") as svc:
|
||||
with patch("grimoire.mcp.tools.systems.systems_svc") as svc:
|
||||
svc.list_systems = AsyncMock(return_value=[sys_a, sys_b])
|
||||
from scribe.mcp.tools.systems import untagged_systems_hint
|
||||
from grimoire.mcp.tools.systems import untagged_systems_hint
|
||||
hint = await untagged_systems_hint(1, 5)
|
||||
assert "workers" in hint and "scrape-pipeline" in hint
|
||||
assert "system_ids" in hint
|
||||
@@ -161,9 +161,9 @@ async def test_untagged_hint_names_the_projects_systems():
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_untagged_hint_zero_systems_prompts_first_create_and_fails_open():
|
||||
from scribe.mcp.tools.systems import untagged_systems_hint
|
||||
with patch("scribe.mcp.tools.systems.systems_svc") as svc, \
|
||||
patch("scribe.mcp.tools.systems.notes_svc") as notes:
|
||||
from grimoire.mcp.tools.systems import untagged_systems_hint
|
||||
with patch("grimoire.mcp.tools.systems.systems_svc") as svc, \
|
||||
patch("grimoire.mcp.tools.systems.notes_svc") as notes:
|
||||
# Zero Systems in a YOUNG project (below the bootstrap threshold):
|
||||
# the mild question stays proportionate — it must prompt the FIRST
|
||||
# create_system, not go silent.
|
||||
@@ -171,7 +171,7 @@ async def test_untagged_hint_zero_systems_prompts_first_create_and_fails_open():
|
||||
notes.list_notes = AsyncMock(return_value=([], 3))
|
||||
hint = await untagged_systems_hint(1, 5)
|
||||
assert "no Systems yet" in hint and "create_system" in hint
|
||||
with patch("scribe.mcp.tools.systems.systems_svc") as svc:
|
||||
with patch("grimoire.mcp.tools.systems.systems_svc") as svc:
|
||||
# A hint must never break a create — DB failure degrades to no hint.
|
||||
svc.list_systems = AsyncMock(side_effect=RuntimeError("db down"))
|
||||
assert await untagged_systems_hint(1, 5) is None
|
||||
@@ -184,10 +184,10 @@ async def test_untagged_hint_escalates_in_a_mature_zero_systems_project():
|
||||
project's OWN evidence — record count and recent titles — and demand a
|
||||
concrete deliverable, because that is the property separating the nudges
|
||||
that convert from the prose that doesn't."""
|
||||
from scribe.mcp.tools.systems import untagged_systems_hint
|
||||
from grimoire.mcp.tools.systems import untagged_systems_hint
|
||||
recent = [fake_note(title="Fix scrape retry backoff"), fake_note(title="Worker pool sizing")]
|
||||
with patch("scribe.mcp.tools.systems.systems_svc") as svc, \
|
||||
patch("scribe.mcp.tools.systems.notes_svc") as notes:
|
||||
with patch("grimoire.mcp.tools.systems.systems_svc") as svc, \
|
||||
patch("grimoire.mcp.tools.systems.notes_svc") as notes:
|
||||
svc.list_systems = AsyncMock(return_value=[])
|
||||
notes.list_notes = AsyncMock(return_value=(recent, 282))
|
||||
# The standard names come from the GLOBAL catalog,
|
||||
@@ -218,10 +218,10 @@ async def test_bootstrap_ask_still_asks_when_the_catalog_is_unreachable():
|
||||
catalog read fails (or an install has an empty one), the ask must still
|
||||
carry the project's evidence and demand the same deliverable — degrading to
|
||||
a weaker nudge is acceptable, going silent is not."""
|
||||
from scribe.mcp.tools.systems import untagged_systems_hint
|
||||
from grimoire.mcp.tools.systems import untagged_systems_hint
|
||||
recent = [fake_note(title="Fix scrape retry backoff")]
|
||||
with patch("scribe.mcp.tools.systems.systems_svc") as svc, \
|
||||
patch("scribe.mcp.tools.systems.notes_svc") as notes:
|
||||
with patch("grimoire.mcp.tools.systems.systems_svc") as svc, \
|
||||
patch("grimoire.mcp.tools.systems.notes_svc") as notes:
|
||||
svc.list_systems = AsyncMock(return_value=[])
|
||||
notes.list_notes = AsyncMock(return_value=(recent, 282))
|
||||
svc.standard_systems = AsyncMock(side_effect=RuntimeError("db down"))
|
||||
@@ -233,12 +233,12 @@ async def test_bootstrap_ask_still_asks_when_the_catalog_is_unreachable():
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_bootstrap_ask_stays_quiet_below_threshold_and_fails_open():
|
||||
from scribe.mcp.tools import systems as tools
|
||||
with patch("scribe.mcp.tools.systems.notes_svc") as notes:
|
||||
from grimoire.mcp.tools import systems as tools
|
||||
with patch("grimoire.mcp.tools.systems.notes_svc") as notes:
|
||||
notes.list_notes = AsyncMock(
|
||||
return_value=([], tools._BOOTSTRAP_MIN_RECORDS - 1))
|
||||
assert await tools.bootstrap_systems_ask(1, 5) is None
|
||||
with patch("scribe.mcp.tools.systems.notes_svc") as notes:
|
||||
with patch("grimoire.mcp.tools.systems.notes_svc") as notes:
|
||||
# The record count is decoration on a hint on a create — three layers
|
||||
# deep, nothing there may break the call.
|
||||
notes.list_notes = AsyncMock(side_effect=RuntimeError("db down"))
|
||||
@@ -249,10 +249,10 @@ async def test_bootstrap_ask_stays_quiet_below_threshold_and_fails_open():
|
||||
async def test_populated_vocabulary_never_counts_records():
|
||||
"""The bootstrap query is zero-state-only: once Systems exist the hint
|
||||
must not spend a count query per untagged record."""
|
||||
from scribe.mcp.tools.systems import untagged_systems_hint
|
||||
from grimoire.mcp.tools.systems import untagged_systems_hint
|
||||
sys_a = MagicMock(); sys_a.id = 1; sys_a.name = "workers"
|
||||
with patch("scribe.mcp.tools.systems.systems_svc") as svc, \
|
||||
patch("scribe.mcp.tools.systems.notes_svc") as notes:
|
||||
with patch("grimoire.mcp.tools.systems.systems_svc") as svc, \
|
||||
patch("grimoire.mcp.tools.systems.notes_svc") as notes:
|
||||
svc.list_systems = AsyncMock(return_value=[sys_a])
|
||||
notes.list_notes = AsyncMock()
|
||||
hint = await untagged_systems_hint(1, 5)
|
||||
@@ -271,15 +271,15 @@ async def test_populated_vocabulary_never_counts_records():
|
||||
async def test_create_task_untagged_in_project_carries_the_question():
|
||||
note = MagicMock(); note.id = 60; note.to_dict.return_value = {"id": 60}
|
||||
sys_a = MagicMock(); sys_a.id = 4; sys_a.name = "plugin-hooks"
|
||||
with patch("scribe.mcp.tools.tasks.current_user_id", return_value=1), \
|
||||
patch("scribe.mcp.tools.tasks.notes_svc") as notes_svc, \
|
||||
patch("scribe.mcp.tools.tasks.dedup_svc") as dedup_svc, \
|
||||
patch("scribe.mcp.tools.systems.systems_svc") as seam_svc:
|
||||
with patch("grimoire.mcp.tools.tasks.current_user_id", return_value=1), \
|
||||
patch("grimoire.mcp.tools.tasks.notes_svc") as notes_svc, \
|
||||
patch("grimoire.mcp.tools.tasks.dedup_svc") as dedup_svc, \
|
||||
patch("grimoire.mcp.tools.systems.systems_svc") as seam_svc:
|
||||
notes_svc.create_note = AsyncMock(return_value=note)
|
||||
dedup_svc.find_duplicate_note = AsyncMock(return_value=None)
|
||||
seam_svc.list_record_systems = AsyncMock(return_value=[])
|
||||
seam_svc.list_systems = AsyncMock(return_value=[sys_a])
|
||||
from scribe.mcp.tools.tasks import create_task
|
||||
from grimoire.mcp.tools.tasks import create_task
|
||||
result = await create_task(title="untagged", project_id=5)
|
||||
assert "plugin-hooks" in result["systems_hint"]
|
||||
|
||||
@@ -291,16 +291,16 @@ async def test_create_task_tagged_shows_systems_and_projectless_gets_neither():
|
||||
# mutation leak into the second create's response.
|
||||
note.to_dict.side_effect = lambda: {"id": 61}
|
||||
tagged_sys = MagicMock(); tagged_sys.to_dict.return_value = {"id": 4, "name": "plugin-hooks"}
|
||||
with patch("scribe.mcp.tools.tasks.current_user_id", return_value=1), \
|
||||
patch("scribe.mcp.tools.tasks.notes_svc") as notes_svc, \
|
||||
patch("scribe.mcp.tools.tasks.dedup_svc") as dedup_svc, \
|
||||
patch("scribe.mcp.tools.tasks.systems_svc") as systems_svc, \
|
||||
patch("scribe.mcp.tools.systems.systems_svc") as seam_svc:
|
||||
with patch("grimoire.mcp.tools.tasks.current_user_id", return_value=1), \
|
||||
patch("grimoire.mcp.tools.tasks.notes_svc") as notes_svc, \
|
||||
patch("grimoire.mcp.tools.tasks.dedup_svc") as dedup_svc, \
|
||||
patch("grimoire.mcp.tools.tasks.systems_svc") as systems_svc, \
|
||||
patch("grimoire.mcp.tools.systems.systems_svc") as seam_svc:
|
||||
notes_svc.create_note = AsyncMock(return_value=note)
|
||||
dedup_svc.find_duplicate_note = AsyncMock(return_value=None)
|
||||
systems_svc.set_record_systems = AsyncMock()
|
||||
seam_svc.list_record_systems = AsyncMock(return_value=[tagged_sys])
|
||||
from scribe.mcp.tools.tasks import create_task
|
||||
from grimoire.mcp.tools.tasks import create_task
|
||||
tagged = await create_task(title="tagged", project_id=5, system_ids=[4])
|
||||
seam_svc.list_record_systems = AsyncMock(return_value=[])
|
||||
orphan = await create_task(title="orphan", project_id=0)
|
||||
@@ -318,15 +318,15 @@ async def test_get_task_shows_the_records_systems_on_read():
|
||||
note.deleted_at = None; note.parent_id = None
|
||||
note.to_dict.return_value = {"id": 70, "task_kind": "work"}
|
||||
tagged_sys = MagicMock(); tagged_sys.to_dict.return_value = {"id": 2, "name": "exporter"}
|
||||
with patch("scribe.mcp.tools.tasks.current_user_id", return_value=1), \
|
||||
patch("scribe.mcp.tools.tasks.notes_svc") as notes_svc, \
|
||||
patch("scribe.mcp.tools.tasks.access_svc") as access_svc, \
|
||||
patch("scribe.mcp.tools.tasks.record_pulled"), \
|
||||
patch("scribe.mcp.tools.systems.systems_svc") as seam_svc:
|
||||
with patch("grimoire.mcp.tools.tasks.current_user_id", return_value=1), \
|
||||
patch("grimoire.mcp.tools.tasks.notes_svc") as notes_svc, \
|
||||
patch("grimoire.mcp.tools.tasks.access_svc") as access_svc, \
|
||||
patch("grimoire.mcp.tools.tasks.record_pulled"), \
|
||||
patch("grimoire.mcp.tools.systems.systems_svc") as seam_svc:
|
||||
notes_svc.get_note_for_user = AsyncMock(return_value=(note, "owner"))
|
||||
access_svc.describe_provenance = AsyncMock(return_value={})
|
||||
seam_svc.list_record_systems = AsyncMock(return_value=[tagged_sys])
|
||||
from scribe.mcp.tools.tasks import get_task
|
||||
from grimoire.mcp.tools.tasks import get_task
|
||||
result = await get_task(task_id=70)
|
||||
assert result["systems"] == [{"id": 2, "name": "exporter"}]
|
||||
|
||||
@@ -336,11 +336,11 @@ async def test_add_task_log_on_untagged_project_task_asks_the_question():
|
||||
"""Logging work IS working in some area — the strongest moment to ask."""
|
||||
log = MagicMock(); log.to_dict.return_value = {"id": 9, "task_id": 70}
|
||||
task = MagicMock(); task.user_id = 1; task.project_id = 5
|
||||
with patch("scribe.mcp.tools.tasks.current_user_id", return_value=1), \
|
||||
patch("scribe.mcp.tools.tasks.task_logs_svc") as logs_svc, \
|
||||
patch("scribe.mcp.tools.tasks.notes_svc") as notes_svc, \
|
||||
patch("scribe.mcp.tools.systems.systems_svc") as seam_svc, \
|
||||
patch("scribe.mcp.tools.systems.notes_svc") as seam_notes:
|
||||
with patch("grimoire.mcp.tools.tasks.current_user_id", return_value=1), \
|
||||
patch("grimoire.mcp.tools.tasks.task_logs_svc") as logs_svc, \
|
||||
patch("grimoire.mcp.tools.tasks.notes_svc") as notes_svc, \
|
||||
patch("grimoire.mcp.tools.systems.systems_svc") as seam_svc, \
|
||||
patch("grimoire.mcp.tools.systems.notes_svc") as seam_notes:
|
||||
logs_svc.create_log = AsyncMock(return_value=log)
|
||||
notes_svc.get_note_for_user = AsyncMock(return_value=(task, "owner"))
|
||||
seam_svc.list_record_systems = AsyncMock(return_value=[])
|
||||
@@ -348,6 +348,6 @@ async def test_add_task_log_on_untagged_project_task_asks_the_question():
|
||||
# Young project: below the bootstrap threshold, so the mild
|
||||
# question is the expected form here.
|
||||
seam_notes.list_notes = AsyncMock(return_value=([], 2))
|
||||
from scribe.mcp.tools.tasks import add_task_log
|
||||
from grimoire.mcp.tools.tasks import add_task_log
|
||||
result = await add_task_log(task_id=70, content="progress")
|
||||
assert "no Systems yet" in result["systems_hint"]
|
||||
|
||||
@@ -7,7 +7,7 @@ from unittest.mock import AsyncMock, MagicMock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
from scribe.mcp.tools.tags import list_tags, _aggregate_tag_counts
|
||||
from grimoire.mcp.tools.tags import list_tags, _aggregate_tag_counts
|
||||
from tests.helpers import make_mock_session
|
||||
|
||||
|
||||
@@ -35,7 +35,7 @@ async def test_fable_list_tags_returns_sorted_by_count_desc():
|
||||
mock_session = make_mock_session()
|
||||
mock_session.execute = AsyncMock(return_value=mock_result)
|
||||
mock_ctx = MagicMock(return_value=mock_session)
|
||||
with patch("scribe.mcp.tools.tags.async_session", mock_ctx):
|
||||
with patch("grimoire.mcp.tools.tags.async_session", mock_ctx):
|
||||
out = await list_tags()
|
||||
assert out["tags"][0] == {"tag": "a", "count": 3}
|
||||
assert out["tags"][1] == {"tag": "b", "count": 1}
|
||||
@@ -49,7 +49,7 @@ async def test_fable_list_tags_clamps_limit():
|
||||
mock_session = make_mock_session()
|
||||
mock_session.execute = AsyncMock(return_value=mock_result)
|
||||
mock_ctx = MagicMock(return_value=mock_session)
|
||||
with patch("scribe.mcp.tools.tags.async_session", mock_ctx):
|
||||
with patch("grimoire.mcp.tools.tags.async_session", mock_ctx):
|
||||
# No exception — just exercises the clamping branch
|
||||
out = await list_tags(limit=99999)
|
||||
assert out["total"] == 0
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user