Files
FabledGrimoire/tests/test_integration_handshake_touched.py
bvandeusenandClaude Opus 5.5 0f5ef0be63
CI & Build / Python lint (push) Successful in 3s
CI & Build / Plugin hooks (push) Successful in 12s
CI & Build / TypeScript typecheck (push) Successful in 56s
CI & Build / integration (push) Successful in 1m4s
CI & Build / Python tests (push) Successful in 1m55s
CI & Build / Build & push image (push) Successful in 44s
rename(547): the server is grimoire — package, image, DB defaults, backups
Step 2 of the Scribe -> Grimoire rename.

- src/scribe -> src/grimoire, and every import, patch target, module path,
  hypercorn entry point and `python -m grimoire.restore` with it; pyproject,
  uv.lock and the frontend package are named grimoire.
- Image git.fabledsword.com/bvandeusen/fabledgrimoire; CI's Postgres service
  runs as grimoire / grimoire_test.
- Compose files default the DB user, database and (prod) volume to grimoire,
  each overridable from .env, so an install created as Scribe keeps its data
  by setting POSTGRES_USER, POSTGRES_DB and PGDATA_VOLUME.
- Backups are written as grimoire-<ts>.tar with format grimoire-backup-tar.
  Archives named scribe-* with the scribe-backup-tar manifest still restore
  and are still counted at the destination (folder and S3), with tests.
- Exports, the MCP auth realm, the ASGI scope key and the persistent logger
  carry the new name.

Kept on purpose: the secret-sealing key label stays "scribe.secrets.v1",
because every stored secret was derived under it.

Plugin identity (plugin name, hooks, env vars, .scribe marker, skills) is
step 3.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-22 10:50:45 -04:00

82 lines
3.6 KiB
Python

"""Real-Postgres tests for "recently touched" in the enter_project handshake.
What a mock cannot show: that a work-log moves its task up the list even
though logging never bumps the task's updated_at, and that a milestone whose
steps changed today reads as touched today even though its own row is old.
"""
from datetime import datetime, timedelta, timezone
import pytest
import pytest_asyncio
from grimoire.models import async_session
from grimoire.models.milestone import Milestone
from grimoire.models.note import Note
from grimoire.models.project import Project
from grimoire.models.task_log import TaskLog
from grimoire.services import milestones as milestones_svc
from grimoire.services import notes as notes_svc
from tests.helpers import ensure_user
pytestmark = [pytest.mark.integration, pytest.mark.usefixtures("_dispose_engine", "_no_embedding")]
NOW = datetime.now(timezone.utc)
@pytest_asyncio.fixture
async def world():
"""Two open tasks and two milestones with deliberately staged timestamps."""
async with async_session() as s:
owner = await ensure_user(s, "handshake_owner")
project = Project(user_id=owner.id, title="Handshake target")
s.add(project)
await s.flush()
# Milestone "old plan": its own row is 30 days old, one step changed now.
old_plan = Milestone(user_id=owner.id, project_id=project.id, title="old plan",
updated_at=NOW - timedelta(days=30))
# Milestone "recent edit": its own row changed 2 days ago, no steps.
recent_edit = Milestone(user_id=owner.id, project_id=project.id, title="recent edit",
updated_at=NOW - timedelta(days=2))
s.add_all([old_plan, recent_edit])
await s.flush()
# "logged": last edited 10 days ago, but worked through a log just now.
logged = Note(user_id=owner.id, project_id=project.id, title="logged",
status="todo", task_kind="work",
updated_at=NOW - timedelta(days=10))
# "edited": last edited 1 day ago, no logs.
edited = Note(user_id=owner.id, project_id=project.id, title="edited",
status="todo", task_kind="work",
updated_at=NOW - timedelta(days=1))
step = Note(user_id=owner.id, project_id=project.id, milestone_id=old_plan.id,
title="step", status="done", task_kind="work", updated_at=NOW)
s.add_all([logged, edited, step])
await s.flush()
s.add(TaskLog(task_id=logged.id, user_id=owner.id, content="worked on it",
created_at=NOW))
ids = {"owner": owner.id, "pid": project.id, "logged": logged.id,
"edited": edited.id, "old_plan": old_plan.id, "recent_edit": recent_edit.id}
await s.commit()
return ids
async def test_a_work_log_counts_as_touching_its_task(world):
by_edit, _ = await notes_svc.list_notes(
world["owner"], is_task=True, project_id=world["pid"],
status=["todo", "in_progress"], sort="updated_at",
)
by_touch, _ = await notes_svc.list_notes(
world["owner"], is_task=True, project_id=world["pid"],
status=["todo", "in_progress"], sort="touched",
)
assert [t.id for t in by_edit] == [world["edited"], world["logged"]]
assert [t.id for t in by_touch] == [world["logged"], world["edited"]]
async def test_a_step_changing_counts_as_touching_its_milestone(world):
rows = await milestones_svc.get_project_milestone_summary(world["owner"], world["pid"])
brief, omitted = milestones_svc.brief_milestone_summary(rows, limit=1)
assert omitted == 1
assert [m["id"] for m in brief] == [world["old_plan"]]