refactor(tests): per-model fakes, FakeMCP and session mocks come from tests/helpers (#2825, milestone 296 area 1, batch 2)
CI & Build / Python lint (push) Successful in 4s
CI & Build / Plugin hooks (push) Successful in 8s
CI & Build / integration (push) Successful in 24s
CI & Build / TypeScript typecheck (push) Successful in 33s
CI & Build / Python tests (push) Failing after 36s
CI & Build / Build & push image (push) Skipped
CI & Build / Python lint (push) Successful in 4s
CI & Build / Plugin hooks (push) Successful in 8s
CI & Build / integration (push) Successful in 24s
CI & Build / TypeScript typecheck (push) Successful in 33s
CI & Build / Python tests (push) Failing after 36s
CI & Build / Build & push image (push) Skipped
Second pass over the tests/ ledger after bbee0d0. fake_record(**attrs) is the
one MagicMock-with-real-attributes builder (to_dict mirrors them; the
note-2109 hazard documented once); fake_note/fake_task/fake_snippet/
fake_project/fake_milestone/fake_system/fake_rulebook/fake_topic/fake_rule
carry each model's ordinary defaults on top of it, replacing 14 per-file
factories (two rulebook trios in tool-vs-service wordings, _fake_task, _fake_ms,
_fake_project, _plan_note, _fake_snippet, two _snippet adapters now one-liners
over fake_snippet). FakeMCP replaces the five closure-over-a-list registrar
fakes (+ _Recorder); loc() and design_token_stub() replace the paired _loc /
_token / _T stand-ins; every hand-built async_session mock (9 helper defs and
14 inline copies) now starts from make_mock_session(). Call sites rewritten by
AST with each file's former defaults made explicit, so behaviour is unchanged.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
+128
-21
@@ -6,6 +6,8 @@ them; a module imports what it needs with ``from tests.helpers import ...``.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import datetime, timezone
|
||||
from types import SimpleNamespace
|
||||
from unittest.mock import AsyncMock, MagicMock
|
||||
|
||||
|
||||
@@ -48,30 +50,135 @@ async def ensure_user(session, username: str, role: str = "user"):
|
||||
return user
|
||||
|
||||
|
||||
def fake_note(**attrs) -> MagicMock:
|
||||
"""A MagicMock note with REAL values on every attribute the product reads
|
||||
to label, scope, or render a record.
|
||||
def fake_record(**attrs) -> MagicMock:
|
||||
"""A MagicMock record with REAL values on the attributes named, and a
|
||||
``to_dict()`` that mirrors them.
|
||||
|
||||
The hazard this exists for (note 2109): an auto-created MagicMock attribute
|
||||
is truthy and has a repr. The injected menu reads ``is_task`` /
|
||||
``task_kind`` / ``note_type`` for its kind marker, ``user_id`` to decide
|
||||
whether a line needs a "shared by …" attribution, ``data`` for a snippet's
|
||||
language tag, and ``deleted_at`` to spot trash — on a bare MagicMock every
|
||||
record renders as another user's trashed task with a mock repr for a
|
||||
language. Defaults below are the ORDINARY state (own note, live, no
|
||||
structured data); override what the test is about.
|
||||
|
||||
``to_dict()`` returns the same values as a plain dict, so a tool that
|
||||
repackages ``note.to_dict()`` sees keys that agree with the attributes.
|
||||
is truthy and has a repr — so a bare MagicMock handed to the product reads
|
||||
as trashed, shared, a task, and owned by a MagicMock. Name every attribute
|
||||
the code under test will read; the per-model ``fake_*`` builders below
|
||||
carry the ordinary defaults so a call site states only what the test is
|
||||
about. ``created_at`` / ``updated_at`` are set as attributes but kept out
|
||||
of ``to_dict()`` (no test serialises them, and the real models isoformat
|
||||
them).
|
||||
"""
|
||||
values = {
|
||||
n = MagicMock()
|
||||
for key, value in attrs.items():
|
||||
setattr(n, key, value)
|
||||
n.to_dict.return_value = {
|
||||
k: v for k, v in attrs.items() if k not in ("created_at", "updated_at")
|
||||
}
|
||||
return n
|
||||
|
||||
|
||||
def _with_defaults(defaults: dict, attrs: dict) -> MagicMock:
|
||||
values = dict(defaults)
|
||||
values.update(attrs)
|
||||
return fake_record(**values)
|
||||
|
||||
|
||||
def _now():
|
||||
return datetime.now(timezone.utc)
|
||||
|
||||
|
||||
def fake_note(**attrs) -> MagicMock:
|
||||
"""A stand-in Note: own (user_id=7, the caller `_bind_user` binds), live,
|
||||
not a task, no structured data. The injected menu reads is_task /
|
||||
task_kind / note_type for its kind marker, user_id for the "shared by …"
|
||||
attribution, data for a snippet's language, deleted_at for trash."""
|
||||
return _with_defaults({
|
||||
"id": 1, "title": "t", "body": "", "tags": [], "user_id": 7,
|
||||
"note_type": "note", "is_task": False, "task_kind": "work",
|
||||
"data": None, "deleted_at": None,
|
||||
}
|
||||
values.update(attrs)
|
||||
n = MagicMock()
|
||||
for key, value in values.items():
|
||||
setattr(n, key, value)
|
||||
n.to_dict.return_value = dict(values)
|
||||
return n
|
||||
}, attrs)
|
||||
|
||||
|
||||
def fake_task(**attrs) -> MagicMock:
|
||||
"""A stand-in task note — get_task reads parent_id, deleted_at, user_id."""
|
||||
return _with_defaults({
|
||||
"id": 1, "title": "t", "body": "", "status": "todo", "priority": "none",
|
||||
"tags": [], "parent_id": None, "project_id": None, "is_task": True,
|
||||
"task_kind": "work", "user_id": 7, "deleted_at": None,
|
||||
}, attrs)
|
||||
|
||||
|
||||
def fake_snippet(**attrs) -> MagicMock:
|
||||
"""A stand-in snippet note. ``data`` is explicitly None: snippet_fields
|
||||
prefers `data` when truthy, and a MagicMock is truthy."""
|
||||
return _with_defaults({
|
||||
"id": 1, "title": "debounce — rate-limit a callback",
|
||||
"body": "```js\nreturn 1\n```\n", "tags": ["js", "snippet"],
|
||||
"note_type": "snippet", "is_task": False, "task_kind": "work",
|
||||
"user_id": 7, "data": None, "deleted_at": None,
|
||||
}, attrs)
|
||||
|
||||
|
||||
def fake_project(**attrs) -> MagicMock:
|
||||
"""design_system_id is explicit: a truthy auto-attribute would route every
|
||||
project through the design-system branch and out to a real database."""
|
||||
return _with_defaults({
|
||||
"id": 1, "title": "P", "description": "", "goal": "", "status": "active",
|
||||
"color": None, "design_system_id": None, "user_id": 7,
|
||||
}, attrs)
|
||||
|
||||
|
||||
def fake_milestone(**attrs) -> MagicMock:
|
||||
return _with_defaults({
|
||||
"id": 1, "project_id": 1, "title": "MS", "description": None,
|
||||
"status": "active", "order_index": 0,
|
||||
}, attrs)
|
||||
|
||||
|
||||
def fake_system(**attrs) -> MagicMock:
|
||||
return _with_defaults({"id": 1, "name": "Reader", "project_id": 5}, attrs)
|
||||
|
||||
|
||||
def fake_rulebook(**attrs) -> MagicMock:
|
||||
return _with_defaults({
|
||||
"id": 1, "owner_user_id": 7, "title": "FabledSword family",
|
||||
"description": "", "created_at": _now(), "updated_at": _now(),
|
||||
}, attrs)
|
||||
|
||||
|
||||
def fake_topic(**attrs) -> MagicMock:
|
||||
return _with_defaults({
|
||||
"id": 10, "rulebook_id": 1, "title": "git-workflow", "description": "",
|
||||
"order_index": 0, "created_at": _now(), "updated_at": _now(),
|
||||
}, attrs)
|
||||
|
||||
|
||||
def fake_rule(**attrs) -> MagicMock:
|
||||
return _with_defaults({
|
||||
"id": 1, "topic_id": 10, "title": "dev is home",
|
||||
"statement": "Work directly on dev", "why": "", "how_to_apply": "",
|
||||
"order_index": 0, "created_at": _now(), "updated_at": _now(),
|
||||
}, attrs)
|
||||
|
||||
|
||||
class FakeMCP:
|
||||
"""Stand-in for the FastMCP server a tool module's ``register(mcp)`` is
|
||||
handed: records the ``name=`` of every ``@mcp.tool(...)`` registration in
|
||||
``names`` and leaves the function untouched, so a test can assert which
|
||||
tools a module exposes."""
|
||||
|
||||
def __init__(self) -> None:
|
||||
self.names: list[str] = []
|
||||
|
||||
def tool(self, name=None):
|
||||
self.names.append(name)
|
||||
return lambda fn: fn
|
||||
|
||||
|
||||
def loc(path: str = "", repo: str = "", symbol: str = "") -> dict:
|
||||
"""One snippet location, in the shape the record stores."""
|
||||
return {"repo": repo, "path": path, "symbol": symbol}
|
||||
|
||||
|
||||
def design_token_stub(name, value_by_mode, group_name=None, purpose=None,
|
||||
order_index=0, supersedes=None) -> SimpleNamespace:
|
||||
"""A design-token row as the cascade / stylesheet code reads it."""
|
||||
return SimpleNamespace(
|
||||
name=name, value_by_mode=value_by_mode, group_name=group_name,
|
||||
purpose=purpose, order_index=order_index, supersedes=supersedes or [],
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user