refactor: rename package fabledassistant -> scribe (code-only)
Renames src/fabledassistant -> src/scribe and all imports, plus the default DB name and DB user/password (fabled -> scribe) in config + compose. 952 refs / 154 files. Reverses the old 'internal name stays fabledassistant' convention. Code-only: live databases are still physically named 'fabledassistant'. Deployed environments must set POSTGRES_DB / POSTGRES_USER (or rename the DB) since the defaults now resolve to 'scribe'. Repo (FabledScribe), git host (fabledsword), MCP (fabled-git) and the image name (fabledscribe) are intentionally unchanged. ruff check src/ clean locally; CI (typecheck + pytest) is the gate. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
+10
-10
@@ -4,7 +4,7 @@ from unittest.mock import AsyncMock, MagicMock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
from fabledassistant.services.api_keys import (
|
||||
from scribe.services.api_keys import (
|
||||
_hash_key,
|
||||
_key_prefix,
|
||||
generate_key,
|
||||
@@ -44,7 +44,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("fabledassistant.services.api_keys.async_session") as mock_session_ctx:
|
||||
with patch("scribe.services.api_keys.async_session") as mock_session_ctx:
|
||||
mock_session = AsyncMock()
|
||||
mock_session.__aenter__ = AsyncMock(return_value=mock_session)
|
||||
mock_session.__aexit__ = AsyncMock(return_value=False)
|
||||
@@ -57,7 +57,7 @@ async def test_create_api_key_returns_full_key():
|
||||
mock_session_ctx.return_value = mock_session
|
||||
|
||||
# Patch the select result
|
||||
with patch("fabledassistant.services.api_keys.ApiKey") as mock_model:
|
||||
with patch("scribe.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")
|
||||
|
||||
@@ -66,7 +66,7 @@ async def test_create_api_key_returns_full_key():
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_lookup_key_returns_none_for_unknown():
|
||||
with patch("fabledassistant.services.api_keys.async_session") as mock_session_ctx:
|
||||
with patch("scribe.services.api_keys.async_session") as mock_session_ctx:
|
||||
mock_session = AsyncMock()
|
||||
mock_session.__aenter__ = AsyncMock(return_value=mock_session)
|
||||
mock_session.__aexit__ = AsyncMock(return_value=False)
|
||||
@@ -112,15 +112,15 @@ async def test_read_only_key_blocked_on_post():
|
||||
from quart import jsonify
|
||||
return jsonify({"ok": True})
|
||||
|
||||
from fabledassistant.auth import _check_auth
|
||||
from scribe.auth import _check_auth
|
||||
wrapped = _check_auth(dummy)
|
||||
|
||||
async with app.test_request_context(
|
||||
"/test", method="POST",
|
||||
headers={"Authorization": "Bearer fmcp_readonly"},
|
||||
):
|
||||
with patch("fabledassistant.auth.lookup_key", AsyncMock(return_value=fake_key)), \
|
||||
patch("fabledassistant.auth.get_user_by_id", AsyncMock(return_value=fake_user)):
|
||||
with patch("scribe.auth.lookup_key", AsyncMock(return_value=fake_key)), \
|
||||
patch("scribe.auth.get_user_by_id", AsyncMock(return_value=fake_user)):
|
||||
resp = await wrapped()
|
||||
assert resp[1] == 403
|
||||
|
||||
@@ -149,15 +149,15 @@ async def test_bearer_token_valid_write_key_passes():
|
||||
from quart import jsonify
|
||||
return jsonify({"ok": True})
|
||||
|
||||
from fabledassistant.auth import _check_auth
|
||||
from scribe.auth import _check_auth
|
||||
wrapped = _check_auth(dummy)
|
||||
|
||||
async with app.test_request_context(
|
||||
"/test", method="POST",
|
||||
headers={"Authorization": "Bearer fmcp_writekey"},
|
||||
):
|
||||
with patch("fabledassistant.auth.lookup_key", AsyncMock(return_value=fake_key)), \
|
||||
patch("fabledassistant.auth.get_user_by_id", AsyncMock(return_value=fake_user)):
|
||||
with patch("scribe.auth.lookup_key", AsyncMock(return_value=fake_key)), \
|
||||
patch("scribe.auth.get_user_by_id", AsyncMock(return_value=fake_user)):
|
||||
resp = await wrapped()
|
||||
|
||||
# Should not be a tuple (no error response)
|
||||
|
||||
@@ -7,7 +7,7 @@ from unittest.mock import AsyncMock, MagicMock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
from fabledassistant.services.embeddings import (
|
||||
from scribe.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(
|
||||
"fabledassistant.services.embeddings._get_model",
|
||||
"scribe.services.embeddings._get_model",
|
||||
AsyncMock(return_value=fake_embedder),
|
||||
):
|
||||
out = await get_embedding("hello world")
|
||||
@@ -71,7 +71,7 @@ async def test_get_embedding_ignores_legacy_model_param():
|
||||
fake_embedder = MagicMock()
|
||||
fake_embedder.embed = MagicMock(return_value=iter([fake_vec]))
|
||||
with patch(
|
||||
"fabledassistant.services.embeddings._get_model",
|
||||
"scribe.services.embeddings._get_model",
|
||||
AsyncMock(return_value=fake_embedder),
|
||||
):
|
||||
out = await get_embedding("x", model="ignored-model-name")
|
||||
@@ -83,7 +83,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(
|
||||
"fabledassistant.services.embeddings._get_model",
|
||||
"scribe.services.embeddings._get_model",
|
||||
AsyncMock(side_effect=RuntimeError("model load failed")),
|
||||
):
|
||||
with pytest.raises(RuntimeError, match="model load failed"):
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
def test_event_model_has_new_columns():
|
||||
from fabledassistant.models.event import Event
|
||||
from scribe.models.event import Event
|
||||
cols = {c.key for c in Event.__table__.columns}
|
||||
assert "caldav_uid" in cols
|
||||
assert "color" in cols
|
||||
|
||||
|
||||
def test_event_to_dict_includes_new_fields():
|
||||
from fabledassistant.models.event import Event
|
||||
from scribe.models.event import Event
|
||||
from datetime import datetime, timezone
|
||||
e = Event(
|
||||
user_id=1, uid="test-uid", title="Test",
|
||||
@@ -21,6 +21,6 @@ def test_event_to_dict_includes_new_fields():
|
||||
def test_caldav_create_event_accepts_uid_param():
|
||||
"""caldav.create_event signature must accept an optional uid param."""
|
||||
import inspect
|
||||
from fabledassistant.services.caldav import create_event
|
||||
from scribe.services.caldav import create_event
|
||||
sig = inspect.signature(create_event)
|
||||
assert "uid" in sig.parameters
|
||||
|
||||
@@ -9,19 +9,19 @@ tests in test_events_service.py.
|
||||
|
||||
def test_events_blueprint_registered():
|
||||
"""events_bp must be importable and have the correct name."""
|
||||
from fabledassistant.routes.events import events_bp
|
||||
from scribe.routes.events import events_bp
|
||||
assert events_bp.name == "events"
|
||||
assert events_bp.url_prefix == "/api/events"
|
||||
|
||||
|
||||
def test_events_blueprint_has_five_routes():
|
||||
"""Blueprint must declare routes for GET/POST '' and GET/PATCH/DELETE '/<id>'."""
|
||||
from fabledassistant.routes.events import events_bp
|
||||
from scribe.routes.events import events_bp
|
||||
methods_by_rule: dict[str, set[str]] = {}
|
||||
for rule in events_bp.deferred_functions:
|
||||
pass # deferred; inspect via url_map after binding
|
||||
# Import routes module to confirm all 5 view functions exist
|
||||
from fabledassistant.routes import events as events_module
|
||||
from scribe.routes import events as events_module
|
||||
assert callable(events_module.list_events)
|
||||
assert callable(events_module.create_event)
|
||||
assert callable(events_module.get_event)
|
||||
@@ -31,7 +31,7 @@ def test_events_blueprint_has_five_routes():
|
||||
|
||||
def test_events_blueprint_registered_in_app():
|
||||
"""events_bp must be registered in the app factory."""
|
||||
from fabledassistant.app import create_app
|
||||
from scribe.app import create_app
|
||||
app = create_app()
|
||||
# Check the blueprint is present in the app's blueprints dict
|
||||
assert "events" in app.blueprints
|
||||
@@ -43,7 +43,7 @@ def test_events_service_ownership_enforced_on_get():
|
||||
# The service returns None when the event belongs to a different user,
|
||||
# and the route converts that to a 404 response.
|
||||
import inspect
|
||||
from fabledassistant.services import events as events_svc
|
||||
from scribe.services import events as events_svc
|
||||
sig = inspect.signature(events_svc.get_event)
|
||||
assert "user_id" in sig.parameters
|
||||
assert "event_id" in sig.parameters
|
||||
@@ -52,7 +52,7 @@ def test_events_service_ownership_enforced_on_get():
|
||||
def test_events_service_ownership_enforced_on_update():
|
||||
"""update_event takes user_id — route passes current user's id."""
|
||||
import inspect
|
||||
from fabledassistant.services import events as events_svc
|
||||
from scribe.services import events as events_svc
|
||||
sig = inspect.signature(events_svc.update_event)
|
||||
assert "user_id" in sig.parameters
|
||||
assert "event_id" in sig.parameters
|
||||
@@ -61,7 +61,7 @@ def test_events_service_ownership_enforced_on_update():
|
||||
def test_events_service_ownership_enforced_on_delete():
|
||||
"""delete_event takes user_id — route verifies ownership before deleting."""
|
||||
import inspect
|
||||
from fabledassistant.services import events as events_svc
|
||||
from scribe.services import events as events_svc
|
||||
sig = inspect.signature(events_svc.delete_event)
|
||||
assert "user_id" in sig.parameters
|
||||
assert "event_id" in sig.parameters
|
||||
|
||||
@@ -49,10 +49,10 @@ def _make_mock_event(id=1, user_id=1, uid="uid-abc", title="Meeting",
|
||||
@pytest.mark.asyncio
|
||||
async def test_create_event_stores_to_db():
|
||||
mock_session = _make_mock_session()
|
||||
with patch("fabledassistant.services.events.async_session") as mock_cls, \
|
||||
patch("fabledassistant.services.events.asyncio.create_task") as mock_task:
|
||||
with patch("scribe.services.events.async_session") as mock_cls, \
|
||||
patch("scribe.services.events.asyncio.create_task") as mock_task:
|
||||
mock_cls.return_value = mock_session
|
||||
from fabledassistant.services.events import create_event
|
||||
from scribe.services.events import create_event
|
||||
result = await create_event(
|
||||
user_id=1,
|
||||
title="Dentist",
|
||||
@@ -72,9 +72,9 @@ async def test_find_events_by_query_returns_ilike_results():
|
||||
mock_result.scalars.return_value.all.return_value = [mock_event]
|
||||
mock_session.execute = AsyncMock(return_value=mock_result)
|
||||
|
||||
with patch("fabledassistant.services.events.async_session") as mock_cls:
|
||||
with patch("scribe.services.events.async_session") as mock_cls:
|
||||
mock_cls.return_value = mock_session
|
||||
from fabledassistant.services.events import find_events_by_query
|
||||
from scribe.services.events import find_events_by_query
|
||||
results = await find_events_by_query(user_id=1, query="meeting")
|
||||
assert len(results) == 1
|
||||
assert results[0].title == "Team Meeting"
|
||||
@@ -88,9 +88,9 @@ async def test_list_events_returns_events_in_range():
|
||||
mock_result.scalars.return_value.all.return_value = [mock_event]
|
||||
mock_session.execute = AsyncMock(return_value=mock_result)
|
||||
|
||||
with patch("fabledassistant.services.events.async_session") as mock_cls:
|
||||
with patch("scribe.services.events.async_session") as mock_cls:
|
||||
mock_cls.return_value = mock_session
|
||||
from fabledassistant.services.events import list_events
|
||||
from scribe.services.events import list_events
|
||||
results = await list_events(
|
||||
user_id=1,
|
||||
date_from=datetime(2026, 3, 1, tzinfo=timezone.utc),
|
||||
@@ -107,10 +107,10 @@ async def test_delete_event_fires_caldav_push_when_uid_set():
|
||||
mock_result.scalar_one_or_none.return_value = mock_event
|
||||
mock_session.execute = AsyncMock(return_value=mock_result)
|
||||
|
||||
with patch("fabledassistant.services.events.async_session") as mock_cls, \
|
||||
patch("fabledassistant.services.events.asyncio.create_task") as mock_task:
|
||||
with patch("scribe.services.events.async_session") as mock_cls, \
|
||||
patch("scribe.services.events.asyncio.create_task") as mock_task:
|
||||
mock_cls.return_value = mock_session
|
||||
from fabledassistant.services.events import delete_event
|
||||
from scribe.services.events import delete_event
|
||||
await delete_event(user_id=1, event_id=1)
|
||||
# Push task fired because caldav_uid is set
|
||||
assert mock_task.called
|
||||
@@ -124,10 +124,10 @@ async def test_update_event_fires_caldav_push():
|
||||
mock_result.scalar_one_or_none.return_value = mock_event
|
||||
mock_session.execute = AsyncMock(return_value=mock_result)
|
||||
|
||||
with patch("fabledassistant.services.events.async_session") as mock_cls, \
|
||||
patch("fabledassistant.services.events.asyncio.create_task") as mock_task:
|
||||
with patch("scribe.services.events.async_session") as mock_cls, \
|
||||
patch("scribe.services.events.asyncio.create_task") as mock_task:
|
||||
mock_cls.return_value = mock_session
|
||||
from fabledassistant.services.events import update_event
|
||||
from scribe.services.events import update_event
|
||||
await update_event(user_id=1, event_id=1, title="Updated Title")
|
||||
assert mock_task.called
|
||||
|
||||
@@ -137,7 +137,7 @@ async def test_update_event_fires_caldav_push():
|
||||
|
||||
def test_normalize_duration_from_end_dt():
|
||||
"""end_dt sugar converts to a positive minute count anchored on start."""
|
||||
from fabledassistant.services.events import _normalize_duration
|
||||
from scribe.services.events import _normalize_duration
|
||||
start = datetime(2026, 5, 1, 8, 0, tzinfo=timezone.utc)
|
||||
end = datetime(2026, 5, 1, 9, 30, tzinfo=timezone.utc)
|
||||
assert _normalize_duration(start_dt=start, end_dt=end, duration_minutes=None) == 90
|
||||
@@ -147,7 +147,7 @@ def test_normalize_duration_zero_is_valid_point_event():
|
||||
"""end_dt == start_dt → duration 0. The point-with-zero-duration case
|
||||
is rare but legal (e.g. an instant marker); the duration model treats
|
||||
it the same as duration None for display purposes."""
|
||||
from fabledassistant.services.events import _normalize_duration
|
||||
from scribe.services.events import _normalize_duration
|
||||
same = datetime(2026, 5, 1, 8, 0, tzinfo=timezone.utc)
|
||||
assert _normalize_duration(start_dt=same, end_dt=same, duration_minutes=None) == 0
|
||||
|
||||
@@ -158,7 +158,7 @@ def test_normalize_duration_rejects_end_before_start():
|
||||
via a CHECK constraint, but write-path callers still get a
|
||||
helpful ValueError if they construct an inconsistent (start, end)
|
||||
pair via the end_dt sugar."""
|
||||
from fabledassistant.services.events import _normalize_duration
|
||||
from scribe.services.events import _normalize_duration
|
||||
start = datetime(2026, 5, 1, 12, 0, tzinfo=timezone.utc)
|
||||
end_before = datetime(2026, 3, 30, 12, 0, tzinfo=timezone.utc)
|
||||
with pytest.raises(ValueError, match="at or after start_dt"):
|
||||
@@ -171,7 +171,7 @@ def test_normalize_duration_rejects_negative_duration():
|
||||
"""Direct duration_minutes < 0 is rejected. Mirrors the DB CHECK
|
||||
constraint at the service boundary so callers get a clean error
|
||||
rather than a constraint violation from psycopg."""
|
||||
from fabledassistant.services.events import _normalize_duration
|
||||
from scribe.services.events import _normalize_duration
|
||||
start = datetime(2026, 5, 1, 12, 0, tzinfo=timezone.utc)
|
||||
with pytest.raises(ValueError, match="must be >= 0"):
|
||||
_normalize_duration(start_dt=start, end_dt=None, duration_minutes=-15)
|
||||
@@ -180,7 +180,7 @@ def test_normalize_duration_rejects_negative_duration():
|
||||
def test_normalize_duration_rejects_inconsistent_end_and_duration():
|
||||
"""If a caller passes both end_dt AND duration_minutes that disagree,
|
||||
the inconsistency is surfaced rather than silently picking one."""
|
||||
from fabledassistant.services.events import _normalize_duration
|
||||
from scribe.services.events import _normalize_duration
|
||||
start = datetime(2026, 5, 1, 12, 0, tzinfo=timezone.utc)
|
||||
end = datetime(2026, 5, 1, 13, 0, tzinfo=timezone.utc) # implies 60 min
|
||||
with pytest.raises(ValueError, match="implies 60 minutes"):
|
||||
@@ -191,7 +191,7 @@ def test_normalize_duration_rejects_inconsistent_end_and_duration():
|
||||
|
||||
def test_normalize_duration_none_for_open_ended():
|
||||
"""Both inputs None → None duration (open-ended event)."""
|
||||
from fabledassistant.services.events import _normalize_duration
|
||||
from scribe.services.events import _normalize_duration
|
||||
start = datetime(2026, 5, 1, 12, 0, tzinfo=timezone.utc)
|
||||
assert _normalize_duration(
|
||||
start_dt=start, end_dt=None, duration_minutes=None,
|
||||
@@ -202,7 +202,7 @@ def test_normalize_duration_none_for_open_ended():
|
||||
async def test_create_event_rejects_end_before_start():
|
||||
"""Service-level rejection — same scenario as the prod bug, surfaced
|
||||
cleanly for tool / route callers via ValueError."""
|
||||
from fabledassistant.services.events import create_event
|
||||
from scribe.services.events import create_event
|
||||
start = datetime(2026, 5, 1, 12, 0, tzinfo=timezone.utc)
|
||||
end_before = datetime(2026, 3, 30, 12, 0, tzinfo=timezone.utc)
|
||||
with pytest.raises(ValueError, match="at or after start_dt"):
|
||||
@@ -225,10 +225,10 @@ async def test_update_event_preserves_duration_when_only_start_changes():
|
||||
mock_result.scalar_one_or_none.return_value = mock_event
|
||||
mock_session.execute = AsyncMock(return_value=mock_result)
|
||||
|
||||
with patch("fabledassistant.services.events.async_session") as mock_cls, \
|
||||
patch("fabledassistant.services.events.asyncio.create_task"):
|
||||
with patch("scribe.services.events.async_session") as mock_cls, \
|
||||
patch("scribe.services.events.asyncio.create_task"):
|
||||
mock_cls.return_value = mock_session
|
||||
from fabledassistant.services.events import update_event
|
||||
from scribe.services.events import update_event
|
||||
# Move start to 12:00; effective end becomes 13:00 automatically.
|
||||
result = await update_event(
|
||||
user_id=1, event_id=1,
|
||||
@@ -251,10 +251,10 @@ async def test_update_event_clearing_end_dt_clears_duration():
|
||||
mock_result.scalar_one_or_none.return_value = mock_event
|
||||
mock_session.execute = AsyncMock(return_value=mock_result)
|
||||
|
||||
with patch("fabledassistant.services.events.async_session") as mock_cls, \
|
||||
patch("fabledassistant.services.events.asyncio.create_task"):
|
||||
with patch("scribe.services.events.async_session") as mock_cls, \
|
||||
patch("scribe.services.events.asyncio.create_task"):
|
||||
mock_cls.return_value = mock_session
|
||||
from fabledassistant.services.events import update_event
|
||||
from scribe.services.events import update_event
|
||||
await update_event(user_id=1, event_id=1, end_dt=None)
|
||||
assert mock_event.duration_minutes is None
|
||||
|
||||
@@ -280,9 +280,9 @@ async def test_list_events_includes_point_event_in_window():
|
||||
mock_result.scalars.return_value.all.return_value = [mock_event]
|
||||
mock_session.execute = AsyncMock(return_value=mock_result)
|
||||
|
||||
with patch("fabledassistant.services.events.async_session") as mock_cls:
|
||||
with patch("scribe.services.events.async_session") as mock_cls:
|
||||
mock_cls.return_value = mock_session
|
||||
from fabledassistant.services.events import list_events
|
||||
from scribe.services.events import list_events
|
||||
results = await list_events(
|
||||
user_id=1,
|
||||
date_from=datetime(2026, 4, 29, tzinfo=timezone.utc),
|
||||
@@ -310,9 +310,9 @@ async def test_list_events_excludes_timed_event_that_already_ended():
|
||||
mock_result.scalars.return_value.all.return_value = [mock_event]
|
||||
mock_session.execute = AsyncMock(return_value=mock_result)
|
||||
|
||||
with patch("fabledassistant.services.events.async_session") as mock_cls:
|
||||
with patch("scribe.services.events.async_session") as mock_cls:
|
||||
mock_cls.return_value = mock_session
|
||||
from fabledassistant.services.events import list_events
|
||||
from scribe.services.events import list_events
|
||||
results = await list_events(
|
||||
user_id=1,
|
||||
date_from=datetime(2026, 4, 29, tzinfo=timezone.utc),
|
||||
|
||||
@@ -3,7 +3,7 @@ from unittest.mock import AsyncMock, MagicMock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
from fabledassistant.mcp.auth import resolve_bearer, resolve_bearer_to_user_id
|
||||
from scribe.mcp.auth import resolve_bearer, resolve_bearer_to_user_id
|
||||
|
||||
|
||||
@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(
|
||||
"fabledassistant.mcp.auth.lookup_key",
|
||||
"scribe.mcp.auth.lookup_key",
|
||||
AsyncMock(return_value=None),
|
||||
):
|
||||
assert await resolve_bearer_to_user_id("Bearer fmcp_doesnotexist") is None
|
||||
@@ -33,7 +33,7 @@ async def test_resolve_bearer_valid_token_returns_user_id():
|
||||
fake_key = MagicMock()
|
||||
fake_key.user_id = 42
|
||||
with patch(
|
||||
"fabledassistant.mcp.auth.lookup_key",
|
||||
"scribe.mcp.auth.lookup_key",
|
||||
AsyncMock(return_value=fake_key),
|
||||
):
|
||||
uid = await resolve_bearer_to_user_id("Bearer fmcp_validkey")
|
||||
@@ -46,7 +46,7 @@ async def test_resolve_bearer_calls_lookup_with_stripped_token():
|
||||
fake_key = MagicMock()
|
||||
fake_key.user_id = 1
|
||||
mock_lookup = AsyncMock(return_value=fake_key)
|
||||
with patch("fabledassistant.mcp.auth.lookup_key", mock_lookup):
|
||||
with patch("scribe.mcp.auth.lookup_key", mock_lookup):
|
||||
await resolve_bearer_to_user_id("Bearer fmcp_abc123 ")
|
||||
mock_lookup.assert_awaited_once_with("fmcp_abc123")
|
||||
|
||||
@@ -58,13 +58,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("fabledassistant.mcp.auth.lookup_key", AsyncMock(return_value=fake_key)):
|
||||
with patch("scribe.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("fabledassistant.mcp.auth.lookup_key", AsyncMock(return_value=None)):
|
||||
with patch("scribe.mcp.auth.lookup_key", AsyncMock(return_value=None)):
|
||||
assert await resolve_bearer("Bearer nope") is None
|
||||
assert await resolve_bearer(None) is None
|
||||
|
||||
@@ -73,7 +73,7 @@ async def test_resolve_bearer_none_for_invalid():
|
||||
|
||||
def test_body_calls_write_tool_classifies_correctly():
|
||||
import json
|
||||
from fabledassistant.mcp.server import _body_calls_write_tool
|
||||
from scribe.mcp.server import _body_calls_write_tool
|
||||
|
||||
def call(name):
|
||||
return json.dumps({"jsonrpc": "2.0", "id": 1, "method": "tools/call",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"""Tests for MCP per-request user_id context."""
|
||||
import pytest
|
||||
|
||||
from fabledassistant.mcp._context import current_user_id, _user_id_ctx
|
||||
from scribe.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 fabledassistant.app import create_app
|
||||
from scribe.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(
|
||||
"fabledassistant.mcp.auth.lookup_key",
|
||||
"scribe.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(
|
||||
"fabledassistant.mcp.auth.lookup_key",
|
||||
"scribe.mcp.auth.lookup_key",
|
||||
AsyncMock(return_value=fake_key),
|
||||
):
|
||||
status, _ = await _send_request(
|
||||
|
||||
@@ -6,8 +6,8 @@ from unittest.mock import AsyncMock, MagicMock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
from fabledassistant.mcp._context import _user_id_ctx
|
||||
from fabledassistant.mcp.tools.entities import (
|
||||
from scribe.mcp._context import _user_id_ctx
|
||||
from scribe.mcp.tools.entities import (
|
||||
list_persons, create_person, update_person,
|
||||
create_place, update_place,
|
||||
create_list, update_list,
|
||||
@@ -38,7 +38,7 @@ def _fake_note(*, note_type="note", entity_meta=None, **overrides) -> MagicMock:
|
||||
@pytest.mark.asyncio
|
||||
async def test_list_persons_calls_knowledge_with_person_type():
|
||||
mock = AsyncMock(return_value=([{"id": 1, "title": "alice"}], 1))
|
||||
with patch("fabledassistant.mcp.tools.entities.knowledge_svc.query_knowledge", mock):
|
||||
with patch("scribe.mcp.tools.entities.knowledge_svc.query_knowledge", mock):
|
||||
out = await list_persons(tag="work")
|
||||
kwargs = mock.call_args.kwargs
|
||||
assert kwargs["note_type"] == "person"
|
||||
@@ -55,7 +55,7 @@ async def test_create_person_only_includes_provided_fields_in_meta():
|
||||
"""Empty-string fields must NOT pollute entity_meta."""
|
||||
fake = _fake_note(note_type="person")
|
||||
mock = AsyncMock(return_value=fake)
|
||||
with patch("fabledassistant.mcp.tools.entities.notes_svc.create_note", mock):
|
||||
with patch("scribe.mcp.tools.entities.notes_svc.create_note", mock):
|
||||
await create_person(name="Alice", email="a@x.com")
|
||||
kwargs = mock.call_args.kwargs
|
||||
assert kwargs["title"] == "Alice"
|
||||
@@ -68,7 +68,7 @@ async def test_create_person_all_empty_meta_is_none():
|
||||
"""Service is called with entity_meta=None when no typed fields were given."""
|
||||
fake = _fake_note(note_type="person")
|
||||
mock = AsyncMock(return_value=fake)
|
||||
with patch("fabledassistant.mcp.tools.entities.notes_svc.create_note", mock):
|
||||
with patch("scribe.mcp.tools.entities.notes_svc.create_note", mock):
|
||||
await create_person(name="Bob")
|
||||
assert mock.call_args.kwargs["entity_meta"] is None
|
||||
|
||||
@@ -77,7 +77,7 @@ async def test_create_person_all_empty_meta_is_none():
|
||||
async def test_create_place_categories_into_meta():
|
||||
fake = _fake_note(note_type="place")
|
||||
mock = AsyncMock(return_value=fake)
|
||||
with patch("fabledassistant.mcp.tools.entities.notes_svc.create_note", mock):
|
||||
with patch("scribe.mcp.tools.entities.notes_svc.create_note", mock):
|
||||
await create_place(name="Cafe X", address="123 Main", category="coffee")
|
||||
meta = mock.call_args.kwargs["entity_meta"]
|
||||
assert meta == {"address": "123 Main", "category": "coffee"}
|
||||
@@ -87,7 +87,7 @@ async def test_create_place_categories_into_meta():
|
||||
async def test_create_list_translates_strings_to_unchecked_items():
|
||||
fake = _fake_note(note_type="list")
|
||||
mock = AsyncMock(return_value=fake)
|
||||
with patch("fabledassistant.mcp.tools.entities.notes_svc.create_note", mock):
|
||||
with patch("scribe.mcp.tools.entities.notes_svc.create_note", mock):
|
||||
await create_list(name="shopping", items=["milk", "bread"])
|
||||
meta = mock.call_args.kwargs["entity_meta"]
|
||||
assert meta["list_items"] == [
|
||||
@@ -110,9 +110,9 @@ async def test_update_person_merges_meta_preserving_other_fields():
|
||||
updated = _fake_note(note_type="person")
|
||||
update_mock = AsyncMock(return_value=updated)
|
||||
with patch(
|
||||
"fabledassistant.mcp.tools.entities.notes_svc.get_note", get_mock,
|
||||
"scribe.mcp.tools.entities.notes_svc.get_note", get_mock,
|
||||
), patch(
|
||||
"fabledassistant.mcp.tools.entities.notes_svc.update_note", update_mock,
|
||||
"scribe.mcp.tools.entities.notes_svc.update_note", update_mock,
|
||||
):
|
||||
await update_person(person_id=5, email="new@x.com")
|
||||
new_meta = update_mock.call_args.kwargs["entity_meta"]
|
||||
@@ -127,10 +127,10 @@ async def test_update_person_no_typed_fields_keeps_meta_unchanged():
|
||||
)
|
||||
updated = _fake_note(note_type="person")
|
||||
with patch(
|
||||
"fabledassistant.mcp.tools.entities.notes_svc.get_note",
|
||||
"scribe.mcp.tools.entities.notes_svc.get_note",
|
||||
AsyncMock(return_value=existing),
|
||||
), patch(
|
||||
"fabledassistant.mcp.tools.entities.notes_svc.update_note",
|
||||
"scribe.mcp.tools.entities.notes_svc.update_note",
|
||||
AsyncMock(return_value=updated),
|
||||
) as update_mock:
|
||||
await update_person(person_id=5, name="New Name")
|
||||
@@ -144,7 +144,7 @@ async def test_update_person_rejects_wrong_type():
|
||||
"""Trying to update a 'place' as a 'person' must fail."""
|
||||
wrong_type = _fake_note(id=5, note_type="place")
|
||||
with patch(
|
||||
"fabledassistant.mcp.tools.entities.notes_svc.get_note",
|
||||
"scribe.mcp.tools.entities.notes_svc.get_note",
|
||||
AsyncMock(return_value=wrong_type),
|
||||
):
|
||||
with pytest.raises(ValueError, match="person 5 not found"):
|
||||
@@ -160,10 +160,10 @@ async def test_update_list_items_empty_list_clears_items():
|
||||
)
|
||||
updated = _fake_note(note_type="list")
|
||||
with patch(
|
||||
"fabledassistant.mcp.tools.entities.notes_svc.get_note",
|
||||
"scribe.mcp.tools.entities.notes_svc.get_note",
|
||||
AsyncMock(return_value=existing),
|
||||
), patch(
|
||||
"fabledassistant.mcp.tools.entities.notes_svc.update_note",
|
||||
"scribe.mcp.tools.entities.notes_svc.update_note",
|
||||
AsyncMock(return_value=updated),
|
||||
) as update_mock:
|
||||
await update_list(list_id=5, items=[])
|
||||
@@ -178,10 +178,10 @@ async def test_update_list_items_none_leaves_items_unchanged():
|
||||
)
|
||||
updated = _fake_note(note_type="list")
|
||||
with patch(
|
||||
"fabledassistant.mcp.tools.entities.notes_svc.get_note",
|
||||
"scribe.mcp.tools.entities.notes_svc.get_note",
|
||||
AsyncMock(return_value=existing),
|
||||
), patch(
|
||||
"fabledassistant.mcp.tools.entities.notes_svc.update_note",
|
||||
"scribe.mcp.tools.entities.notes_svc.update_note",
|
||||
AsyncMock(return_value=updated),
|
||||
) as update_mock:
|
||||
await update_list(list_id=5, name="renamed")
|
||||
|
||||
@@ -4,8 +4,8 @@ from unittest.mock import AsyncMock, MagicMock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
from fabledassistant.mcp._context import _user_id_ctx
|
||||
from fabledassistant.mcp.tools.events import (
|
||||
from scribe.mcp._context import _user_id_ctx
|
||||
from scribe.mcp.tools.events import (
|
||||
list_events, create_event, get_event,
|
||||
update_event, delete_event,
|
||||
)
|
||||
@@ -38,7 +38,7 @@ async def test_list_events_passes_timezone_aware_range():
|
||||
mock = AsyncMock(return_value=[
|
||||
{"id": 1, "title": "morning standup"},
|
||||
])
|
||||
with patch("fabledassistant.mcp.tools.events.events_svc.list_events", mock):
|
||||
with patch("scribe.mcp.tools.events.events_svc.list_events", mock):
|
||||
out = await list_events(date_from="2026-06-01", date_to="2026-06-08")
|
||||
args, _ = mock.call_args
|
||||
assert args[0] == 7 # user_id
|
||||
@@ -52,7 +52,7 @@ async def test_list_events_passes_timezone_aware_range():
|
||||
async def test_create_event_combines_date_and_time():
|
||||
e = _fake_event()
|
||||
mock = AsyncMock(return_value=e)
|
||||
with patch("fabledassistant.mcp.tools.events.events_svc.create_event", mock):
|
||||
with patch("scribe.mcp.tools.events.events_svc.create_event", mock):
|
||||
await create_event(
|
||||
title="standup", start_date="2026-06-01", start_time="09:30",
|
||||
duration_minutes=15,
|
||||
@@ -67,7 +67,7 @@ async def test_create_event_zero_duration_means_point_event():
|
||||
"""duration_minutes=0 must map to None at the service layer (NULL = point)."""
|
||||
e = _fake_event()
|
||||
mock = AsyncMock(return_value=e)
|
||||
with patch("fabledassistant.mcp.tools.events.events_svc.create_event", mock):
|
||||
with patch("scribe.mcp.tools.events.events_svc.create_event", mock):
|
||||
await create_event(title="x", start_date="2026-06-01")
|
||||
assert mock.call_args.kwargs["duration_minutes"] is None
|
||||
|
||||
@@ -75,7 +75,7 @@ async def test_create_event_zero_duration_means_point_event():
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_event_raises_when_not_found():
|
||||
with patch(
|
||||
"fabledassistant.mcp.tools.events.events_svc.get_event",
|
||||
"scribe.mcp.tools.events.events_svc.get_event",
|
||||
AsyncMock(return_value=None),
|
||||
):
|
||||
with pytest.raises(ValueError, match="event 999 not found"):
|
||||
@@ -86,7 +86,7 @@ async def test_get_event_raises_when_not_found():
|
||||
async def test_update_event_only_sends_non_default_fields():
|
||||
e = _fake_event()
|
||||
mock = AsyncMock(return_value=e)
|
||||
with patch("fabledassistant.mcp.tools.events.events_svc.update_event", mock):
|
||||
with patch("scribe.mcp.tools.events.events_svc.update_event", mock):
|
||||
await update_event(event_id=1, title="new title")
|
||||
args, kwargs = mock.call_args
|
||||
assert args == (7, 1)
|
||||
@@ -97,7 +97,7 @@ async def test_update_event_only_sends_non_default_fields():
|
||||
async def test_update_event_duration_minus_one_means_unchanged():
|
||||
e = _fake_event()
|
||||
mock = AsyncMock(return_value=e)
|
||||
with patch("fabledassistant.mcp.tools.events.events_svc.update_event", mock):
|
||||
with patch("scribe.mcp.tools.events.events_svc.update_event", mock):
|
||||
await update_event(event_id=1, duration_minutes=-1)
|
||||
assert "duration_minutes" not in mock.call_args.kwargs
|
||||
|
||||
@@ -107,7 +107,7 @@ async def test_update_event_duration_zero_clears_to_point():
|
||||
"""duration_minutes=0 means "set to point event" (NULL)."""
|
||||
e = _fake_event()
|
||||
mock = AsyncMock(return_value=e)
|
||||
with patch("fabledassistant.mcp.tools.events.events_svc.update_event", mock):
|
||||
with patch("scribe.mcp.tools.events.events_svc.update_event", mock):
|
||||
await update_event(event_id=1, duration_minutes=0)
|
||||
assert mock.call_args.kwargs["duration_minutes"] is None
|
||||
|
||||
@@ -116,7 +116,7 @@ async def test_update_event_duration_zero_clears_to_point():
|
||||
async def test_update_event_requires_both_date_and_time_to_move():
|
||||
e = _fake_event()
|
||||
mock = AsyncMock(return_value=e)
|
||||
with patch("fabledassistant.mcp.tools.events.events_svc.update_event", mock):
|
||||
with patch("scribe.mcp.tools.events.events_svc.update_event", mock):
|
||||
await update_event(event_id=1, start_date="2026-06-02")
|
||||
# Only start_date, no start_time → start_dt NOT in fields
|
||||
assert "start_dt" not in mock.call_args.kwargs
|
||||
@@ -131,7 +131,7 @@ async def test_update_event_requires_both_date_and_time_to_move():
|
||||
@pytest.mark.asyncio
|
||||
async def test_update_event_raises_when_not_found():
|
||||
with patch(
|
||||
"fabledassistant.mcp.tools.events.events_svc.update_event",
|
||||
"scribe.mcp.tools.events.events_svc.update_event",
|
||||
AsyncMock(return_value=None),
|
||||
):
|
||||
with pytest.raises(ValueError, match="event 999 not found"):
|
||||
@@ -141,7 +141,7 @@ async def test_update_event_raises_when_not_found():
|
||||
@pytest.mark.asyncio
|
||||
async def test_delete_event_soft_deletes_and_returns_batch():
|
||||
with patch(
|
||||
"fabledassistant.mcp.tools.events.trash_svc.delete",
|
||||
"scribe.mcp.tools.events.trash_svc.delete",
|
||||
AsyncMock(return_value="batch-1"),
|
||||
):
|
||||
result = await delete_event(event_id=7)
|
||||
@@ -151,7 +151,7 @@ async def test_delete_event_soft_deletes_and_returns_batch():
|
||||
@pytest.mark.asyncio
|
||||
async def test_delete_event_raises_when_not_found():
|
||||
with patch(
|
||||
"fabledassistant.mcp.tools.events.trash_svc.delete",
|
||||
"scribe.mcp.tools.events.trash_svc.delete",
|
||||
AsyncMock(return_value=None),
|
||||
):
|
||||
with pytest.raises(ValueError, match="event 999 not found"):
|
||||
|
||||
@@ -3,8 +3,8 @@ from unittest.mock import AsyncMock, MagicMock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
from fabledassistant.mcp._context import _user_id_ctx
|
||||
from fabledassistant.mcp.tools.milestones import (
|
||||
from scribe.mcp._context import _user_id_ctx
|
||||
from scribe.mcp.tools.milestones import (
|
||||
list_milestones, create_milestone, update_milestone,
|
||||
)
|
||||
|
||||
@@ -29,7 +29,7 @@ def _fake_ms(**overrides) -> MagicMock:
|
||||
async def test_list_milestones_returns_dict_with_progress():
|
||||
rows = [{"id": 1, "title": "MS1", "status": "active", "task_count": 2}]
|
||||
with patch(
|
||||
"fabledassistant.mcp.tools.milestones.milestones_svc.get_project_milestone_summary",
|
||||
"scribe.mcp.tools.milestones.milestones_svc.get_project_milestone_summary",
|
||||
AsyncMock(return_value=rows),
|
||||
):
|
||||
out = await list_milestones(project_id=1)
|
||||
@@ -40,7 +40,7 @@ async def test_list_milestones_returns_dict_with_progress():
|
||||
async def test_create_milestone_passes_through():
|
||||
m = _fake_ms(id=5)
|
||||
mock = AsyncMock(return_value=m)
|
||||
with patch("fabledassistant.mcp.tools.milestones.milestones_svc.create_milestone", mock):
|
||||
with patch("scribe.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
|
||||
@@ -52,7 +52,7 @@ async def test_create_milestone_passes_through():
|
||||
async def test_create_milestone_empty_description_becomes_none():
|
||||
m = _fake_ms()
|
||||
mock = AsyncMock(return_value=m)
|
||||
with patch("fabledassistant.mcp.tools.milestones.milestones_svc.create_milestone", mock):
|
||||
with patch("scribe.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
|
||||
|
||||
@@ -61,7 +61,7 @@ async def test_create_milestone_empty_description_becomes_none():
|
||||
async def test_update_milestone_only_sends_non_default_fields():
|
||||
m = _fake_ms()
|
||||
mock = AsyncMock(return_value=m)
|
||||
with patch("fabledassistant.mcp.tools.milestones.milestones_svc.update_milestone", mock):
|
||||
with patch("scribe.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)
|
||||
@@ -73,7 +73,7 @@ async def test_update_milestone_order_index_negative_is_omitted():
|
||||
"""order_index=-1 sentinel means leave unchanged."""
|
||||
m = _fake_ms()
|
||||
mock = AsyncMock(return_value=m)
|
||||
with patch("fabledassistant.mcp.tools.milestones.milestones_svc.update_milestone", mock):
|
||||
with patch("scribe.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
|
||||
|
||||
@@ -83,7 +83,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_ms()
|
||||
mock = AsyncMock(return_value=m)
|
||||
with patch("fabledassistant.mcp.tools.milestones.milestones_svc.update_milestone", mock):
|
||||
with patch("scribe.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
|
||||
|
||||
@@ -91,7 +91,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(
|
||||
"fabledassistant.mcp.tools.milestones.milestones_svc.update_milestone",
|
||||
"scribe.mcp.tools.milestones.milestones_svc.update_milestone",
|
||||
AsyncMock(return_value=None),
|
||||
):
|
||||
with pytest.raises(ValueError, match="milestone 999 not found"):
|
||||
|
||||
@@ -3,8 +3,8 @@ from unittest.mock import AsyncMock, MagicMock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
from fabledassistant.mcp._context import _user_id_ctx
|
||||
from fabledassistant.mcp.tools.notes import (
|
||||
from scribe.mcp._context import _user_id_ctx
|
||||
from scribe.mcp.tools.notes import (
|
||||
list_notes, get_note, create_note,
|
||||
update_note, delete_note,
|
||||
)
|
||||
@@ -29,7 +29,7 @@ def _fake_note(**overrides) -> MagicMock:
|
||||
async def test_list_notes_repackages_tuple_into_dict():
|
||||
rows = [_fake_note(id=1), _fake_note(id=2)]
|
||||
with patch(
|
||||
"fabledassistant.mcp.tools.notes.notes_svc.list_notes",
|
||||
"scribe.mcp.tools.notes.notes_svc.list_notes",
|
||||
AsyncMock(return_value=(rows, 2)),
|
||||
):
|
||||
out = await list_notes()
|
||||
@@ -40,7 +40,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("fabledassistant.mcp.tools.notes.notes_svc.list_notes", mock):
|
||||
with patch("scribe.mcp.tools.notes.notes_svc.list_notes", mock):
|
||||
await list_notes()
|
||||
assert mock.call_args.kwargs["is_task"] is False
|
||||
|
||||
@@ -49,7 +49,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("fabledassistant.mcp.tools.notes.notes_svc.list_notes", mock):
|
||||
with patch("scribe.mcp.tools.notes.notes_svc.list_notes", mock):
|
||||
await list_notes(tag="ops")
|
||||
assert mock.call_args.kwargs["tags"] == ["ops"]
|
||||
|
||||
@@ -57,7 +57,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("fabledassistant.mcp.tools.notes.notes_svc.list_notes", mock):
|
||||
with patch("scribe.mcp.tools.notes.notes_svc.list_notes", mock):
|
||||
await list_notes(tag="")
|
||||
assert mock.call_args.kwargs["tags"] is None
|
||||
|
||||
@@ -65,7 +65,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("fabledassistant.mcp.tools.notes.notes_svc.list_notes", mock):
|
||||
with patch("scribe.mcp.tools.notes.notes_svc.list_notes", mock):
|
||||
await list_notes(search_text="kafka")
|
||||
assert mock.call_args.kwargs["q"] == "kafka"
|
||||
|
||||
@@ -73,7 +73,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("fabledassistant.mcp.tools.notes.notes_svc.list_notes", mock):
|
||||
with patch("scribe.mcp.tools.notes.notes_svc.list_notes", mock):
|
||||
await list_notes(limit=9999)
|
||||
assert mock.call_args.kwargs["limit"] == 100
|
||||
|
||||
@@ -82,7 +82,7 @@ async def test_list_notes_limit_clamped():
|
||||
async def test_get_note_returns_dict():
|
||||
fake = _fake_note(id=5, title="found")
|
||||
with patch(
|
||||
"fabledassistant.mcp.tools.notes.notes_svc.get_note",
|
||||
"scribe.mcp.tools.notes.notes_svc.get_note",
|
||||
AsyncMock(return_value=fake),
|
||||
):
|
||||
out = await get_note(note_id=5)
|
||||
@@ -93,7 +93,7 @@ async def test_get_note_returns_dict():
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_note_raises_when_not_found():
|
||||
with patch(
|
||||
"fabledassistant.mcp.tools.notes.notes_svc.get_note",
|
||||
"scribe.mcp.tools.notes.notes_svc.get_note",
|
||||
AsyncMock(return_value=None),
|
||||
):
|
||||
with pytest.raises(ValueError, match="note 999 not found"):
|
||||
@@ -104,7 +104,7 @@ async def test_get_note_raises_when_not_found():
|
||||
async def test_create_note_passes_through():
|
||||
fake = _fake_note(id=10, title="new")
|
||||
mock = AsyncMock(return_value=fake)
|
||||
with patch("fabledassistant.mcp.tools.notes.notes_svc.create_note", mock):
|
||||
with patch("scribe.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"
|
||||
@@ -116,7 +116,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("fabledassistant.mcp.tools.notes.notes_svc.create_note", mock):
|
||||
with patch("scribe.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
|
||||
|
||||
@@ -127,7 +127,7 @@ async def test_update_note_only_sends_non_default_fields():
|
||||
overwrite real data with empty strings."""
|
||||
fake = _fake_note()
|
||||
mock = AsyncMock(return_value=fake)
|
||||
with patch("fabledassistant.mcp.tools.notes.notes_svc.update_note", mock):
|
||||
with patch("scribe.mcp.tools.notes.notes_svc.update_note", mock):
|
||||
await update_note(note_id=1, title="new title")
|
||||
# Service got user_id, note_id positional + only the title kwarg
|
||||
args, kwargs = mock.call_args
|
||||
@@ -140,7 +140,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("fabledassistant.mcp.tools.notes.notes_svc.update_note", mock):
|
||||
with patch("scribe.mcp.tools.notes.notes_svc.update_note", mock):
|
||||
await update_note(note_id=1, tags=[])
|
||||
assert mock.call_args.kwargs == {"tags": []}
|
||||
|
||||
@@ -149,7 +149,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("fabledassistant.mcp.tools.notes.notes_svc.update_note", mock):
|
||||
with patch("scribe.mcp.tools.notes.notes_svc.update_note", mock):
|
||||
await update_note(note_id=1, tags=None)
|
||||
assert "tags" not in mock.call_args.kwargs
|
||||
|
||||
@@ -157,7 +157,7 @@ async def test_update_note_tags_none_means_omit():
|
||||
@pytest.mark.asyncio
|
||||
async def test_update_note_raises_when_not_found():
|
||||
with patch(
|
||||
"fabledassistant.mcp.tools.notes.notes_svc.update_note",
|
||||
"scribe.mcp.tools.notes.notes_svc.update_note",
|
||||
AsyncMock(return_value=None),
|
||||
):
|
||||
with pytest.raises(ValueError, match="note 999 not found"):
|
||||
@@ -167,7 +167,7 @@ async def test_update_note_raises_when_not_found():
|
||||
@pytest.mark.asyncio
|
||||
async def test_delete_note_soft_deletes_and_returns_batch():
|
||||
with patch(
|
||||
"fabledassistant.mcp.tools.notes.trash_svc.delete",
|
||||
"scribe.mcp.tools.notes.trash_svc.delete",
|
||||
AsyncMock(return_value="batch-1"),
|
||||
):
|
||||
result = await delete_note(note_id=7)
|
||||
@@ -177,7 +177,7 @@ async def test_delete_note_soft_deletes_and_returns_batch():
|
||||
@pytest.mark.asyncio
|
||||
async def test_delete_note_raises_when_not_found():
|
||||
with patch(
|
||||
"fabledassistant.mcp.tools.notes.trash_svc.delete",
|
||||
"scribe.mcp.tools.notes.trash_svc.delete",
|
||||
AsyncMock(return_value=None),
|
||||
):
|
||||
with pytest.raises(ValueError, match="note 999 not found"):
|
||||
|
||||
@@ -2,7 +2,7 @@ from unittest.mock import AsyncMock, MagicMock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
from fabledassistant.mcp._context import _user_id_ctx
|
||||
from scribe.mcp._context import _user_id_ctx
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
@@ -16,9 +16,9 @@ def _bind_user():
|
||||
async def test_start_planning_tool_delegates_to_service():
|
||||
payload = {"task": {"id": 5}, "applicable_rules": [], "subscribed_rulebooks": [],
|
||||
"applicable_rules_truncated": False, "project_goal": "", "open_task_count": 0}
|
||||
with patch("fabledassistant.mcp.tools.tasks.planning_svc.start_planning",
|
||||
with patch("scribe.mcp.tools.tasks.planning_svc.start_planning",
|
||||
AsyncMock(return_value=payload)) as mock:
|
||||
from fabledassistant.mcp.tools.tasks import start_planning
|
||||
from scribe.mcp.tools.tasks import start_planning
|
||||
out = await start_planning(project_id=3, title="Plan it")
|
||||
assert out["task"]["id"] == 5
|
||||
assert mock.call_args.kwargs == {"user_id": 7, "project_id": 3, "title": "Plan it"}
|
||||
@@ -32,11 +32,11 @@ async def test_get_task_augments_plan_with_rules():
|
||||
note.to_dict.return_value = {"id": 9, "task_kind": "plan", "project_id": 3}
|
||||
applicable = {"rules": [{"id": 1, "title": "r"}], "truncated": False,
|
||||
"subscribed_rulebooks": [{"id": 2, "title": "rb"}]}
|
||||
with patch("fabledassistant.mcp.tools.tasks.notes_svc.get_note",
|
||||
with patch("scribe.mcp.tools.tasks.notes_svc.get_note",
|
||||
AsyncMock(return_value=note)), \
|
||||
patch("fabledassistant.mcp.tools.tasks.rulebooks_svc.get_applicable_rules",
|
||||
patch("scribe.mcp.tools.tasks.rulebooks_svc.get_applicable_rules",
|
||||
AsyncMock(return_value=applicable)):
|
||||
from fabledassistant.mcp.tools.tasks import get_task
|
||||
from scribe.mcp.tools.tasks import get_task
|
||||
out = await get_task(task_id=9)
|
||||
assert out["applicable_rules"] == [{"id": 1, "title": "r"}]
|
||||
assert out["subscribed_rulebooks"] == [{"id": 2, "title": "rb"}]
|
||||
@@ -49,11 +49,11 @@ async def test_get_task_work_kind_has_no_rules():
|
||||
note.parent_id = None
|
||||
note.project_id = 3
|
||||
note.to_dict.return_value = {"id": 9, "task_kind": "work", "project_id": 3}
|
||||
with patch("fabledassistant.mcp.tools.tasks.notes_svc.get_note",
|
||||
with patch("scribe.mcp.tools.tasks.notes_svc.get_note",
|
||||
AsyncMock(return_value=note)), \
|
||||
patch("fabledassistant.mcp.tools.tasks.rulebooks_svc.get_applicable_rules",
|
||||
patch("scribe.mcp.tools.tasks.rulebooks_svc.get_applicable_rules",
|
||||
AsyncMock()) as mock_rules:
|
||||
from fabledassistant.mcp.tools.tasks import get_task
|
||||
from scribe.mcp.tools.tasks import get_task
|
||||
out = await get_task(task_id=9)
|
||||
assert "applicable_rules" not in out
|
||||
assert not mock_rules.called
|
||||
|
||||
@@ -3,7 +3,7 @@ from unittest.mock import AsyncMock, MagicMock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
from fabledassistant.mcp._context import _user_id_ctx
|
||||
from scribe.mcp._context import _user_id_ctx
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
@@ -24,7 +24,7 @@ def _fake_note(id=1, title="Drift Audit", note_type="process"):
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_create_process_requires_title_and_body():
|
||||
from fabledassistant.mcp.tools.processes import create_process
|
||||
from scribe.mcp.tools.processes import create_process
|
||||
with pytest.raises(ValueError):
|
||||
await create_process(title="", body="something")
|
||||
with pytest.raises(ValueError):
|
||||
@@ -34,9 +34,9 @@ async def test_create_process_requires_title_and_body():
|
||||
@pytest.mark.asyncio
|
||||
async def test_create_process_sets_note_type():
|
||||
created = _fake_note()
|
||||
with patch("fabledassistant.services.notes.create_note",
|
||||
with patch("scribe.services.notes.create_note",
|
||||
AsyncMock(return_value=created)) as mock_create:
|
||||
from fabledassistant.mcp.tools.processes import create_process
|
||||
from scribe.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
|
||||
@@ -47,9 +47,9 @@ async def test_create_process_sets_note_type():
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_process_returns_body_and_candidates():
|
||||
note = _fake_note(id=7)
|
||||
with patch("fabledassistant.services.notes.resolve_process",
|
||||
with patch("scribe.services.notes.resolve_process",
|
||||
AsyncMock(return_value=(note, [{"id": 9, "title": "Drift Audit Notes"}]))):
|
||||
from fabledassistant.mcp.tools.processes import get_process
|
||||
from scribe.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"}]
|
||||
@@ -57,9 +57,9 @@ async def test_get_process_returns_body_and_candidates():
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_process_not_found_raises():
|
||||
with patch("fabledassistant.services.notes.resolve_process",
|
||||
with patch("scribe.services.notes.resolve_process",
|
||||
AsyncMock(return_value=(None, []))):
|
||||
from fabledassistant.mcp.tools.processes import get_process
|
||||
from scribe.mcp.tools.processes import get_process
|
||||
with pytest.raises(ValueError):
|
||||
await get_process("missing")
|
||||
|
||||
@@ -67,15 +67,15 @@ async def test_get_process_not_found_raises():
|
||||
@pytest.mark.asyncio
|
||||
async def test_update_process_rejects_non_process_note():
|
||||
plain = _fake_note(id=3, note_type="note")
|
||||
with patch("fabledassistant.services.notes.get_note",
|
||||
with patch("scribe.services.notes.get_note",
|
||||
AsyncMock(return_value=plain)):
|
||||
from fabledassistant.mcp.tools.processes import update_process
|
||||
from scribe.mcp.tools.processes import update_process
|
||||
with pytest.raises(ValueError):
|
||||
await update_process(process_id=3, title="x")
|
||||
|
||||
|
||||
def test_register_attaches_four_tools():
|
||||
from fabledassistant.mcp.tools import processes
|
||||
from scribe.mcp.tools import processes
|
||||
names: list[str] = []
|
||||
|
||||
class FakeMcp:
|
||||
|
||||
@@ -3,8 +3,8 @@ from unittest.mock import AsyncMock, MagicMock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
from fabledassistant.mcp._context import _user_id_ctx
|
||||
from fabledassistant.mcp.tools.projects import (
|
||||
from scribe.mcp._context import _user_id_ctx
|
||||
from scribe.mcp.tools.projects import (
|
||||
list_projects, get_project, create_project,
|
||||
update_project, enter_project,
|
||||
)
|
||||
@@ -30,7 +30,7 @@ def _fake_project(**overrides) -> MagicMock:
|
||||
async def test_list_projects_wraps_in_dict():
|
||||
rows = [_fake_project(id=1), _fake_project(id=2)]
|
||||
with patch(
|
||||
"fabledassistant.mcp.tools.projects.projects_svc.list_projects",
|
||||
"scribe.mcp.tools.projects.projects_svc.list_projects",
|
||||
AsyncMock(return_value=rows),
|
||||
):
|
||||
out = await list_projects()
|
||||
@@ -45,13 +45,13 @@ async def test_get_project_enriches_with_milestone_summary():
|
||||
"rules": [], "truncated": False, "subscribed_rulebooks": [],
|
||||
}
|
||||
with patch(
|
||||
"fabledassistant.mcp.tools.projects.projects_svc.get_project",
|
||||
"scribe.mcp.tools.projects.projects_svc.get_project",
|
||||
AsyncMock(return_value=p),
|
||||
), patch(
|
||||
"fabledassistant.mcp.tools.projects.milestones_svc.get_project_milestone_summary",
|
||||
"scribe.mcp.tools.projects.milestones_svc.get_project_milestone_summary",
|
||||
AsyncMock(return_value=milestone_summary),
|
||||
), patch(
|
||||
"fabledassistant.mcp.tools.projects.rulebooks_svc.get_applicable_rules",
|
||||
"scribe.mcp.tools.projects.rulebooks_svc.get_applicable_rules",
|
||||
AsyncMock(return_value=applicable_payload),
|
||||
):
|
||||
out = await get_project(project_id=5)
|
||||
@@ -77,13 +77,13 @@ async def test_get_project_includes_applicable_rules_and_subscribed_rulebooks():
|
||||
"subscribed_rulebooks": [{"id": 1, "title": "FabledSword family"}],
|
||||
}
|
||||
with patch(
|
||||
"fabledassistant.mcp.tools.projects.projects_svc.get_project",
|
||||
"scribe.mcp.tools.projects.projects_svc.get_project",
|
||||
AsyncMock(return_value=p),
|
||||
), patch(
|
||||
"fabledassistant.mcp.tools.projects.milestones_svc.get_project_milestone_summary",
|
||||
"scribe.mcp.tools.projects.milestones_svc.get_project_milestone_summary",
|
||||
AsyncMock(return_value=milestone_summary),
|
||||
), patch(
|
||||
"fabledassistant.mcp.tools.projects.rulebooks_svc.get_applicable_rules",
|
||||
"scribe.mcp.tools.projects.rulebooks_svc.get_applicable_rules",
|
||||
AsyncMock(return_value=applicable_payload),
|
||||
):
|
||||
out = await get_project(project_id=3)
|
||||
@@ -95,7 +95,7 @@ async def test_get_project_includes_applicable_rules_and_subscribed_rulebooks():
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_project_raises_when_not_found():
|
||||
with patch(
|
||||
"fabledassistant.mcp.tools.projects.projects_svc.get_project",
|
||||
"scribe.mcp.tools.projects.projects_svc.get_project",
|
||||
AsyncMock(return_value=None),
|
||||
):
|
||||
with pytest.raises(ValueError, match="project 999 not found"):
|
||||
@@ -106,7 +106,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("fabledassistant.mcp.tools.projects.projects_svc.create_project", mock):
|
||||
with patch("scribe.mcp.tools.projects.projects_svc.create_project", mock):
|
||||
await create_project(title="P", color="")
|
||||
assert mock.call_args.kwargs["color"] is None
|
||||
|
||||
@@ -115,7 +115,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("fabledassistant.mcp.tools.projects.projects_svc.update_project", mock):
|
||||
with patch("scribe.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)
|
||||
@@ -125,7 +125,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(
|
||||
"fabledassistant.mcp.tools.projects.projects_svc.update_project",
|
||||
"scribe.mcp.tools.projects.projects_svc.update_project",
|
||||
AsyncMock(return_value=None),
|
||||
):
|
||||
with pytest.raises(ValueError, match="project 999 not found"):
|
||||
@@ -154,16 +154,16 @@ async def test_enter_project_composes_full_context():
|
||||
note1.updated_at = None # avoids datetime mocking
|
||||
|
||||
with patch(
|
||||
"fabledassistant.mcp.tools.projects.projects_svc.get_project",
|
||||
"scribe.mcp.tools.projects.projects_svc.get_project",
|
||||
AsyncMock(return_value=p),
|
||||
), patch(
|
||||
"fabledassistant.mcp.tools.projects.rulebooks_svc.get_applicable_rules",
|
||||
"scribe.mcp.tools.projects.rulebooks_svc.get_applicable_rules",
|
||||
AsyncMock(return_value=applicable_payload),
|
||||
), patch(
|
||||
"fabledassistant.mcp.tools.projects.milestones_svc.get_project_milestone_summary",
|
||||
"scribe.mcp.tools.projects.milestones_svc.get_project_milestone_summary",
|
||||
AsyncMock(return_value=milestone_summary),
|
||||
), patch(
|
||||
"fabledassistant.mcp.tools.projects.notes_svc.list_notes",
|
||||
"scribe.mcp.tools.projects.notes_svc.list_notes",
|
||||
AsyncMock(side_effect=[([task1], 1), ([note1], 1)]),
|
||||
):
|
||||
out = await enter_project(project_id=5)
|
||||
@@ -181,7 +181,7 @@ async def test_enter_project_composes_full_context():
|
||||
@pytest.mark.asyncio
|
||||
async def test_enter_project_raises_when_project_not_found():
|
||||
with patch(
|
||||
"fabledassistant.mcp.tools.projects.projects_svc.get_project",
|
||||
"scribe.mcp.tools.projects.projects_svc.get_project",
|
||||
AsyncMock(return_value=None),
|
||||
):
|
||||
with pytest.raises(ValueError, match="project 999 not found"):
|
||||
@@ -190,7 +190,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 fabledassistant.mcp.tools.projects import register
|
||||
from scribe.mcp.tools.projects import register
|
||||
registered: list[str] = []
|
||||
|
||||
class FakeMCP:
|
||||
|
||||
@@ -3,7 +3,7 @@ from unittest.mock import AsyncMock, MagicMock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
from fabledassistant.mcp._context import _user_id_ctx
|
||||
from scribe.mcp._context import _user_id_ctx
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
@@ -43,10 +43,10 @@ def _fake_rule(id=100, title="r", statement="s"):
|
||||
async def test_list_rulebooks_wraps_in_dict():
|
||||
rows = [_fake_rulebook(id=1), _fake_rulebook(id=2)]
|
||||
with patch(
|
||||
"fabledassistant.mcp.tools.rulebooks.rulebooks_svc.list_rulebooks",
|
||||
"scribe.mcp.tools.rulebooks.rulebooks_svc.list_rulebooks",
|
||||
AsyncMock(return_value=rows),
|
||||
):
|
||||
from fabledassistant.mcp.tools.rulebooks import list_rulebooks
|
||||
from scribe.mcp.tools.rulebooks import list_rulebooks
|
||||
out = await list_rulebooks()
|
||||
assert len(out["rulebooks"]) == 2
|
||||
|
||||
@@ -56,13 +56,13 @@ async def test_get_rulebook_includes_topics():
|
||||
rb = _fake_rulebook(id=1)
|
||||
topics = [_fake_topic(id=10), _fake_topic(id=11)]
|
||||
with patch(
|
||||
"fabledassistant.mcp.tools.rulebooks.rulebooks_svc.get_rulebook",
|
||||
"scribe.mcp.tools.rulebooks.rulebooks_svc.get_rulebook",
|
||||
AsyncMock(return_value=rb),
|
||||
), patch(
|
||||
"fabledassistant.mcp.tools.rulebooks.rulebooks_svc.list_topics",
|
||||
"scribe.mcp.tools.rulebooks.rulebooks_svc.list_topics",
|
||||
AsyncMock(return_value=topics),
|
||||
):
|
||||
from fabledassistant.mcp.tools.rulebooks import get_rulebook
|
||||
from scribe.mcp.tools.rulebooks import get_rulebook
|
||||
out = await get_rulebook(rulebook_id=1)
|
||||
assert out["id"] == 1
|
||||
assert len(out["topics"]) == 2
|
||||
@@ -71,10 +71,10 @@ async def test_get_rulebook_includes_topics():
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_rulebook_raises_when_not_found():
|
||||
with patch(
|
||||
"fabledassistant.mcp.tools.rulebooks.rulebooks_svc.get_rulebook",
|
||||
"scribe.mcp.tools.rulebooks.rulebooks_svc.get_rulebook",
|
||||
AsyncMock(return_value=None),
|
||||
):
|
||||
from fabledassistant.mcp.tools.rulebooks import get_rulebook
|
||||
from scribe.mcp.tools.rulebooks import get_rulebook
|
||||
with pytest.raises(ValueError, match="rulebook 999 not found"):
|
||||
await get_rulebook(rulebook_id=999)
|
||||
|
||||
@@ -83,8 +83,8 @@ async def test_get_rulebook_raises_when_not_found():
|
||||
async def test_create_rule_passes_required_fields():
|
||||
rule = _fake_rule()
|
||||
mock = AsyncMock(return_value=rule)
|
||||
with patch("fabledassistant.mcp.tools.rulebooks.rulebooks_svc.create_rule", mock):
|
||||
from fabledassistant.mcp.tools.rulebooks import create_rule
|
||||
with patch("scribe.mcp.tools.rulebooks.rulebooks_svc.create_rule", mock):
|
||||
from scribe.mcp.tools.rulebooks import create_rule
|
||||
await create_rule(
|
||||
topic_id=10, title="dev is home", statement="Work directly on dev",
|
||||
)
|
||||
@@ -98,8 +98,8 @@ async def test_create_rule_passes_required_fields():
|
||||
async def test_update_rule_only_sends_non_default_fields():
|
||||
rule = _fake_rule()
|
||||
mock = AsyncMock(return_value=rule)
|
||||
with patch("fabledassistant.mcp.tools.rulebooks.rulebooks_svc.update_rule", mock):
|
||||
from fabledassistant.mcp.tools.rulebooks import update_rule
|
||||
with patch("scribe.mcp.tools.rulebooks.rulebooks_svc.update_rule", mock):
|
||||
from scribe.mcp.tools.rulebooks import update_rule
|
||||
await update_rule(rule_id=1, statement="new statement")
|
||||
args, kwargs = mock.call_args
|
||||
assert args == (1, 7)
|
||||
@@ -111,13 +111,13 @@ async def test_delete_rule_without_confirmed_returns_warning():
|
||||
"""delete_rule with confirmed=False returns a preview, not an action."""
|
||||
rule = _fake_rule()
|
||||
with patch(
|
||||
"fabledassistant.mcp.tools.rulebooks.rulebooks_svc.get_rule",
|
||||
"scribe.mcp.tools.rulebooks.rulebooks_svc.get_rule",
|
||||
AsyncMock(return_value=rule),
|
||||
), patch(
|
||||
"fabledassistant.mcp.tools.rulebooks.rulebooks_svc.delete_rule",
|
||||
"scribe.mcp.tools.rulebooks.rulebooks_svc.delete_rule",
|
||||
AsyncMock(),
|
||||
) as mock_delete:
|
||||
from fabledassistant.mcp.tools.rulebooks import delete_rule
|
||||
from scribe.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", "")
|
||||
@@ -129,13 +129,13 @@ async def test_delete_rule_with_confirmed_soft_deletes():
|
||||
rule = _fake_rule()
|
||||
mock_delete = AsyncMock(return_value="batch-1")
|
||||
with patch(
|
||||
"fabledassistant.mcp.tools.rulebooks.rulebooks_svc.get_rule",
|
||||
"scribe.mcp.tools.rulebooks.rulebooks_svc.get_rule",
|
||||
AsyncMock(return_value=rule),
|
||||
), patch(
|
||||
"fabledassistant.mcp.tools.rulebooks.trash_svc.delete",
|
||||
"scribe.mcp.tools.rulebooks.trash_svc.delete",
|
||||
mock_delete,
|
||||
):
|
||||
from fabledassistant.mcp.tools.rulebooks import delete_rule
|
||||
from scribe.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"
|
||||
@@ -146,9 +146,9 @@ async def test_delete_rule_with_confirmed_soft_deletes():
|
||||
async def test_subscribe_project_to_rulebook_calls_service():
|
||||
mock = AsyncMock()
|
||||
with patch(
|
||||
"fabledassistant.mcp.tools.rulebooks.rulebooks_svc.subscribe_project", mock,
|
||||
"scribe.mcp.tools.rulebooks.rulebooks_svc.subscribe_project", mock,
|
||||
):
|
||||
from fabledassistant.mcp.tools.rulebooks import subscribe_project_to_rulebook
|
||||
from scribe.mcp.tools.rulebooks import subscribe_project_to_rulebook
|
||||
out = await subscribe_project_to_rulebook(project_id=3, rulebook_id=1)
|
||||
assert out["subscribed"] is True
|
||||
assert mock.called
|
||||
@@ -158,9 +158,9 @@ async def test_subscribe_project_to_rulebook_calls_service():
|
||||
async def test_unsubscribe_project_from_rulebook_calls_service():
|
||||
mock = AsyncMock()
|
||||
with patch(
|
||||
"fabledassistant.mcp.tools.rulebooks.rulebooks_svc.unsubscribe_project", mock,
|
||||
"scribe.mcp.tools.rulebooks.rulebooks_svc.unsubscribe_project", mock,
|
||||
):
|
||||
from fabledassistant.mcp.tools.rulebooks import unsubscribe_project_from_rulebook
|
||||
from scribe.mcp.tools.rulebooks import unsubscribe_project_from_rulebook
|
||||
out = await unsubscribe_project_from_rulebook(project_id=3, rulebook_id=1)
|
||||
assert out["subscribed"] is False
|
||||
assert mock.called
|
||||
@@ -168,7 +168,7 @@ async def test_unsubscribe_project_from_rulebook_calls_service():
|
||||
|
||||
def test_register_attaches_all_sixteen_tools():
|
||||
"""register(mcp) should call mcp.tool(name=...) for all 16 tools."""
|
||||
from fabledassistant.mcp.tools.rulebooks import register
|
||||
from scribe.mcp.tools.rulebooks import register
|
||||
registered: list[str] = []
|
||||
|
||||
class FakeMCP:
|
||||
@@ -195,10 +195,10 @@ def test_register_attaches_all_sixteen_tools():
|
||||
@pytest.mark.asyncio
|
||||
async def test_list_always_on_rules_returns_empty_when_no_always_on_rulebooks():
|
||||
with patch(
|
||||
"fabledassistant.mcp.tools.rulebooks.rulebooks_svc.list_always_on_rules",
|
||||
"scribe.mcp.tools.rulebooks.rulebooks_svc.list_always_on_rules",
|
||||
AsyncMock(return_value=[]),
|
||||
):
|
||||
from fabledassistant.mcp.tools.rulebooks import list_always_on_rules
|
||||
from scribe.mcp.tools.rulebooks import list_always_on_rules
|
||||
out = await list_always_on_rules()
|
||||
assert out == {"rules": [], "total": 0}
|
||||
|
||||
@@ -207,10 +207,10 @@ async def test_list_always_on_rules_returns_empty_when_no_always_on_rulebooks():
|
||||
async def test_list_always_on_rules_projects_each_rule():
|
||||
rules = [_fake_rule(id=100), _fake_rule(id=101)]
|
||||
with patch(
|
||||
"fabledassistant.mcp.tools.rulebooks.rulebooks_svc.list_always_on_rules",
|
||||
"scribe.mcp.tools.rulebooks.rulebooks_svc.list_always_on_rules",
|
||||
AsyncMock(return_value=rules),
|
||||
):
|
||||
from fabledassistant.mcp.tools.rulebooks import list_always_on_rules
|
||||
from scribe.mcp.tools.rulebooks import list_always_on_rules
|
||||
out = await list_always_on_rules()
|
||||
assert out["total"] == 2
|
||||
assert {r["id"] for r in out["rules"]} == {100, 101}
|
||||
@@ -221,8 +221,8 @@ async def test_list_always_on_rules_projects_each_rule():
|
||||
async def test_update_rulebook_forwards_always_on_when_set():
|
||||
rb = _fake_rulebook(id=1, title="t")
|
||||
mock = AsyncMock(return_value=rb)
|
||||
with patch("fabledassistant.mcp.tools.rulebooks.rulebooks_svc.update_rulebook", mock):
|
||||
from fabledassistant.mcp.tools.rulebooks import update_rulebook
|
||||
with patch("scribe.mcp.tools.rulebooks.rulebooks_svc.update_rulebook", mock):
|
||||
from scribe.mcp.tools.rulebooks import update_rulebook
|
||||
await update_rulebook(rulebook_id=1, always_on=True)
|
||||
kwargs = mock.call_args.kwargs
|
||||
assert kwargs.get("always_on") is True
|
||||
@@ -234,8 +234,8 @@ async def test_update_rulebook_forwards_always_on_when_set():
|
||||
async def test_update_rulebook_omits_always_on_when_none():
|
||||
rb = _fake_rulebook(id=1, title="t")
|
||||
mock = AsyncMock(return_value=rb)
|
||||
with patch("fabledassistant.mcp.tools.rulebooks.rulebooks_svc.update_rulebook", mock):
|
||||
from fabledassistant.mcp.tools.rulebooks import update_rulebook
|
||||
with patch("scribe.mcp.tools.rulebooks.rulebooks_svc.update_rulebook", mock):
|
||||
from scribe.mcp.tools.rulebooks import update_rulebook
|
||||
await update_rulebook(rulebook_id=1, title="new title")
|
||||
kwargs = mock.call_args.kwargs
|
||||
assert "always_on" not in kwargs
|
||||
@@ -246,8 +246,8 @@ async def test_update_rulebook_omits_always_on_when_none():
|
||||
async def test_create_project_rule_passes_required_fields():
|
||||
rule = _fake_rule()
|
||||
mock = AsyncMock(return_value=rule)
|
||||
with patch("fabledassistant.mcp.tools.rulebooks.rulebooks_svc.create_project_rule", mock):
|
||||
from fabledassistant.mcp.tools.rulebooks import create_project_rule
|
||||
with patch("scribe.mcp.tools.rulebooks.rulebooks_svc.create_project_rule", mock):
|
||||
from scribe.mcp.tools.rulebooks import create_project_rule
|
||||
await create_project_rule(
|
||||
project_id=42,
|
||||
statement="Always run migrations through alembic, not raw SQL.",
|
||||
@@ -264,8 +264,8 @@ async def test_create_project_rule_passes_required_fields():
|
||||
async def test_create_project_rule_derives_title_from_statement():
|
||||
rule = _fake_rule()
|
||||
mock = AsyncMock(return_value=rule)
|
||||
with patch("fabledassistant.mcp.tools.rulebooks.rulebooks_svc.create_project_rule", mock):
|
||||
from fabledassistant.mcp.tools.rulebooks import create_project_rule
|
||||
with patch("scribe.mcp.tools.rulebooks.rulebooks_svc.create_project_rule", mock):
|
||||
from scribe.mcp.tools.rulebooks import create_project_rule
|
||||
await create_project_rule(
|
||||
project_id=42,
|
||||
statement="Avoid auto-generated docstrings. Reviewers find them noise.",
|
||||
@@ -279,8 +279,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()
|
||||
mock = AsyncMock(return_value=rule)
|
||||
with patch("fabledassistant.mcp.tools.rulebooks.rulebooks_svc.create_project_rule", mock):
|
||||
from fabledassistant.mcp.tools.rulebooks import create_project_rule
|
||||
with patch("scribe.mcp.tools.rulebooks.rulebooks_svc.create_project_rule", mock):
|
||||
from scribe.mcp.tools.rulebooks import create_project_rule
|
||||
await create_project_rule(
|
||||
project_id=42,
|
||||
statement="anything",
|
||||
@@ -293,8 +293,8 @@ async def test_create_project_rule_uses_explicit_title_when_given():
|
||||
@pytest.mark.asyncio
|
||||
async def test_suppress_rule_for_project_passes_through():
|
||||
mock = AsyncMock(return_value=None)
|
||||
with patch("fabledassistant.mcp.tools.rulebooks.rulebooks_svc.suppress_rule_for_project", mock):
|
||||
from fabledassistant.mcp.tools.rulebooks import suppress_rule_for_project
|
||||
with patch("scribe.mcp.tools.rulebooks.rulebooks_svc.suppress_rule_for_project", mock):
|
||||
from scribe.mcp.tools.rulebooks import suppress_rule_for_project
|
||||
out = await suppress_rule_for_project(project_id=3, rule_id=17)
|
||||
kwargs = mock.call_args.kwargs
|
||||
assert kwargs == {"project_id": 3, "rule_id": 17, "user_id": 7}
|
||||
@@ -304,8 +304,8 @@ async def test_suppress_rule_for_project_passes_through():
|
||||
@pytest.mark.asyncio
|
||||
async def test_unsuppress_rule_for_project_passes_through():
|
||||
mock = AsyncMock(return_value=None)
|
||||
with patch("fabledassistant.mcp.tools.rulebooks.rulebooks_svc.unsuppress_rule_for_project", mock):
|
||||
from fabledassistant.mcp.tools.rulebooks import unsuppress_rule_for_project
|
||||
with patch("scribe.mcp.tools.rulebooks.rulebooks_svc.unsuppress_rule_for_project", mock):
|
||||
from scribe.mcp.tools.rulebooks import unsuppress_rule_for_project
|
||||
out = await unsuppress_rule_for_project(project_id=3, rule_id=17)
|
||||
kwargs = mock.call_args.kwargs
|
||||
assert kwargs == {"project_id": 3, "rule_id": 17, "user_id": 7}
|
||||
@@ -315,8 +315,8 @@ async def test_unsuppress_rule_for_project_passes_through():
|
||||
@pytest.mark.asyncio
|
||||
async def test_suppress_topic_for_project_passes_through():
|
||||
mock = AsyncMock(return_value=None)
|
||||
with patch("fabledassistant.mcp.tools.rulebooks.rulebooks_svc.suppress_topic_for_project", mock):
|
||||
from fabledassistant.mcp.tools.rulebooks import suppress_topic_for_project
|
||||
with patch("scribe.mcp.tools.rulebooks.rulebooks_svc.suppress_topic_for_project", mock):
|
||||
from scribe.mcp.tools.rulebooks import suppress_topic_for_project
|
||||
out = await suppress_topic_for_project(project_id=3, topic_id=22)
|
||||
kwargs = mock.call_args.kwargs
|
||||
assert kwargs == {"project_id": 3, "topic_id": 22, "user_id": 7}
|
||||
@@ -326,8 +326,8 @@ async def test_suppress_topic_for_project_passes_through():
|
||||
@pytest.mark.asyncio
|
||||
async def test_unsuppress_topic_for_project_passes_through():
|
||||
mock = AsyncMock(return_value=None)
|
||||
with patch("fabledassistant.mcp.tools.rulebooks.rulebooks_svc.unsuppress_topic_for_project", mock):
|
||||
from fabledassistant.mcp.tools.rulebooks import unsuppress_topic_for_project
|
||||
with patch("scribe.mcp.tools.rulebooks.rulebooks_svc.unsuppress_topic_for_project", mock):
|
||||
from scribe.mcp.tools.rulebooks import unsuppress_topic_for_project
|
||||
out = await unsuppress_topic_for_project(project_id=3, topic_id=22)
|
||||
kwargs = mock.call_args.kwargs
|
||||
assert kwargs == {"project_id": 3, "topic_id": 22, "user_id": 7}
|
||||
|
||||
@@ -5,8 +5,8 @@ from unittest.mock import AsyncMock, MagicMock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
from fabledassistant.mcp._context import _user_id_ctx
|
||||
from fabledassistant.mcp.tools.search import search
|
||||
from scribe.mcp._context import _user_id_ctx
|
||||
from scribe.mcp.tools.search import search
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
@@ -40,7 +40,7 @@ async def test_fable_search_returns_repackaged_results():
|
||||
fake = _fake_note(id=1, title="kafka rebalance", body="HPA details",
|
||||
tags=["ops"], is_task=False)
|
||||
with patch(
|
||||
"fabledassistant.mcp.tools.search.semantic_search_notes",
|
||||
"scribe.mcp.tools.search.semantic_search_notes",
|
||||
AsyncMock(return_value=[(0.93, fake)]),
|
||||
):
|
||||
out = await search(q="kafka")
|
||||
@@ -62,7 +62,7 @@ async def test_fable_search_body_is_truncated_to_240_chars():
|
||||
long_body = "x" * 500
|
||||
fake = _fake_note(id=1, title="t", body=long_body)
|
||||
with patch(
|
||||
"fabledassistant.mcp.tools.search.semantic_search_notes",
|
||||
"scribe.mcp.tools.search.semantic_search_notes",
|
||||
AsyncMock(return_value=[(0.5, fake)]),
|
||||
):
|
||||
out = await search(q="x")
|
||||
@@ -74,7 +74,7 @@ async def test_fable_search_content_type_filters_at_service_layer():
|
||||
"""content_type maps to the is_task kwarg passed to the service."""
|
||||
_user_id_ctx.set(7)
|
||||
mock_search = AsyncMock(return_value=[])
|
||||
with patch("fabledassistant.mcp.tools.search.semantic_search_notes", mock_search):
|
||||
with patch("scribe.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
|
||||
|
||||
@@ -91,7 +91,7 @@ async def test_fable_search_content_type_filters_at_service_layer():
|
||||
async def test_fable_search_limit_is_clamped():
|
||||
_user_id_ctx.set(7)
|
||||
mock_search = AsyncMock(return_value=[])
|
||||
with patch("fabledassistant.mcp.tools.search.semantic_search_notes", mock_search):
|
||||
with patch("scribe.mcp.tools.search.semantic_search_notes", mock_search):
|
||||
await search(q="x", limit=999)
|
||||
assert mock_search.call_args.kwargs["limit"] == 50
|
||||
|
||||
|
||||
@@ -7,8 +7,8 @@ from unittest.mock import AsyncMock, MagicMock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
from fabledassistant.mcp._context import _user_id_ctx
|
||||
from fabledassistant.mcp.tools.tags import list_tags, _aggregate_tag_counts
|
||||
from scribe.mcp._context import _user_id_ctx
|
||||
from scribe.mcp.tools.tags import list_tags, _aggregate_tag_counts
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
@@ -41,7 +41,7 @@ async def test_fable_list_tags_returns_sorted_by_count_desc():
|
||||
mock_session.__aexit__ = AsyncMock(return_value=False)
|
||||
mock_session.execute = AsyncMock(return_value=mock_result)
|
||||
mock_ctx = MagicMock(return_value=mock_session)
|
||||
with patch("fabledassistant.mcp.tools.tags.async_session", mock_ctx):
|
||||
with patch("scribe.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}
|
||||
@@ -57,7 +57,7 @@ async def test_fable_list_tags_clamps_limit():
|
||||
mock_session.__aexit__ = AsyncMock(return_value=False)
|
||||
mock_session.execute = AsyncMock(return_value=mock_result)
|
||||
mock_ctx = MagicMock(return_value=mock_session)
|
||||
with patch("fabledassistant.mcp.tools.tags.async_session", mock_ctx):
|
||||
with patch("scribe.mcp.tools.tags.async_session", mock_ctx):
|
||||
# No exception — just exercises the clamping branch
|
||||
out = await list_tags(limit=99999)
|
||||
assert out["total"] == 0
|
||||
|
||||
@@ -4,8 +4,8 @@ from unittest.mock import AsyncMock, MagicMock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
from fabledassistant.mcp._context import _user_id_ctx
|
||||
from fabledassistant.mcp.tools.tasks import (
|
||||
from scribe.mcp._context import _user_id_ctx
|
||||
from scribe.mcp.tools.tasks import (
|
||||
list_tasks, get_task, create_task,
|
||||
update_task, add_task_log,
|
||||
)
|
||||
@@ -36,7 +36,7 @@ def _fake_task(*, parent_id: int | None = None, **overrides) -> MagicMock:
|
||||
async def test_list_tasks_passes_is_task_true_and_repackages():
|
||||
rows = [_fake_task(id=1), _fake_task(id=2)]
|
||||
mock = AsyncMock(return_value=(rows, 2))
|
||||
with patch("fabledassistant.mcp.tools.tasks.notes_svc.list_notes", mock):
|
||||
with patch("scribe.mcp.tools.tasks.notes_svc.list_notes", mock):
|
||||
out = await list_tasks()
|
||||
assert out["total"] == 2
|
||||
assert len(out["tasks"]) == 2
|
||||
@@ -46,7 +46,7 @@ async def test_list_tasks_passes_is_task_true_and_repackages():
|
||||
@pytest.mark.asyncio
|
||||
async def test_list_tasks_status_filter():
|
||||
mock = AsyncMock(return_value=([], 0))
|
||||
with patch("fabledassistant.mcp.tools.tasks.notes_svc.list_notes", mock):
|
||||
with patch("scribe.mcp.tools.tasks.notes_svc.list_notes", mock):
|
||||
await list_tasks(status="in_progress")
|
||||
assert mock.call_args.kwargs["status"] == "in_progress"
|
||||
|
||||
@@ -54,7 +54,7 @@ async def test_list_tasks_status_filter():
|
||||
@pytest.mark.asyncio
|
||||
async def test_list_tasks_empty_status_means_no_filter():
|
||||
mock = AsyncMock(return_value=([], 0))
|
||||
with patch("fabledassistant.mcp.tools.tasks.notes_svc.list_notes", mock):
|
||||
with patch("scribe.mcp.tools.tasks.notes_svc.list_notes", mock):
|
||||
await list_tasks(status="")
|
||||
assert mock.call_args.kwargs["status"] is None
|
||||
|
||||
@@ -63,7 +63,7 @@ async def test_list_tasks_empty_status_means_no_filter():
|
||||
async def test_get_task_with_no_parent_returns_null_parent_title():
|
||||
fake = _fake_task(id=5, title="solo", parent_id=None)
|
||||
with patch(
|
||||
"fabledassistant.mcp.tools.tasks.notes_svc.get_note",
|
||||
"scribe.mcp.tools.tasks.notes_svc.get_note",
|
||||
AsyncMock(return_value=fake),
|
||||
):
|
||||
out = await get_task(task_id=5)
|
||||
@@ -78,7 +78,7 @@ async def test_get_task_enriches_with_parent_title():
|
||||
parent = _fake_task(id=5, title="parent of 10", parent_id=None)
|
||||
# get_note is called twice: once for child, once for parent
|
||||
mock_get = AsyncMock(side_effect=[child, parent])
|
||||
with patch("fabledassistant.mcp.tools.tasks.notes_svc.get_note", mock_get):
|
||||
with patch("scribe.mcp.tools.tasks.notes_svc.get_note", mock_get):
|
||||
out = await get_task(task_id=10)
|
||||
assert out["parent_title"] == "parent of 10"
|
||||
assert mock_get.await_count == 2
|
||||
@@ -89,7 +89,7 @@ async def test_get_task_parent_missing_returns_null():
|
||||
"""If parent_id is set but the parent is gone (orphaned), parent_title is None."""
|
||||
child = _fake_task(id=10, parent_id=5)
|
||||
mock_get = AsyncMock(side_effect=[child, None])
|
||||
with patch("fabledassistant.mcp.tools.tasks.notes_svc.get_note", mock_get):
|
||||
with patch("scribe.mcp.tools.tasks.notes_svc.get_note", mock_get):
|
||||
out = await get_task(task_id=10)
|
||||
assert out["parent_title"] is None
|
||||
|
||||
@@ -97,7 +97,7 @@ async def test_get_task_parent_missing_returns_null():
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_task_raises_when_not_found():
|
||||
with patch(
|
||||
"fabledassistant.mcp.tools.tasks.notes_svc.get_note",
|
||||
"scribe.mcp.tools.tasks.notes_svc.get_note",
|
||||
AsyncMock(return_value=None),
|
||||
):
|
||||
with pytest.raises(ValueError, match="task 999 not found"):
|
||||
@@ -108,7 +108,7 @@ async def test_get_task_raises_when_not_found():
|
||||
async def test_create_task_passes_status():
|
||||
fake = _fake_task()
|
||||
mock = AsyncMock(return_value=fake)
|
||||
with patch("fabledassistant.mcp.tools.tasks.notes_svc.create_note", mock):
|
||||
with patch("scribe.mcp.tools.tasks.notes_svc.create_note", mock):
|
||||
await create_task(title="do x", status="todo")
|
||||
assert mock.call_args.kwargs["status"] == "todo"
|
||||
|
||||
@@ -117,7 +117,7 @@ async def test_create_task_passes_status():
|
||||
async def test_create_task_priority_empty_becomes_none():
|
||||
fake = _fake_task()
|
||||
mock = AsyncMock(return_value=fake)
|
||||
with patch("fabledassistant.mcp.tools.tasks.notes_svc.create_note", mock):
|
||||
with patch("scribe.mcp.tools.tasks.notes_svc.create_note", mock):
|
||||
await create_task(title="x", priority="")
|
||||
assert mock.call_args.kwargs["priority"] is None
|
||||
|
||||
@@ -126,7 +126,7 @@ async def test_create_task_priority_empty_becomes_none():
|
||||
async def test_create_task_zero_id_sentinels_become_none():
|
||||
fake = _fake_task()
|
||||
mock = AsyncMock(return_value=fake)
|
||||
with patch("fabledassistant.mcp.tools.tasks.notes_svc.create_note", mock):
|
||||
with patch("scribe.mcp.tools.tasks.notes_svc.create_note", mock):
|
||||
await create_task(title="x", project_id=0, milestone_id=0, parent_id=0)
|
||||
assert mock.call_args.kwargs["project_id"] is None
|
||||
assert mock.call_args.kwargs["milestone_id"] is None
|
||||
@@ -137,7 +137,7 @@ async def test_create_task_zero_id_sentinels_become_none():
|
||||
async def test_update_task_only_sends_non_default_fields():
|
||||
fake = _fake_task()
|
||||
mock = AsyncMock(return_value=fake)
|
||||
with patch("fabledassistant.mcp.tools.tasks.notes_svc.update_note", mock):
|
||||
with patch("scribe.mcp.tools.tasks.notes_svc.update_note", mock):
|
||||
await update_task(task_id=1, status="done")
|
||||
args, kwargs = mock.call_args
|
||||
assert args == (7, 1)
|
||||
@@ -149,7 +149,7 @@ async def test_update_task_empty_priority_is_omitted():
|
||||
"""Priority="" is "leave unchanged" — must not reach service as empty string."""
|
||||
fake = _fake_task()
|
||||
mock = AsyncMock(return_value=fake)
|
||||
with patch("fabledassistant.mcp.tools.tasks.notes_svc.update_note", mock):
|
||||
with patch("scribe.mcp.tools.tasks.notes_svc.update_note", mock):
|
||||
await update_task(task_id=1, status="done", priority="")
|
||||
assert "priority" not in mock.call_args.kwargs
|
||||
|
||||
@@ -157,7 +157,7 @@ async def test_update_task_empty_priority_is_omitted():
|
||||
@pytest.mark.asyncio
|
||||
async def test_update_task_raises_when_not_found():
|
||||
with patch(
|
||||
"fabledassistant.mcp.tools.tasks.notes_svc.update_note",
|
||||
"scribe.mcp.tools.tasks.notes_svc.update_note",
|
||||
AsyncMock(return_value=None),
|
||||
):
|
||||
with pytest.raises(ValueError, match="task 999 not found"):
|
||||
@@ -169,7 +169,7 @@ async def test_update_task_milestone_zero_is_omitted():
|
||||
"""milestone_id=0 is 'leave unchanged' — must not reach the service."""
|
||||
fake = _fake_task()
|
||||
mock = AsyncMock(return_value=fake)
|
||||
with patch("fabledassistant.mcp.tools.tasks.notes_svc.update_note", mock):
|
||||
with patch("scribe.mcp.tools.tasks.notes_svc.update_note", mock):
|
||||
await update_task(task_id=1, milestone_id=0)
|
||||
assert "milestone_id" not in mock.call_args.kwargs
|
||||
|
||||
@@ -178,7 +178,7 @@ async def test_update_task_milestone_zero_is_omitted():
|
||||
async def test_update_task_milestone_positive_is_set():
|
||||
fake = _fake_task()
|
||||
mock = AsyncMock(return_value=fake)
|
||||
with patch("fabledassistant.mcp.tools.tasks.notes_svc.update_note", mock):
|
||||
with patch("scribe.mcp.tools.tasks.notes_svc.update_note", mock):
|
||||
await update_task(task_id=1, milestone_id=42)
|
||||
assert mock.call_args.kwargs["milestone_id"] == 42
|
||||
|
||||
@@ -188,7 +188,7 @@ async def test_update_task_milestone_negative_one_clears():
|
||||
"""milestone_id=-1 clears the milestone (sets the column NULL)."""
|
||||
fake = _fake_task()
|
||||
mock = AsyncMock(return_value=fake)
|
||||
with patch("fabledassistant.mcp.tools.tasks.notes_svc.update_note", mock):
|
||||
with patch("scribe.mcp.tools.tasks.notes_svc.update_note", mock):
|
||||
await update_task(task_id=1, milestone_id=-1)
|
||||
assert mock.call_args.kwargs["milestone_id"] is None
|
||||
|
||||
@@ -198,7 +198,7 @@ async def test_update_task_clearing_project_also_clears_milestone():
|
||||
"""project_id=-1 clears the project and, with it, the milestone."""
|
||||
fake = _fake_task()
|
||||
mock = AsyncMock(return_value=fake)
|
||||
with patch("fabledassistant.mcp.tools.tasks.notes_svc.update_note", mock):
|
||||
with patch("scribe.mcp.tools.tasks.notes_svc.update_note", mock):
|
||||
await update_task(task_id=1, project_id=-1)
|
||||
assert mock.call_args.kwargs["project_id"] is None
|
||||
assert mock.call_args.kwargs["milestone_id"] is None
|
||||
@@ -214,7 +214,7 @@ async def test_add_task_log_returns_log_dict():
|
||||
# No to_dict on TaskLog model (per task_logs.py inspection) — fall through to manual dict
|
||||
del log.to_dict
|
||||
with patch(
|
||||
"fabledassistant.mcp.tools.tasks.task_logs_svc.create_log",
|
||||
"scribe.mcp.tools.tasks.task_logs_svc.create_log",
|
||||
AsyncMock(return_value=log),
|
||||
):
|
||||
out = await add_task_log(task_id=1, content="checkpoint")
|
||||
@@ -228,7 +228,7 @@ async def test_add_task_log_returns_log_dict():
|
||||
async def test_add_task_log_propagates_value_error_when_task_missing():
|
||||
"""create_log raises ValueError if the task doesn't exist; we let it through."""
|
||||
with patch(
|
||||
"fabledassistant.mcp.tools.tasks.task_logs_svc.create_log",
|
||||
"scribe.mcp.tools.tasks.task_logs_svc.create_log",
|
||||
AsyncMock(side_effect=ValueError("Task 999 not found")),
|
||||
):
|
||||
with pytest.raises(ValueError):
|
||||
|
||||
@@ -2,7 +2,7 @@ from unittest.mock import AsyncMock, MagicMock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
from fabledassistant.mcp._context import _user_id_ctx
|
||||
from scribe.mcp._context import _user_id_ctx
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
@@ -21,8 +21,8 @@ def _fake_note(task_kind="work"):
|
||||
@pytest.mark.asyncio
|
||||
async def test_create_task_passes_kind():
|
||||
mock = AsyncMock(return_value=_fake_note(task_kind="plan"))
|
||||
with patch("fabledassistant.mcp.tools.tasks.notes_svc.create_note", mock):
|
||||
from fabledassistant.mcp.tools.tasks import create_task
|
||||
with patch("scribe.mcp.tools.tasks.notes_svc.create_note", mock):
|
||||
from scribe.mcp.tools.tasks import create_task
|
||||
await create_task(title="P", kind="plan")
|
||||
assert mock.call_args.kwargs["task_kind"] == "plan"
|
||||
|
||||
@@ -30,8 +30,8 @@ async def test_create_task_passes_kind():
|
||||
@pytest.mark.asyncio
|
||||
async def test_list_tasks_passes_kind_filter():
|
||||
mock = AsyncMock(return_value=([], 0))
|
||||
with patch("fabledassistant.mcp.tools.tasks.notes_svc.list_notes", mock):
|
||||
from fabledassistant.mcp.tools.tasks import list_tasks
|
||||
with patch("scribe.mcp.tools.tasks.notes_svc.list_notes", mock):
|
||||
from scribe.mcp.tools.tasks import list_tasks
|
||||
await list_tasks(kind="plan")
|
||||
assert mock.call_args.kwargs["task_kind"] == "plan"
|
||||
|
||||
@@ -39,7 +39,7 @@ async def test_list_tasks_passes_kind_filter():
|
||||
@pytest.mark.asyncio
|
||||
async def test_list_tasks_kind_empty_means_no_filter():
|
||||
mock = AsyncMock(return_value=([], 0))
|
||||
with patch("fabledassistant.mcp.tools.tasks.notes_svc.list_notes", mock):
|
||||
from fabledassistant.mcp.tools.tasks import list_tasks
|
||||
with patch("scribe.mcp.tools.tasks.notes_svc.list_notes", mock):
|
||||
from scribe.mcp.tools.tasks import list_tasks
|
||||
await list_tasks()
|
||||
assert mock.call_args.kwargs["task_kind"] is None
|
||||
|
||||
@@ -3,7 +3,7 @@ from unittest.mock import AsyncMock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
from fabledassistant.mcp._context import _user_id_ctx
|
||||
from scribe.mcp._context import _user_id_ctx
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
@@ -15,18 +15,18 @@ def _bind_user():
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_list_trash_wraps_batches():
|
||||
with patch("fabledassistant.mcp.tools.trash.trash_svc.list_trash",
|
||||
with patch("scribe.mcp.tools.trash.trash_svc.list_trash",
|
||||
AsyncMock(return_value=[{"batch_id": "b1", "count": 2}])):
|
||||
from fabledassistant.mcp.tools.trash import list_trash
|
||||
from scribe.mcp.tools.trash import list_trash
|
||||
out = await list_trash()
|
||||
assert out["batches"][0]["batch_id"] == "b1"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_restore_delegates():
|
||||
with patch("fabledassistant.mcp.tools.trash.trash_svc.restore",
|
||||
with patch("scribe.mcp.tools.trash.trash_svc.restore",
|
||||
AsyncMock(return_value=3)) as mock:
|
||||
from fabledassistant.mcp.tools.trash import restore
|
||||
from scribe.mcp.tools.trash import restore
|
||||
out = await restore("b1")
|
||||
assert out == {"restored": 3, "batch_id": "b1"}
|
||||
assert mock.call_args.args == (7, "b1")
|
||||
@@ -34,8 +34,8 @@ async def test_restore_delegates():
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_purge_trash_requires_confirmed():
|
||||
with patch("fabledassistant.mcp.tools.trash.trash_svc.purge", AsyncMock()) as mock:
|
||||
from fabledassistant.mcp.tools.trash import purge_trash
|
||||
with patch("scribe.mcp.tools.trash.trash_svc.purge", AsyncMock()) as mock:
|
||||
from scribe.mcp.tools.trash import purge_trash
|
||||
out = await purge_trash("b1", confirmed=False)
|
||||
assert out.get("confirmed_required") is True
|
||||
assert not mock.called
|
||||
@@ -43,16 +43,16 @@ async def test_purge_trash_requires_confirmed():
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_purge_trash_when_confirmed():
|
||||
with patch("fabledassistant.mcp.tools.trash.trash_svc.purge",
|
||||
with patch("scribe.mcp.tools.trash.trash_svc.purge",
|
||||
AsyncMock(return_value=5)) as mock:
|
||||
from fabledassistant.mcp.tools.trash import purge_trash
|
||||
from scribe.mcp.tools.trash import purge_trash
|
||||
out = await purge_trash("b1", confirmed=True)
|
||||
assert out == {"purged": 5, "batch_id": "b1"}
|
||||
assert mock.called
|
||||
|
||||
|
||||
def test_register_attaches_three_tools():
|
||||
from fabledassistant.mcp.tools.trash import register
|
||||
from scribe.mcp.tools.trash import register
|
||||
names: list[str] = []
|
||||
|
||||
class FakeMCP:
|
||||
|
||||
@@ -34,10 +34,10 @@ async def test_update_note_persists_description():
|
||||
mock_note.project_id = None
|
||||
|
||||
with patch(
|
||||
"fabledassistant.services.notes.async_session",
|
||||
"scribe.services.notes.async_session",
|
||||
return_value=_mock_session_for_update(mock_note),
|
||||
):
|
||||
from fabledassistant.services.notes import update_note
|
||||
from scribe.services.notes import update_note
|
||||
await update_note(1, 1, description="the goal text")
|
||||
|
||||
assert mock_note.description == "the goal text"
|
||||
@@ -64,9 +64,9 @@ async def test_create_note_forwards_description_to_model():
|
||||
mock_session.__aexit__ = AsyncMock(return_value=False)
|
||||
|
||||
with patch(
|
||||
"fabledassistant.services.notes.async_session", return_value=mock_session
|
||||
), patch("fabledassistant.services.notes.Note", FakeNote):
|
||||
from fabledassistant.services.notes import create_note
|
||||
"scribe.services.notes.async_session", return_value=mock_session
|
||||
), patch("scribe.services.notes.Note", FakeNote):
|
||||
from scribe.services.notes import create_note
|
||||
await create_note(
|
||||
user_id=1, title="renew cert", description="the goal text",
|
||||
)
|
||||
|
||||
+31
-31
@@ -21,8 +21,8 @@ async def test_update_note_sets_started_at_on_in_progress():
|
||||
mock_session.__aenter__ = AsyncMock(return_value=mock_session)
|
||||
mock_session.__aexit__ = AsyncMock(return_value=False)
|
||||
|
||||
with patch("fabledassistant.services.notes.async_session", return_value=mock_session):
|
||||
from fabledassistant.services.notes import update_note
|
||||
with patch("scribe.services.notes.async_session", return_value=mock_session):
|
||||
from scribe.services.notes import update_note
|
||||
await update_note(1, 1, status="in_progress")
|
||||
|
||||
assert mock_note.started_at is not None
|
||||
@@ -45,8 +45,8 @@ async def test_update_note_sets_completed_at_on_done():
|
||||
mock_session.__aenter__ = AsyncMock(return_value=mock_session)
|
||||
mock_session.__aexit__ = AsyncMock(return_value=False)
|
||||
|
||||
with patch("fabledassistant.services.notes.async_session", return_value=mock_session):
|
||||
from fabledassistant.services.notes import update_note
|
||||
with patch("scribe.services.notes.async_session", return_value=mock_session):
|
||||
from scribe.services.notes import update_note
|
||||
await update_note(1, 1, status="done")
|
||||
|
||||
assert mock_note.completed_at is not None
|
||||
@@ -69,8 +69,8 @@ async def test_update_note_clears_timestamps_on_todo():
|
||||
mock_session.__aenter__ = AsyncMock(return_value=mock_session)
|
||||
mock_session.__aexit__ = AsyncMock(return_value=False)
|
||||
|
||||
with patch("fabledassistant.services.notes.async_session", return_value=mock_session):
|
||||
from fabledassistant.services.notes import update_note
|
||||
with patch("scribe.services.notes.async_session", return_value=mock_session):
|
||||
from scribe.services.notes import update_note
|
||||
await update_note(1, 1, status="todo")
|
||||
|
||||
assert mock_note.started_at is None
|
||||
@@ -94,8 +94,8 @@ async def test_update_note_preserves_started_at_if_already_set():
|
||||
mock_session.__aenter__ = AsyncMock(return_value=mock_session)
|
||||
mock_session.__aexit__ = AsyncMock(return_value=False)
|
||||
|
||||
with patch("fabledassistant.services.notes.async_session", return_value=mock_session):
|
||||
from fabledassistant.services.notes import update_note
|
||||
with patch("scribe.services.notes.async_session", return_value=mock_session):
|
||||
from scribe.services.notes import update_note
|
||||
await update_note(1, 1, status="in_progress")
|
||||
|
||||
assert mock_note.started_at == original_start
|
||||
@@ -107,42 +107,42 @@ import pytest
|
||||
|
||||
|
||||
def test_validate_interval_rule_valid():
|
||||
from fabledassistant.services.recurrence import validate_recurrence_rule
|
||||
from scribe.services.recurrence import validate_recurrence_rule
|
||||
validate_recurrence_rule({"type": "interval", "every": 3, "unit": "month"}) # no error
|
||||
|
||||
|
||||
def test_validate_interval_rule_bad_unit():
|
||||
from fabledassistant.services.recurrence import validate_recurrence_rule
|
||||
from scribe.services.recurrence import validate_recurrence_rule
|
||||
with pytest.raises(ValueError, match="unit"):
|
||||
validate_recurrence_rule({"type": "interval", "every": 3, "unit": "fortnight"})
|
||||
|
||||
|
||||
def test_validate_interval_rule_bad_every():
|
||||
from fabledassistant.services.recurrence import validate_recurrence_rule
|
||||
from scribe.services.recurrence import validate_recurrence_rule
|
||||
with pytest.raises(ValueError, match="every"):
|
||||
validate_recurrence_rule({"type": "interval", "every": 0, "unit": "week"})
|
||||
|
||||
|
||||
def test_validate_calendar_monthly_valid():
|
||||
from fabledassistant.services.recurrence import validate_recurrence_rule
|
||||
from scribe.services.recurrence import validate_recurrence_rule
|
||||
validate_recurrence_rule({"type": "calendar", "unit": "month", "day_of_month": 1})
|
||||
|
||||
|
||||
def test_validate_calendar_annual_valid():
|
||||
from fabledassistant.services.recurrence import validate_recurrence_rule
|
||||
from scribe.services.recurrence import validate_recurrence_rule
|
||||
validate_recurrence_rule(
|
||||
{"type": "calendar", "unit": "year", "day_of_month": 15, "month": 6}
|
||||
)
|
||||
|
||||
|
||||
def test_validate_calendar_bad_day():
|
||||
from fabledassistant.services.recurrence import validate_recurrence_rule
|
||||
from scribe.services.recurrence import validate_recurrence_rule
|
||||
with pytest.raises(ValueError, match="day_of_month"):
|
||||
validate_recurrence_rule({"type": "calendar", "unit": "month", "day_of_month": 32})
|
||||
|
||||
|
||||
def test_validate_calendar_annual_missing_month():
|
||||
from fabledassistant.services.recurrence import validate_recurrence_rule
|
||||
from scribe.services.recurrence import validate_recurrence_rule
|
||||
with pytest.raises(ValueError, match="month"):
|
||||
validate_recurrence_rule(
|
||||
{"type": "calendar", "unit": "year", "day_of_month": 15}
|
||||
@@ -150,7 +150,7 @@ def test_validate_calendar_annual_missing_month():
|
||||
|
||||
|
||||
def test_validate_unknown_type():
|
||||
from fabledassistant.services.recurrence import validate_recurrence_rule
|
||||
from scribe.services.recurrence import validate_recurrence_rule
|
||||
with pytest.raises(ValueError, match="type"):
|
||||
validate_recurrence_rule({"type": "cron", "every": 1, "unit": "day"})
|
||||
|
||||
@@ -158,60 +158,60 @@ def test_validate_unknown_type():
|
||||
# ── calculate_next_due ────────────────────────────────────────────────────────
|
||||
|
||||
def test_calculate_next_due_interval_days():
|
||||
from fabledassistant.services.recurrence import calculate_next_due
|
||||
from scribe.services.recurrence import calculate_next_due
|
||||
rule = {"type": "interval", "every": 7, "unit": "day"}
|
||||
assert calculate_next_due(rule, date(2026, 3, 27)) == date(2026, 4, 3)
|
||||
|
||||
|
||||
def test_calculate_next_due_interval_weeks():
|
||||
from fabledassistant.services.recurrence import calculate_next_due
|
||||
from scribe.services.recurrence import calculate_next_due
|
||||
rule = {"type": "interval", "every": 2, "unit": "week"}
|
||||
assert calculate_next_due(rule, date(2026, 3, 27)) == date(2026, 4, 10)
|
||||
|
||||
|
||||
def test_calculate_next_due_interval_months():
|
||||
from fabledassistant.services.recurrence import calculate_next_due
|
||||
from scribe.services.recurrence import calculate_next_due
|
||||
rule = {"type": "interval", "every": 3, "unit": "month"}
|
||||
assert calculate_next_due(rule, date(2026, 3, 1)) == date(2026, 6, 1)
|
||||
|
||||
|
||||
def test_calculate_next_due_interval_months_end_of_month():
|
||||
"""Month addition clamps to last day when target month is shorter."""
|
||||
from fabledassistant.services.recurrence import calculate_next_due
|
||||
from scribe.services.recurrence import calculate_next_due
|
||||
rule = {"type": "interval", "every": 1, "unit": "month"}
|
||||
assert calculate_next_due(rule, date(2026, 1, 31)) == date(2026, 2, 28)
|
||||
|
||||
|
||||
def test_calculate_next_due_interval_years():
|
||||
from fabledassistant.services.recurrence import calculate_next_due
|
||||
from scribe.services.recurrence import calculate_next_due
|
||||
rule = {"type": "interval", "every": 1, "unit": "year"}
|
||||
assert calculate_next_due(rule, date(2026, 3, 27)) == date(2027, 3, 27)
|
||||
|
||||
|
||||
def test_calculate_next_due_calendar_monthly_future_day():
|
||||
"""If day_of_month is later this month, return that date."""
|
||||
from fabledassistant.services.recurrence import calculate_next_due
|
||||
from scribe.services.recurrence import calculate_next_due
|
||||
rule = {"type": "calendar", "unit": "month", "day_of_month": 28}
|
||||
assert calculate_next_due(rule, date(2026, 3, 1)) == date(2026, 3, 28)
|
||||
|
||||
|
||||
def test_calculate_next_due_calendar_monthly_same_or_past_day():
|
||||
"""If day_of_month is today or earlier, advance to next month."""
|
||||
from fabledassistant.services.recurrence import calculate_next_due
|
||||
from scribe.services.recurrence import calculate_next_due
|
||||
rule = {"type": "calendar", "unit": "month", "day_of_month": 1}
|
||||
assert calculate_next_due(rule, date(2026, 3, 1)) == date(2026, 4, 1)
|
||||
|
||||
|
||||
def test_calculate_next_due_calendar_annual_future():
|
||||
"""Annual date later this year is returned."""
|
||||
from fabledassistant.services.recurrence import calculate_next_due
|
||||
from scribe.services.recurrence import calculate_next_due
|
||||
rule = {"type": "calendar", "unit": "year", "day_of_month": 15, "month": 6}
|
||||
assert calculate_next_due(rule, date(2026, 3, 27)) == date(2026, 6, 15)
|
||||
|
||||
|
||||
def test_calculate_next_due_calendar_annual_past():
|
||||
"""Annual date already passed this year → next year."""
|
||||
from fabledassistant.services.recurrence import calculate_next_due
|
||||
from scribe.services.recurrence import calculate_next_due
|
||||
rule = {"type": "calendar", "unit": "year", "day_of_month": 15, "month": 1}
|
||||
assert calculate_next_due(rule, date(2026, 3, 27)) == date(2027, 1, 15)
|
||||
|
||||
@@ -248,15 +248,15 @@ async def test_spawn_recurring_tasks_creates_child():
|
||||
mock_child.body = ""
|
||||
|
||||
with (
|
||||
patch("fabledassistant.services.recurrence.async_session", return_value=mock_session),
|
||||
patch("scribe.services.recurrence.async_session", return_value=mock_session),
|
||||
patch(
|
||||
"fabledassistant.services.notes.create_note",
|
||||
"scribe.services.notes.create_note",
|
||||
new_callable=AsyncMock,
|
||||
return_value=mock_child,
|
||||
) as mock_create,
|
||||
patch("fabledassistant.services.embeddings.upsert_note_embedding"),
|
||||
patch("scribe.services.embeddings.upsert_note_embedding"),
|
||||
):
|
||||
from fabledassistant.services.recurrence import spawn_recurring_tasks
|
||||
from scribe.services.recurrence import spawn_recurring_tasks
|
||||
count = await spawn_recurring_tasks()
|
||||
|
||||
assert count == 1
|
||||
@@ -282,8 +282,8 @@ async def test_list_notes_multi_status_builds_in_clause():
|
||||
mock_session.__aenter__ = AsyncMock(return_value=mock_session)
|
||||
mock_session.__aexit__ = AsyncMock(return_value=False)
|
||||
|
||||
with patch("fabledassistant.services.notes.async_session", return_value=mock_session):
|
||||
from fabledassistant.services.notes import list_notes
|
||||
with patch("scribe.services.notes.async_session", return_value=mock_session):
|
||||
from scribe.services.notes import list_notes
|
||||
notes, total = await list_notes(1, status=["todo", "in_progress"], is_task=True)
|
||||
|
||||
assert total == 0
|
||||
|
||||
@@ -2,17 +2,17 @@
|
||||
|
||||
|
||||
def test_dashboard_blueprint_registered():
|
||||
from fabledassistant.routes.dashboard import dashboard_bp
|
||||
from scribe.routes.dashboard import dashboard_bp
|
||||
assert dashboard_bp.name == "dashboard"
|
||||
assert dashboard_bp.url_prefix == "/api/dashboard"
|
||||
|
||||
|
||||
def test_dashboard_blueprint_registered_in_app():
|
||||
from fabledassistant.app import create_app
|
||||
from scribe.app import create_app
|
||||
app = create_app()
|
||||
assert "dashboard" in app.blueprints
|
||||
|
||||
|
||||
def test_dashboard_handler_callable():
|
||||
from fabledassistant.routes import dashboard as dashboard_routes
|
||||
from scribe.routes import dashboard as dashboard_routes
|
||||
assert callable(dashboard_routes.get_dashboard)
|
||||
|
||||
@@ -2,11 +2,11 @@ import inspect
|
||||
|
||||
|
||||
def test_planning_handler_callable():
|
||||
from fabledassistant.routes import tasks as tasks_routes
|
||||
from scribe.routes import tasks as tasks_routes
|
||||
assert callable(tasks_routes.start_planning_route)
|
||||
|
||||
|
||||
def test_planning_service_signature():
|
||||
from fabledassistant.services.planning import start_planning
|
||||
from scribe.services.planning import start_planning
|
||||
params = inspect.signature(start_planning).parameters
|
||||
assert "user_id" in params and "project_id" in params and "title" in params
|
||||
|
||||
@@ -9,19 +9,19 @@ import inspect
|
||||
|
||||
|
||||
def test_rulebooks_blueprint_registered():
|
||||
from fabledassistant.routes.rulebooks import rulebooks_bp
|
||||
from scribe.routes.rulebooks import rulebooks_bp
|
||||
assert rulebooks_bp.name == "rulebooks"
|
||||
assert rulebooks_bp.url_prefix == "/api"
|
||||
|
||||
|
||||
def test_rulebooks_blueprint_registered_in_app():
|
||||
from fabledassistant.app import create_app
|
||||
from scribe.app import create_app
|
||||
app = create_app()
|
||||
assert "rulebooks" in app.blueprints
|
||||
|
||||
|
||||
def test_rulebook_handlers_callable():
|
||||
from fabledassistant.routes import rulebooks as rb_routes
|
||||
from scribe.routes import rulebooks as rb_routes
|
||||
for name in (
|
||||
"list_rulebooks", "create_rulebook", "get_rulebook",
|
||||
"update_rulebook", "delete_rulebook",
|
||||
@@ -30,7 +30,7 @@ def test_rulebook_handlers_callable():
|
||||
|
||||
|
||||
def test_topic_handlers_callable():
|
||||
from fabledassistant.routes import rulebooks as rb_routes
|
||||
from scribe.routes import rulebooks as rb_routes
|
||||
for name in (
|
||||
"list_topics", "create_topic", "update_topic", "delete_topic",
|
||||
):
|
||||
@@ -39,7 +39,7 @@ def test_topic_handlers_callable():
|
||||
|
||||
def test_service_signatures_require_user_id():
|
||||
"""Routes must call services with user_id — verify the contract."""
|
||||
from fabledassistant.services import rulebooks as svc
|
||||
from scribe.services import rulebooks as svc
|
||||
for fn_name in (
|
||||
"create_rulebook", "list_rulebooks", "get_rulebook",
|
||||
"update_rulebook", "delete_rulebook", "find_rulebook_by_title",
|
||||
@@ -57,7 +57,7 @@ def test_service_signatures_require_user_id():
|
||||
|
||||
def test_rule_model_carries_project_id_and_topic_id_nullable():
|
||||
"""Migration 0059 made topic_id nullable and added project_id."""
|
||||
from fabledassistant.models.rulebook import Rule
|
||||
from scribe.models.rulebook import Rule
|
||||
assert "project_id" in Rule.__table__.columns
|
||||
assert Rule.__table__.columns["topic_id"].nullable is True
|
||||
assert Rule.__table__.columns["project_id"].nullable is True
|
||||
@@ -65,13 +65,13 @@ def test_rule_model_carries_project_id_and_topic_id_nullable():
|
||||
|
||||
def test_create_project_rule_route_exists():
|
||||
"""POST /api/projects/<id>/rules — the frontend fast-path endpoint."""
|
||||
from fabledassistant.routes import rulebooks as rb_routes
|
||||
from scribe.routes import rulebooks as rb_routes
|
||||
assert callable(getattr(rb_routes, "create_project_rule"))
|
||||
|
||||
|
||||
def test_suppression_route_handlers_exist():
|
||||
"""The 4 suppression endpoint handlers are registered as Python callables."""
|
||||
from fabledassistant.routes import rulebooks as rb_routes
|
||||
from scribe.routes import rulebooks as rb_routes
|
||||
for name in (
|
||||
"suppress_project_rule", "unsuppress_project_rule",
|
||||
"suppress_project_topic", "unsuppress_project_topic",
|
||||
@@ -83,7 +83,7 @@ def test_suppression_association_tables_declared():
|
||||
"""Migration 0060 created two new association tables; the models module
|
||||
must declare matching Table() objects so the rest of the service layer
|
||||
can reference them via .c.<column>."""
|
||||
from fabledassistant.models import rulebook as rb_models
|
||||
from scribe.models import rulebook as rb_models
|
||||
for tbl_name in ("project_rule_suppressions", "project_topic_suppressions"):
|
||||
tbl = getattr(rb_models, tbl_name, None)
|
||||
assert tbl is not None, f"models.rulebook missing {tbl_name}"
|
||||
@@ -94,7 +94,7 @@ def test_suppression_association_tables_declared():
|
||||
|
||||
def test_rulebook_model_carries_always_on():
|
||||
"""Migration 0058 added rulebooks.always_on — verify the model declares it."""
|
||||
from fabledassistant.models.rulebook import Rulebook
|
||||
from scribe.models.rulebook import Rulebook
|
||||
assert "always_on" in Rulebook.__table__.columns
|
||||
col = Rulebook.__table__.columns["always_on"]
|
||||
assert col.nullable is False
|
||||
@@ -107,13 +107,13 @@ def test_update_rulebook_route_accepts_always_on():
|
||||
include always_on or toggling from the UI silently drops the field.
|
||||
"""
|
||||
import inspect as _inspect
|
||||
from fabledassistant.routes import rulebooks as rb_routes
|
||||
from scribe.routes import rulebooks as rb_routes
|
||||
src = _inspect.getsource(rb_routes.update_rulebook)
|
||||
assert "always_on" in src, "update_rulebook handler missing always_on in field whitelist"
|
||||
|
||||
|
||||
def test_rule_and_subscription_handlers_callable():
|
||||
from fabledassistant.routes import rulebooks as rb_routes
|
||||
from scribe.routes import rulebooks as rb_routes
|
||||
for name in (
|
||||
"list_rules", "create_rule", "get_rule", "update_rule", "delete_rule",
|
||||
"subscribe_project", "unsubscribe_project", "get_project_rules",
|
||||
|
||||
@@ -2,16 +2,16 @@ import inspect
|
||||
|
||||
|
||||
def test_create_note_accepts_task_kind():
|
||||
from fabledassistant.services.notes import create_note
|
||||
from scribe.services.notes import create_note
|
||||
assert "task_kind" in inspect.signature(create_note).parameters
|
||||
|
||||
|
||||
def test_list_notes_accepts_task_kind():
|
||||
from fabledassistant.services.notes import list_notes
|
||||
from scribe.services.notes import list_notes
|
||||
assert "task_kind" in inspect.signature(list_notes).parameters
|
||||
|
||||
|
||||
def test_tasks_blueprint_registered_in_app():
|
||||
from fabledassistant.app import create_app
|
||||
from scribe.app import create_app
|
||||
app = create_app()
|
||||
assert "tasks" in app.blueprints
|
||||
|
||||
@@ -3,30 +3,30 @@ import inspect
|
||||
|
||||
|
||||
def test_trash_blueprint_registered():
|
||||
from fabledassistant.routes.trash import trash_bp
|
||||
from scribe.routes.trash import trash_bp
|
||||
assert trash_bp.name == "trash"
|
||||
assert trash_bp.url_prefix == "/api/trash"
|
||||
|
||||
|
||||
def test_trash_blueprint_registered_in_app():
|
||||
from fabledassistant.app import create_app
|
||||
from scribe.app import create_app
|
||||
app = create_app()
|
||||
assert "trash" in app.blueprints
|
||||
|
||||
|
||||
def test_trash_handlers_callable():
|
||||
from fabledassistant.routes import trash as trash_routes
|
||||
from scribe.routes import trash as trash_routes
|
||||
for name in ("list_trash", "restore_batch", "purge_batch"):
|
||||
assert callable(getattr(trash_routes, name))
|
||||
|
||||
|
||||
def test_trash_service_surface():
|
||||
from fabledassistant.services import trash as svc
|
||||
from scribe.services import trash as svc
|
||||
for fn_name in ("delete", "restore", "purge", "list_trash", "purge_expired", "alive"):
|
||||
assert hasattr(svc, fn_name), f"trash service missing {fn_name}"
|
||||
|
||||
|
||||
def test_delete_service_signature():
|
||||
from fabledassistant.services.trash import delete
|
||||
from scribe.services.trash import delete
|
||||
params = inspect.signature(delete).parameters
|
||||
assert "user_id" in params and "entity_type" in params and "entity_id" in params
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
"""Unit tests for the search route parameter mapping."""
|
||||
from fabledassistant.routes.search import _content_type_to_is_task
|
||||
from scribe.routes.search import _content_type_to_is_task
|
||||
|
||||
|
||||
def test_content_type_note():
|
||||
|
||||
@@ -10,7 +10,7 @@ import pytest
|
||||
|
||||
|
||||
def test_task_row_maps_fields():
|
||||
from fabledassistant.services.dashboard import _task_row
|
||||
from scribe.services.dashboard import _task_row
|
||||
n = MagicMock()
|
||||
n.id = 5
|
||||
n.title = "Wire reminders"
|
||||
@@ -23,7 +23,7 @@ def test_task_row_maps_fields():
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_safe_returns_value_then_empty_on_error():
|
||||
from fabledassistant.services.dashboard import _safe
|
||||
from scribe.services.dashboard import _safe
|
||||
|
||||
async def ok():
|
||||
return [1, 2, 3]
|
||||
@@ -39,7 +39,7 @@ async def test_safe_returns_value_then_empty_on_error():
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_build_dashboard_composes_sections():
|
||||
import fabledassistant.services.dashboard as dash
|
||||
import scribe.services.dashboard as dash
|
||||
with patch.object(dash, "_active_projects", AsyncMock(return_value=["P"])), \
|
||||
patch.object(dash, "_recently_completed", AsyncMock(return_value=["done"])), \
|
||||
patch.object(dash, "_upcoming_events", AsyncMock(return_value=["evt"])), \
|
||||
@@ -55,7 +55,7 @@ async def test_build_dashboard_composes_sections():
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_build_dashboard_isolates_failing_section():
|
||||
import fabledassistant.services.dashboard as dash
|
||||
import scribe.services.dashboard as dash
|
||||
with patch.object(dash, "_active_projects", AsyncMock(side_effect=RuntimeError("db down"))), \
|
||||
patch.object(dash, "_recently_completed", AsyncMock(return_value=["done"])), \
|
||||
patch.object(dash, "_upcoming_events", AsyncMock(return_value=[])), \
|
||||
|
||||
@@ -32,9 +32,9 @@ async def test_counts_include_process_in_facet_and_total():
|
||||
_scalar(1), # tasks
|
||||
_scalar(0), # plans
|
||||
])
|
||||
with patch("fabledassistant.services.knowledge.async_session") as cls:
|
||||
with patch("scribe.services.knowledge.async_session") as cls:
|
||||
cls.return_value = session
|
||||
from fabledassistant.services.knowledge import get_knowledge_counts
|
||||
from scribe.services.knowledge import get_knowledge_counts
|
||||
counts = await get_knowledge_counts(user_id=1)
|
||||
|
||||
assert counts["process"] == 2
|
||||
|
||||
@@ -35,9 +35,9 @@ async def test_resolve_process_by_numeric_id():
|
||||
session = _make_mock_session()
|
||||
# numeric id → first execute (id lookup) hits
|
||||
session.execute = AsyncMock(side_effect=[_result(first=note)])
|
||||
with patch("fabledassistant.services.notes.async_session") as cls:
|
||||
with patch("scribe.services.notes.async_session") as cls:
|
||||
cls.return_value = session
|
||||
from fabledassistant.services.notes import resolve_process
|
||||
from scribe.services.notes import resolve_process
|
||||
found, candidates = await resolve_process(1, "5")
|
||||
assert found is note
|
||||
assert candidates == []
|
||||
@@ -50,9 +50,9 @@ async def test_resolve_process_exact_title_beats_substring():
|
||||
session = _make_mock_session()
|
||||
# non-digit → exact-title query (first execute) hits; substring never runs
|
||||
session.execute = AsyncMock(side_effect=[_result(first=note)])
|
||||
with patch("fabledassistant.services.notes.async_session") as cls:
|
||||
with patch("scribe.services.notes.async_session") as cls:
|
||||
cls.return_value = session
|
||||
from fabledassistant.services.notes import resolve_process
|
||||
from scribe.services.notes import resolve_process
|
||||
found, candidates = await resolve_process(1, "Drift Audit")
|
||||
assert found is note
|
||||
assert candidates == []
|
||||
@@ -66,9 +66,9 @@ async def test_resolve_process_substring_returns_candidates():
|
||||
session = _make_mock_session()
|
||||
# exact miss, then substring returns two (most-recent first)
|
||||
session.execute = AsyncMock(side_effect=[_result(first=None), _result(all_=[n1, n2])])
|
||||
with patch("fabledassistant.services.notes.async_session") as cls:
|
||||
with patch("scribe.services.notes.async_session") as cls:
|
||||
cls.return_value = session
|
||||
from fabledassistant.services.notes import resolve_process
|
||||
from scribe.services.notes import resolve_process
|
||||
found, candidates = await resolve_process(1, "drift")
|
||||
assert found is n1
|
||||
assert candidates == [{"id": 9, "title": "Drift Audit Notes"}]
|
||||
@@ -79,9 +79,9 @@ async def test_resolve_process_substring_returns_candidates():
|
||||
async def test_resolve_process_no_match():
|
||||
session = _make_mock_session()
|
||||
session.execute = AsyncMock(side_effect=[_result(first=None), _result(all_=[])])
|
||||
with patch("fabledassistant.services.notes.async_session") as cls:
|
||||
with patch("scribe.services.notes.async_session") as cls:
|
||||
cls.return_value = session
|
||||
from fabledassistant.services.notes import resolve_process
|
||||
from scribe.services.notes import resolve_process
|
||||
found, candidates = await resolve_process(1, "nope")
|
||||
assert found is None
|
||||
assert candidates == []
|
||||
|
||||
@@ -23,10 +23,10 @@ async def test_create_note_passes_task_kind_to_model():
|
||||
captured["task_kind"] = getattr(obj, "task_kind", "MISSING")
|
||||
|
||||
mock_session.add = MagicMock(side_effect=_capture_add)
|
||||
with patch("fabledassistant.services.notes.async_session") as mock_cls, \
|
||||
patch("fabledassistant.services.notes._maybe_reactivate_project", AsyncMock()):
|
||||
with patch("scribe.services.notes.async_session") as mock_cls, \
|
||||
patch("scribe.services.notes._maybe_reactivate_project", AsyncMock()):
|
||||
mock_cls.return_value = mock_session
|
||||
from fabledassistant.services.notes import create_note
|
||||
from scribe.services.notes import create_note
|
||||
await create_note(user_id=1, title="P", status="todo", task_kind="plan")
|
||||
assert captured["task_kind"] == "plan"
|
||||
|
||||
@@ -38,9 +38,9 @@ async def test_list_notes_filters_by_task_kind():
|
||||
exec_result = MagicMock()
|
||||
exec_result.scalars.return_value.all.return_value = []
|
||||
mock_session.execute = AsyncMock(return_value=exec_result)
|
||||
with patch("fabledassistant.services.notes.async_session") as mock_cls:
|
||||
with patch("scribe.services.notes.async_session") as mock_cls:
|
||||
mock_cls.return_value = mock_session
|
||||
from fabledassistant.services.notes import list_notes
|
||||
from scribe.services.notes import list_notes
|
||||
# Should not raise; task_kind is a recognized kwarg.
|
||||
rows, total = await list_notes(user_id=1, is_task=True, task_kind="plan")
|
||||
assert rows == []
|
||||
|
||||
@@ -13,15 +13,15 @@ async def test_start_planning_creates_plan_task_and_returns_rules():
|
||||
"truncated": False,
|
||||
"subscribed_rulebooks": [{"id": 2, "title": "FabledSword family"}],
|
||||
}
|
||||
with patch("fabledassistant.services.planning.notes_svc.create_note",
|
||||
with patch("scribe.services.planning.notes_svc.create_note",
|
||||
AsyncMock(return_value=fake_note)) as mock_create, \
|
||||
patch("fabledassistant.services.planning.rulebooks_svc.get_applicable_rules",
|
||||
patch("scribe.services.planning.rulebooks_svc.get_applicable_rules",
|
||||
AsyncMock(return_value=applicable)), \
|
||||
patch("fabledassistant.services.planning.notes_svc.list_notes",
|
||||
patch("scribe.services.planning.notes_svc.list_notes",
|
||||
AsyncMock(return_value=([], 3))), \
|
||||
patch("fabledassistant.services.planning.projects_svc.get_project",
|
||||
patch("scribe.services.planning.projects_svc.get_project",
|
||||
AsyncMock(return_value=MagicMock(goal="ship it"))):
|
||||
from fabledassistant.services.planning import start_planning
|
||||
from scribe.services.planning import start_planning
|
||||
out = await start_planning(user_id=7, project_id=3, title="Plan it")
|
||||
|
||||
# Created a plan-task (status set => task, kind=plan)
|
||||
@@ -39,8 +39,8 @@ async def test_start_planning_creates_plan_task_and_returns_rules():
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_start_planning_raises_when_project_not_found():
|
||||
with patch("fabledassistant.services.planning.projects_svc.get_project",
|
||||
with patch("scribe.services.planning.projects_svc.get_project",
|
||||
AsyncMock(return_value=None)):
|
||||
from fabledassistant.services.planning import start_planning
|
||||
from scribe.services.planning import start_planning
|
||||
with pytest.raises(ValueError, match="project 999 not found"):
|
||||
await start_planning(user_id=7, project_id=999, title="x")
|
||||
|
||||
@@ -36,9 +36,9 @@ def _fake_rulebook(id=1, owner_user_id=7, title="FabledSword family", descriptio
|
||||
@pytest.mark.asyncio
|
||||
async def test_create_rulebook_stores_to_db():
|
||||
mock_session = _make_mock_session()
|
||||
with patch("fabledassistant.services.rulebooks.async_session") as mock_cls:
|
||||
with patch("scribe.services.rulebooks.async_session") as mock_cls:
|
||||
mock_cls.return_value = mock_session
|
||||
from fabledassistant.services.rulebooks import create_rulebook
|
||||
from scribe.services.rulebooks import create_rulebook
|
||||
await create_rulebook(
|
||||
user_id=7, title="FabledSword family", description="rules for the family",
|
||||
)
|
||||
@@ -53,9 +53,9 @@ async def test_list_rulebooks_returns_owned_only():
|
||||
mock_result = MagicMock()
|
||||
mock_result.scalars.return_value.all.return_value = [rb]
|
||||
mock_session.execute = AsyncMock(return_value=mock_result)
|
||||
with patch("fabledassistant.services.rulebooks.async_session") as mock_cls:
|
||||
with patch("scribe.services.rulebooks.async_session") as mock_cls:
|
||||
mock_cls.return_value = mock_session
|
||||
from fabledassistant.services.rulebooks import list_rulebooks
|
||||
from scribe.services.rulebooks import list_rulebooks
|
||||
results = await list_rulebooks(user_id=7)
|
||||
assert len(results) == 1
|
||||
|
||||
@@ -67,9 +67,9 @@ async def test_get_rulebook_returns_none_when_not_owner():
|
||||
mock_result = MagicMock()
|
||||
mock_result.scalar_one_or_none.return_value = None
|
||||
mock_session.execute = AsyncMock(return_value=mock_result)
|
||||
with patch("fabledassistant.services.rulebooks.async_session") as mock_cls:
|
||||
with patch("scribe.services.rulebooks.async_session") as mock_cls:
|
||||
mock_cls.return_value = mock_session
|
||||
from fabledassistant.services.rulebooks import get_rulebook
|
||||
from scribe.services.rulebooks import get_rulebook
|
||||
result = await get_rulebook(rulebook_id=1, user_id=99)
|
||||
assert result is None
|
||||
|
||||
@@ -81,9 +81,9 @@ async def test_update_rulebook_only_sets_provided_fields():
|
||||
mock_result = MagicMock()
|
||||
mock_result.scalar_one_or_none.return_value = rb
|
||||
mock_session.execute = AsyncMock(return_value=mock_result)
|
||||
with patch("fabledassistant.services.rulebooks.async_session") as mock_cls:
|
||||
with patch("scribe.services.rulebooks.async_session") as mock_cls:
|
||||
mock_cls.return_value = mock_session
|
||||
from fabledassistant.services.rulebooks import update_rulebook
|
||||
from scribe.services.rulebooks import update_rulebook
|
||||
await update_rulebook(rulebook_id=1, user_id=7, title="new")
|
||||
assert rb.title == "new"
|
||||
|
||||
@@ -96,9 +96,9 @@ async def test_delete_rulebook_calls_delete():
|
||||
mock_result.scalar_one_or_none.return_value = rb
|
||||
mock_session.execute = AsyncMock(return_value=mock_result)
|
||||
mock_session.delete = AsyncMock()
|
||||
with patch("fabledassistant.services.rulebooks.async_session") as mock_cls:
|
||||
with patch("scribe.services.rulebooks.async_session") as mock_cls:
|
||||
mock_cls.return_value = mock_session
|
||||
from fabledassistant.services.rulebooks import delete_rulebook
|
||||
from scribe.services.rulebooks import delete_rulebook
|
||||
await delete_rulebook(rulebook_id=1, user_id=7)
|
||||
assert mock_session.delete.called
|
||||
|
||||
@@ -128,9 +128,9 @@ async def test_create_topic_requires_owned_rulebook():
|
||||
mock_result = MagicMock()
|
||||
mock_result.scalar_one_or_none.return_value = None
|
||||
mock_session.execute = AsyncMock(return_value=mock_result)
|
||||
with patch("fabledassistant.services.rulebooks.async_session") as mock_cls:
|
||||
with patch("scribe.services.rulebooks.async_session") as mock_cls:
|
||||
mock_cls.return_value = mock_session
|
||||
from fabledassistant.services.rulebooks import create_topic
|
||||
from scribe.services.rulebooks import create_topic
|
||||
with pytest.raises(ValueError, match="not found"):
|
||||
await create_topic(
|
||||
rulebook_id=999, user_id=7, title="git-workflow",
|
||||
@@ -150,9 +150,9 @@ async def test_list_topics_returns_topics_for_owned_rulebook():
|
||||
topic_result.scalars.return_value.all.return_value = [topic]
|
||||
mock_session.execute = AsyncMock(side_effect=[rb_result, topic_result])
|
||||
|
||||
with patch("fabledassistant.services.rulebooks.async_session") as mock_cls:
|
||||
with patch("scribe.services.rulebooks.async_session") as mock_cls:
|
||||
mock_cls.return_value = mock_session
|
||||
from fabledassistant.services.rulebooks import list_topics
|
||||
from scribe.services.rulebooks import list_topics
|
||||
results = await list_topics(rulebook_id=1, user_id=7)
|
||||
assert len(results) == 1
|
||||
assert results[0].title == "git-workflow"
|
||||
@@ -186,9 +186,9 @@ async def test_create_rule_requires_owned_topic():
|
||||
mock_result = MagicMock()
|
||||
mock_result.scalar_one_or_none.return_value = None # topic not found
|
||||
mock_session.execute = AsyncMock(return_value=mock_result)
|
||||
with patch("fabledassistant.services.rulebooks.async_session") as mock_cls:
|
||||
with patch("scribe.services.rulebooks.async_session") as mock_cls:
|
||||
mock_cls.return_value = mock_session
|
||||
from fabledassistant.services.rulebooks import create_rule
|
||||
from scribe.services.rulebooks import create_rule
|
||||
with pytest.raises(ValueError, match="topic .* not found"):
|
||||
await create_rule(
|
||||
topic_id=999, user_id=7, title="x", statement="y",
|
||||
@@ -203,9 +203,9 @@ async def test_list_rules_filters_by_topic_id():
|
||||
mock_result = MagicMock()
|
||||
mock_result.scalars.return_value.all.return_value = [rule]
|
||||
mock_session.execute = AsyncMock(return_value=mock_result)
|
||||
with patch("fabledassistant.services.rulebooks.async_session") as mock_cls:
|
||||
with patch("scribe.services.rulebooks.async_session") as mock_cls:
|
||||
mock_cls.return_value = mock_session
|
||||
from fabledassistant.services.rulebooks import list_rules
|
||||
from scribe.services.rulebooks import list_rules
|
||||
results = await list_rules(user_id=7, topic_id=10)
|
||||
assert len(results) == 1
|
||||
|
||||
@@ -216,9 +216,9 @@ async def test_get_rule_returns_none_when_not_owner():
|
||||
mock_result = MagicMock()
|
||||
mock_result.scalar_one_or_none.return_value = None
|
||||
mock_session.execute = AsyncMock(return_value=mock_result)
|
||||
with patch("fabledassistant.services.rulebooks.async_session") as mock_cls:
|
||||
with patch("scribe.services.rulebooks.async_session") as mock_cls:
|
||||
mock_cls.return_value = mock_session
|
||||
from fabledassistant.services.rulebooks import get_rule
|
||||
from scribe.services.rulebooks import get_rule
|
||||
result = await get_rule(rule_id=1, user_id=99)
|
||||
assert result is None
|
||||
|
||||
@@ -232,9 +232,9 @@ async def test_subscribe_project_requires_owned_rulebook():
|
||||
mock_result = MagicMock()
|
||||
mock_result.scalar_one_or_none.return_value = None
|
||||
mock_session.execute = AsyncMock(return_value=mock_result)
|
||||
with patch("fabledassistant.services.rulebooks.async_session") as mock_cls:
|
||||
with patch("scribe.services.rulebooks.async_session") as mock_cls:
|
||||
mock_cls.return_value = mock_session
|
||||
from fabledassistant.services.rulebooks import subscribe_project
|
||||
from scribe.services.rulebooks import subscribe_project
|
||||
with pytest.raises(ValueError, match="not found"):
|
||||
await subscribe_project(
|
||||
project_id=1, rulebook_id=999, user_id=7,
|
||||
@@ -267,9 +267,9 @@ async def test_get_applicable_rules_returns_shape():
|
||||
sub_result, _empty(), _empty(), rules_result, _empty(),
|
||||
])
|
||||
|
||||
with patch("fabledassistant.services.rulebooks.async_session") as mock_cls:
|
||||
with patch("scribe.services.rulebooks.async_session") as mock_cls:
|
||||
mock_cls.return_value = mock_session
|
||||
from fabledassistant.services.rulebooks import get_applicable_rules
|
||||
from scribe.services.rulebooks import get_applicable_rules
|
||||
result = await get_applicable_rules(project_id=3, user_id=7, limit=50)
|
||||
|
||||
assert "rules" in result
|
||||
@@ -302,9 +302,9 @@ async def test_get_applicable_rules_truncates_when_over_limit():
|
||||
sub_result, _empty(), _empty(), rules_result, _empty(),
|
||||
])
|
||||
|
||||
with patch("fabledassistant.services.rulebooks.async_session") as mock_cls:
|
||||
with patch("scribe.services.rulebooks.async_session") as mock_cls:
|
||||
mock_cls.return_value = mock_session
|
||||
from fabledassistant.services.rulebooks import get_applicable_rules
|
||||
from scribe.services.rulebooks import get_applicable_rules
|
||||
result = await get_applicable_rules(project_id=3, user_id=7, limit=50)
|
||||
|
||||
assert result["truncated"] is True
|
||||
@@ -324,9 +324,9 @@ async def test_get_applicable_rules_includes_project_scoped_rules():
|
||||
_empty(), _empty(), _empty(), _empty(), proj_rules_result,
|
||||
])
|
||||
|
||||
with patch("fabledassistant.services.rulebooks.async_session") as mock_cls:
|
||||
with patch("scribe.services.rulebooks.async_session") as mock_cls:
|
||||
mock_cls.return_value = mock_session
|
||||
from fabledassistant.services.rulebooks import get_applicable_rules
|
||||
from scribe.services.rulebooks import get_applicable_rules
|
||||
result = await get_applicable_rules(project_id=3, user_id=7)
|
||||
|
||||
assert len(result["project_rules"]) == 2
|
||||
@@ -353,9 +353,9 @@ async def test_get_applicable_rules_surfaces_suppressed_with_context():
|
||||
_empty(), suppressed_rules_result, suppressed_topics_result, _empty(), _empty(),
|
||||
])
|
||||
|
||||
with patch("fabledassistant.services.rulebooks.async_session") as mock_cls:
|
||||
with patch("scribe.services.rulebooks.async_session") as mock_cls:
|
||||
mock_cls.return_value = mock_session
|
||||
from fabledassistant.services.rulebooks import get_applicable_rules
|
||||
from scribe.services.rulebooks import get_applicable_rules
|
||||
result = await get_applicable_rules(project_id=3, user_id=7)
|
||||
|
||||
assert len(result["suppressed_rules"]) == 1
|
||||
|
||||
@@ -26,9 +26,9 @@ async def test_delete_note_returns_batch_and_commits():
|
||||
no_children = MagicMock()
|
||||
no_children.scalars.return_value.all.return_value = []
|
||||
session.execute = AsyncMock(side_effect=[_exists_result(True), no_children, MagicMock()])
|
||||
with patch("fabledassistant.services.trash.async_session") as cls:
|
||||
with patch("scribe.services.trash.async_session") as cls:
|
||||
cls.return_value = session
|
||||
from fabledassistant.services.trash import delete
|
||||
from scribe.services.trash import delete
|
||||
batch = await delete(user_id=1, entity_type="note", entity_id=5)
|
||||
assert isinstance(batch, str) and len(batch) > 0
|
||||
assert session.commit.called
|
||||
@@ -40,9 +40,9 @@ async def test_delete_note_returns_batch_and_commits():
|
||||
async def test_delete_returns_none_when_not_found():
|
||||
session = _make_mock_session()
|
||||
session.execute = AsyncMock(return_value=_exists_result(False))
|
||||
with patch("fabledassistant.services.trash.async_session") as cls:
|
||||
with patch("scribe.services.trash.async_session") as cls:
|
||||
cls.return_value = session
|
||||
from fabledassistant.services.trash import delete
|
||||
from scribe.services.trash import delete
|
||||
batch = await delete(user_id=1, entity_type="task", entity_id=999)
|
||||
assert batch is None
|
||||
assert not session.commit.called
|
||||
@@ -63,9 +63,9 @@ async def test_delete_project_cascades_to_notes_milestones_project_rules_and_sup
|
||||
MagicMock(), MagicMock(),
|
||||
MagicMock(),
|
||||
])
|
||||
with patch("fabledassistant.services.trash.async_session") as cls:
|
||||
with patch("scribe.services.trash.async_session") as cls:
|
||||
cls.return_value = session
|
||||
from fabledassistant.services.trash import delete
|
||||
from scribe.services.trash import delete
|
||||
batch = await delete(user_id=1, entity_type="project", entity_id=3)
|
||||
assert isinstance(batch, str)
|
||||
assert session.execute.await_count == 7
|
||||
@@ -80,9 +80,9 @@ async def test_delete_rulebook_cascades_topics_and_rules():
|
||||
session.execute = AsyncMock(side_effect=[
|
||||
_exists_result(True), topic_ids_result, MagicMock(), MagicMock(), MagicMock(),
|
||||
])
|
||||
with patch("fabledassistant.services.trash.async_session") as cls:
|
||||
with patch("scribe.services.trash.async_session") as cls:
|
||||
cls.return_value = session
|
||||
from fabledassistant.services.trash import delete
|
||||
from scribe.services.trash import delete
|
||||
batch = await delete(user_id=7, entity_type="rulebook", entity_id=2)
|
||||
assert isinstance(batch, str)
|
||||
assert session.execute.await_count == 5
|
||||
@@ -99,9 +99,9 @@ async def test_restore_clears_batch_across_all_models():
|
||||
session = _make_mock_session()
|
||||
# 7 models, each returns a rowcount
|
||||
session.execute = AsyncMock(side_effect=[_rowcount_result(i) for i in [2, 0, 1, 1, 0, 0, 0]])
|
||||
with patch("fabledassistant.services.trash.async_session") as cls:
|
||||
with patch("scribe.services.trash.async_session") as cls:
|
||||
cls.return_value = session
|
||||
from fabledassistant.services.trash import restore
|
||||
from scribe.services.trash import restore
|
||||
n = await restore(user_id=1, batch_id="abc")
|
||||
assert n == 4
|
||||
assert session.execute.await_count == 7
|
||||
@@ -112,9 +112,9 @@ async def test_restore_clears_batch_across_all_models():
|
||||
async def test_purge_expired_skips_when_retention_zero():
|
||||
session = _make_mock_session()
|
||||
session.execute = AsyncMock()
|
||||
with patch("fabledassistant.services.trash.async_session") as cls:
|
||||
with patch("scribe.services.trash.async_session") as cls:
|
||||
cls.return_value = session
|
||||
from fabledassistant.services.trash import purge_expired
|
||||
from scribe.services.trash import purge_expired
|
||||
n = await purge_expired(1, 0)
|
||||
assert n == 0
|
||||
assert not session.execute.called # never opens a delete
|
||||
@@ -124,9 +124,9 @@ async def test_purge_expired_skips_when_retention_zero():
|
||||
async def test_purge_expired_deletes_across_models_when_positive():
|
||||
session = _make_mock_session()
|
||||
session.execute = AsyncMock(side_effect=[_rowcount_result(1) for _ in range(7)])
|
||||
with patch("fabledassistant.services.trash.async_session") as cls:
|
||||
with patch("scribe.services.trash.async_session") as cls:
|
||||
cls.return_value = session
|
||||
from fabledassistant.services.trash import purge_expired
|
||||
from scribe.services.trash import purge_expired
|
||||
n = await purge_expired(1, 90)
|
||||
assert n == 7
|
||||
assert session.execute.await_count == 7
|
||||
@@ -135,9 +135,9 @@ async def test_purge_expired_deletes_across_models_when_positive():
|
||||
def test_owner_clause_scopes_every_model():
|
||||
"""Regression: every trash op must owner-scope, including topics/rules
|
||||
which previously had NO owner check (IDOR across tenants)."""
|
||||
from fabledassistant.services.trash import _owner_clause, _MODEL_FOR
|
||||
from fabledassistant.models.note import Note
|
||||
from fabledassistant.models.rulebook import Rulebook, RulebookTopic, Rule
|
||||
from scribe.services.trash import _owner_clause, _MODEL_FOR
|
||||
from scribe.models.note import Note
|
||||
from scribe.models.rulebook import Rulebook, RulebookTopic, Rule
|
||||
|
||||
# Models with a direct owner column.
|
||||
assert "user_id" in str(_owner_clause(Note, 7))
|
||||
@@ -172,9 +172,9 @@ async def test_list_trash_groups_by_batch():
|
||||
empty = MagicMock()
|
||||
empty.scalars.return_value.all.return_value = []
|
||||
session.execute = AsyncMock(side_effect=[note_rows] + [empty] * 6)
|
||||
with patch("fabledassistant.services.trash.async_session") as cls:
|
||||
with patch("scribe.services.trash.async_session") as cls:
|
||||
cls.return_value = session
|
||||
from fabledassistant.services.trash import list_trash
|
||||
from scribe.services.trash import list_trash
|
||||
out = await list_trash(user_id=1)
|
||||
assert len(out) == 1
|
||||
assert out[0]["batch_id"] == "b1"
|
||||
|
||||
@@ -39,9 +39,9 @@ def _has_filter(captured: list[str]) -> bool:
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_note_excludes_trashed():
|
||||
cap: list[str] = []
|
||||
with patch("fabledassistant.services.notes.async_session") as cls:
|
||||
with patch("scribe.services.notes.async_session") as cls:
|
||||
cls.return_value = _capturing_session(cap)
|
||||
from fabledassistant.services.notes import get_note
|
||||
from scribe.services.notes import get_note
|
||||
await get_note(1, 5)
|
||||
assert _has_filter(cap)
|
||||
|
||||
@@ -49,9 +49,9 @@ async def test_get_note_excludes_trashed():
|
||||
@pytest.mark.asyncio
|
||||
async def test_list_notes_excludes_trashed():
|
||||
cap: list[str] = []
|
||||
with patch("fabledassistant.services.notes.async_session") as cls:
|
||||
with patch("scribe.services.notes.async_session") as cls:
|
||||
cls.return_value = _capturing_session(cap)
|
||||
from fabledassistant.services.notes import list_notes
|
||||
from scribe.services.notes import list_notes
|
||||
await list_notes(1)
|
||||
assert _has_filter(cap)
|
||||
|
||||
@@ -59,9 +59,9 @@ async def test_list_notes_excludes_trashed():
|
||||
@pytest.mark.asyncio
|
||||
async def test_list_projects_excludes_trashed():
|
||||
cap: list[str] = []
|
||||
with patch("fabledassistant.services.projects.async_session") as cls:
|
||||
with patch("scribe.services.projects.async_session") as cls:
|
||||
cls.return_value = _capturing_session(cap)
|
||||
from fabledassistant.services.projects import list_projects
|
||||
from scribe.services.projects import list_projects
|
||||
await list_projects(1)
|
||||
assert _has_filter(cap)
|
||||
|
||||
@@ -69,9 +69,9 @@ async def test_list_projects_excludes_trashed():
|
||||
@pytest.mark.asyncio
|
||||
async def test_list_events_excludes_trashed():
|
||||
cap: list[str] = []
|
||||
with patch("fabledassistant.services.events.async_session") as cls:
|
||||
with patch("scribe.services.events.async_session") as cls:
|
||||
cls.return_value = _capturing_session(cap)
|
||||
from fabledassistant.services.events import list_events
|
||||
from scribe.services.events import list_events
|
||||
await list_events(
|
||||
1,
|
||||
datetime(2026, 5, 1, tzinfo=timezone.utc),
|
||||
@@ -83,9 +83,9 @@ async def test_list_events_excludes_trashed():
|
||||
@pytest.mark.asyncio
|
||||
async def test_query_knowledge_excludes_trashed():
|
||||
cap: list[str] = []
|
||||
with patch("fabledassistant.services.knowledge.async_session") as cls:
|
||||
with patch("scribe.services.knowledge.async_session") as cls:
|
||||
cls.return_value = _capturing_session(cap)
|
||||
from fabledassistant.services.knowledge import query_knowledge
|
||||
from scribe.services.knowledge import query_knowledge
|
||||
await query_knowledge(user_id=1, note_type=None, tags=[], sort="modified", q=None, limit=10, offset=0)
|
||||
assert _has_filter(cap)
|
||||
|
||||
@@ -93,8 +93,8 @@ async def test_query_knowledge_excludes_trashed():
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_applicable_rules_excludes_trashed():
|
||||
cap: list[str] = []
|
||||
with patch("fabledassistant.services.rulebooks.async_session") as cls:
|
||||
with patch("scribe.services.rulebooks.async_session") as cls:
|
||||
cls.return_value = _capturing_session(cap)
|
||||
from fabledassistant.services.rulebooks import get_applicable_rules
|
||||
from scribe.services.rulebooks import get_applicable_rules
|
||||
await get_applicable_rules(project_id=3, user_id=1)
|
||||
assert _has_filter(cap)
|
||||
|
||||
@@ -9,7 +9,7 @@ import pytest
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_user_tz_returns_configured_zone():
|
||||
from fabledassistant.services import tz as tz_mod
|
||||
from scribe.services import tz as tz_mod
|
||||
|
||||
with patch.object(tz_mod, "get_setting", AsyncMock(return_value="America/New_York")):
|
||||
zone = await tz_mod.get_user_tz(1)
|
||||
@@ -18,7 +18,7 @@ async def test_get_user_tz_returns_configured_zone():
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_user_tz_falls_back_to_utc_on_bad_input():
|
||||
from fabledassistant.services import tz as tz_mod
|
||||
from scribe.services import tz as tz_mod
|
||||
|
||||
with patch.object(tz_mod, "get_setting", AsyncMock(return_value="Not/AZone")):
|
||||
zone = await tz_mod.get_user_tz(1)
|
||||
@@ -28,7 +28,7 @@ async def test_get_user_tz_falls_back_to_utc_on_bad_input():
|
||||
@pytest.mark.asyncio
|
||||
async def test_user_day_date_before_4am_returns_yesterday():
|
||||
"""00:00–03:59 local still shows yesterday's day_date."""
|
||||
from fabledassistant.services import tz as tz_mod
|
||||
from scribe.services import tz as tz_mod
|
||||
|
||||
ny = ZoneInfo("America/New_York")
|
||||
# 02:00 NY on 2026-04-13 → day_date = 2026-04-12 (still pre-rollover)
|
||||
@@ -47,7 +47,7 @@ async def test_user_day_date_before_4am_returns_yesterday():
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_user_day_date_after_4am_returns_today():
|
||||
from fabledassistant.services import tz as tz_mod
|
||||
from scribe.services import tz as tz_mod
|
||||
|
||||
ny = ZoneInfo("America/New_York")
|
||||
fake_now = datetime(2026, 4, 13, 4, 30, tzinfo=ny)
|
||||
|
||||
+1
-1
@@ -2,7 +2,7 @@
|
||||
Unit tests for pure utility functions — no database or network required.
|
||||
"""
|
||||
import pytest
|
||||
from fabledassistant.routes.export import _safe_filename
|
||||
from scribe.routes.export import _safe_filename
|
||||
|
||||
|
||||
class TestSafeFilename:
|
||||
|
||||
@@ -20,10 +20,10 @@ async def test_pin_version_sets_manual_kind_and_label():
|
||||
mock_version.pin_label = None
|
||||
|
||||
with patch(
|
||||
"fabledassistant.services.version_pinning.async_session",
|
||||
"scribe.services.version_pinning.async_session",
|
||||
return_value=_mock_session_for_version(mock_version),
|
||||
):
|
||||
from fabledassistant.services.version_pinning import pin_version
|
||||
from scribe.services.version_pinning import pin_version
|
||||
result = await pin_version(
|
||||
user_id=1, note_id=42, version_id=7, label="the runbook circa Q2",
|
||||
)
|
||||
@@ -40,10 +40,10 @@ async def test_pin_version_accepts_null_label():
|
||||
mock_version.pin_label = None
|
||||
|
||||
with patch(
|
||||
"fabledassistant.services.version_pinning.async_session",
|
||||
"scribe.services.version_pinning.async_session",
|
||||
return_value=_mock_session_for_version(mock_version),
|
||||
):
|
||||
from fabledassistant.services.version_pinning import pin_version
|
||||
from scribe.services.version_pinning import pin_version
|
||||
await pin_version(user_id=1, note_id=42, version_id=7, label=None)
|
||||
|
||||
assert mock_version.pin_kind == "manual"
|
||||
@@ -57,10 +57,10 @@ async def test_pin_version_promotes_auto_to_manual_with_label_update():
|
||||
mock_version.pin_label = "stable 2026-04-01 → 2026-04-05"
|
||||
|
||||
with patch(
|
||||
"fabledassistant.services.version_pinning.async_session",
|
||||
"scribe.services.version_pinning.async_session",
|
||||
return_value=_mock_session_for_version(mock_version),
|
||||
):
|
||||
from fabledassistant.services.version_pinning import pin_version
|
||||
from scribe.services.version_pinning import pin_version
|
||||
await pin_version(
|
||||
user_id=1, note_id=42, version_id=7, label="post-network-refresh",
|
||||
)
|
||||
@@ -78,10 +78,10 @@ async def test_pin_version_rejects_overlong_label():
|
||||
# even be entered. We still patch it to avoid accidental DB lookups
|
||||
# if the implementation order changes.
|
||||
with patch(
|
||||
"fabledassistant.services.version_pinning.async_session",
|
||||
"scribe.services.version_pinning.async_session",
|
||||
return_value=_mock_session_for_version(mock_version),
|
||||
):
|
||||
from fabledassistant.services.version_pinning import pin_version
|
||||
from scribe.services.version_pinning import pin_version
|
||||
try:
|
||||
await pin_version(
|
||||
user_id=1, note_id=42, version_id=7, label="x" * 501,
|
||||
@@ -102,10 +102,10 @@ async def test_pin_version_returns_none_when_not_found():
|
||||
mock_session.__aexit__ = AsyncMock(return_value=False)
|
||||
|
||||
with patch(
|
||||
"fabledassistant.services.version_pinning.async_session",
|
||||
"scribe.services.version_pinning.async_session",
|
||||
return_value=mock_session,
|
||||
):
|
||||
from fabledassistant.services.version_pinning import pin_version
|
||||
from scribe.services.version_pinning import pin_version
|
||||
out = await pin_version(user_id=1, note_id=42, version_id=99, label=None)
|
||||
|
||||
assert out is None
|
||||
@@ -117,10 +117,10 @@ async def test_unpin_version_clears_kind_and_label():
|
||||
mock_version.pin_label = "previous label"
|
||||
|
||||
with patch(
|
||||
"fabledassistant.services.version_pinning.async_session",
|
||||
"scribe.services.version_pinning.async_session",
|
||||
return_value=_mock_session_for_version(mock_version),
|
||||
):
|
||||
from fabledassistant.services.version_pinning import unpin_version
|
||||
from scribe.services.version_pinning import unpin_version
|
||||
result = await unpin_version(user_id=1, note_id=42, version_id=7)
|
||||
|
||||
assert result is mock_version
|
||||
|
||||
@@ -32,10 +32,10 @@ async def test_create_version_prune_sql_filters_to_unpinned():
|
||||
mock_session.execute = AsyncMock(side_effect=execute_capture)
|
||||
|
||||
with patch(
|
||||
"fabledassistant.services.note_versions.async_session",
|
||||
"scribe.services.note_versions.async_session",
|
||||
return_value=mock_session,
|
||||
):
|
||||
from fabledassistant.services.note_versions import create_version
|
||||
from scribe.services.note_versions import create_version
|
||||
await create_version(
|
||||
user_id=1, note_id=42, body="content", title="t", tags=["a"],
|
||||
)
|
||||
@@ -67,10 +67,10 @@ async def test_prune_auto_pins_filters_to_auto_kind():
|
||||
mock_session.execute = AsyncMock(side_effect=execute_capture)
|
||||
|
||||
with patch(
|
||||
"fabledassistant.services.version_pinning.async_session",
|
||||
"scribe.services.version_pinning.async_session",
|
||||
return_value=mock_session,
|
||||
):
|
||||
from fabledassistant.services.version_pinning import (
|
||||
from scribe.services.version_pinning import (
|
||||
prune_auto_pins, MAX_AUTO_PINS,
|
||||
)
|
||||
await prune_auto_pins(user_id=1, note_id=42)
|
||||
|
||||
@@ -19,7 +19,7 @@ def _v(version_id: int, days_ago: int, pin_kind=None):
|
||||
|
||||
def test_promote_stable_versions_pins_versions_with_2_day_gap():
|
||||
"""Versions followed by another version >= 2 days later get promoted."""
|
||||
from fabledassistant.services.version_pinning import (
|
||||
from scribe.services.version_pinning import (
|
||||
_promote_stable_versions_for_note,
|
||||
)
|
||||
# 3 versions: v1 (10 days ago), v2 (5 days ago), v3 (1 day ago)
|
||||
@@ -35,7 +35,7 @@ def test_promote_stable_versions_pins_versions_with_2_day_gap():
|
||||
|
||||
def test_promote_stable_versions_pins_latest_if_old_enough():
|
||||
"""If the latest version is >= 2 days old with no successor, pin it."""
|
||||
from fabledassistant.services.version_pinning import (
|
||||
from scribe.services.version_pinning import (
|
||||
_promote_stable_versions_for_note,
|
||||
)
|
||||
versions = [_v(1, 10), _v(2, 3)] # latest is 3 days old
|
||||
@@ -44,7 +44,7 @@ def test_promote_stable_versions_pins_latest_if_old_enough():
|
||||
|
||||
|
||||
def test_promote_stable_versions_skips_already_pinned():
|
||||
from fabledassistant.services.version_pinning import (
|
||||
from scribe.services.version_pinning import (
|
||||
_promote_stable_versions_for_note,
|
||||
)
|
||||
versions = [_v(1, 10, pin_kind="manual"), _v(2, 5)]
|
||||
@@ -67,7 +67,7 @@ def test_promote_stable_versions_skips_active_editing():
|
||||
id=2, created_at=base - timedelta(hours=2),
|
||||
pin_kind=None, pin_label=None,
|
||||
)
|
||||
from fabledassistant.services.version_pinning import (
|
||||
from scribe.services.version_pinning import (
|
||||
_promote_stable_versions_for_note,
|
||||
)
|
||||
pinned = _promote_stable_versions_for_note([v1, v2])
|
||||
@@ -76,7 +76,7 @@ def test_promote_stable_versions_skips_active_editing():
|
||||
|
||||
def test_promote_stable_versions_writes_descriptive_label():
|
||||
"""The auto-generated label references stability."""
|
||||
from fabledassistant.services.version_pinning import (
|
||||
from scribe.services.version_pinning import (
|
||||
_promote_stable_versions_for_note,
|
||||
)
|
||||
versions = [_v(1, 10), _v(2, 5), _v(3, 1)]
|
||||
@@ -94,7 +94,7 @@ def test_promote_stable_versions_naive_datetime_is_treated_as_utc():
|
||||
SimpleNamespace(id=1, created_at=naive_old, pin_kind=None, pin_label=None),
|
||||
SimpleNamespace(id=2, created_at=naive_recent, pin_kind=None, pin_label=None),
|
||||
]
|
||||
from fabledassistant.services.version_pinning import (
|
||||
from scribe.services.version_pinning import (
|
||||
_promote_stable_versions_for_note,
|
||||
)
|
||||
pinned = _promote_stable_versions_for_note(versions)
|
||||
@@ -105,7 +105,7 @@ def test_promote_stable_versions_naive_datetime_is_treated_as_utc():
|
||||
async def test_scan_user_for_auto_pins_iterates_all_notes(monkeypatch):
|
||||
"""scan_user_for_auto_pins iterates every note for the user and calls
|
||||
the per-note flow on each, summing returned counts."""
|
||||
from fabledassistant.services import version_pinning
|
||||
from scribe.services import version_pinning
|
||||
|
||||
note_ids = [10, 20, 30]
|
||||
seen: list[int] = []
|
||||
@@ -136,7 +136,7 @@ async def test_scan_user_for_auto_pins_iterates_all_notes(monkeypatch):
|
||||
async def test_scan_user_for_auto_pins_swallows_per_note_errors(monkeypatch):
|
||||
"""One bad note doesn't stop the scan; the error is logged and the
|
||||
others continue."""
|
||||
from fabledassistant.services import version_pinning
|
||||
from scribe.services import version_pinning
|
||||
|
||||
async def fake_list_note_ids(uid):
|
||||
return [10, 20, 30]
|
||||
|
||||
Reference in New Issue
Block a user