docs(6209): coverage, guidance-ownership, shape, warnings tests and shared fixtures
CI & Build / Python lint (push) Successful in 2s
CI & Build / Plugin hooks (push) Successful in 12s
CI & Build / TypeScript typecheck (push) Successful in 52s
CI & Build / integration (push) Successful in 1m12s
CI & Build / Python tests (push) Successful in 1m56s
CI & Build / Build & push image (push) Successful in 13s

test_pattern_coverage, test_guidance_ownership, test_integration_shape_classify,
test_instruction_surfaces_agree, test_retrieval_warnings, helpers.py and
conftest.py state the property each test or fixture holds. The issue and
milestone numbers, consolidation dates and one-install measurements that
recorded how each was found are dropped; fixture dates and ids stay.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-21 21:21:52 -04:00
co-authored by Claude Opus 5.5
parent 8518afb54d
commit 91c96a8b8a
7 changed files with 199 additions and 256 deletions
+24 -40
View File
@@ -1,8 +1,7 @@
"""Shared test helpers — the plain functions tests call, as opposed to the
fixtures in conftest.py.
Each of these was copied into several test modules before #2825 consolidated
them; a module imports what it needs with ``from tests.helpers import ...``.
Each of these serves several test modules; a module imports what it needs with ``from tests.helpers import ...``.
"""
from __future__ import annotations
@@ -17,9 +16,7 @@ async def drive_update_note(note, **kwargs):
The patch stack is the point: update_note reaches for a version snapshot,
an embedding refresh and a project reactivation on its way out, none of
which a unit test has. Written twice — once for the snippet mirror
(#3128) and once for the verification fields (#3182/317) — before being
consolidated here.
which a unit test has.
Returns whatever update_note returned; assert on the `note` you passed in.
"""
@@ -43,12 +40,11 @@ def tool_doc(module: str, name: str) -> str:
Flattened because these are hard-wrapped at ~76 characters, so any phrase
worth asserting on is liable to straddle a line break — a property of the
formatter, not of the guidance. The disambiguator guard (#3123) learned
that on its own first run, matching raw text and reporting a phrase absent
that was plainly there.
formatter, not of the guidance. Matching raw text reports a phrase absent
that is plainly there.
Used by every test that pins the docstring CONTRACT rather than its
wording. The tool docstring is the agent-facing contract (rule 119), so
wording. The tool docstring is the agent-facing contract, so
these guards exist to catch it being tidied down to a parameter list.
"""
import importlib
@@ -63,8 +59,7 @@ def compiled_sql(element, dialect=None) -> str:
"""A SQLAlchemy clause or statement rendered as literal SQL text.
For asserting on the shape of a predicate without a database — which is how
the visibility clauses and the knowledge facets are both tested. Was a
private copy in each of those modules before #3128 needed a third.
the visibility clauses and the knowledge facets are both tested.
Pass `dialect` when the assertion is about something only one backend
renders — a Postgres row-lock mode, say. The generic dialect is enough for
@@ -93,9 +88,8 @@ def session_returning(note):
"""``make_mock_session`` whose every ``execute()`` yields `note` (or None).
The commonest shape above the bare session: a service that loads one
record by id and acts on it. Two files had spelled this out identically
before a third was about to (#3207 — derive before the third copy), and
it belongs beside `make_mock_session` for the same reason that one does.
record by id and acts on it, and it belongs beside `make_mock_session`
for the same reason that one does.
For a service making SEVERAL different reads, set
``session.execute.side_effect`` to a list of results instead.
@@ -134,7 +128,7 @@ 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
The hazard this exists for: an auto-created MagicMock attribute
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
@@ -180,10 +174,10 @@ def fake_note(**attrs) -> MagicMock:
"note_type": "note", "is_task": False, "task_kind": "work",
"status": "todo" if is_task else None,
"data": None, "deleted_at": None,
# Milestone 317: a truthy mock here reads as "this note carries a
# A truthy mock here reads as "this note carries a
# check", which trips the guard on records that may not have one.
"verify_with": None, "expires_when": None, "verified_at": None,
# Milestone 381: a truthy mock would read as a live claim.
# A truthy mock would read as a live claim.
"claimed_by": None, "claimed_at": None, "claim_touched_at": None,
"claim_session": None,
}, attrs)
@@ -196,7 +190,7 @@ def fake_task(**attrs) -> MagicMock:
"tags": [], "parent_id": None, "project_id": None, "is_task": True,
"task_kind": "work", "user_id": 7, "deleted_at": None,
"verify_with": None, "expires_when": None, "verified_at": None,
# Milestone 381: a truthy mock would read as a live claim.
# A truthy mock would read as a live claim.
"claimed_by": None, "claimed_at": None, "claim_touched_at": None,
"claim_session": None,
}, attrs)
@@ -219,9 +213,8 @@ def fake_lesson(**attrs) -> MagicMock:
"""A stand-in lesson: a note whose `note_type` is what makes it one.
The title is the subject alone and the trigger lives in `data`, because
that is the record the product creates (milestone 427) — the trigger joins
the title only in the embedded document. A default whose title carried the
trigger would be testing a row only an un-migrated database holds.
that is the record the product creates — the trigger joins the title only
in the embedded document.
The check fields and `arose_from_id` are explicitly None for the reason
`fake_snippet`'s `data` is: `update_note` reads `verify_with` and
@@ -310,8 +303,7 @@ def plain_rule_detail():
the TOOL forwarded the right arguments, so the seam is stubbed the same
way the create/update calls themselves already are.
Consolidated here on its second copy, per this module's own reason for
existing (#2825) — two stubs for one seam drift apart quietly, and a test
Shared here because two stubs for one seam drift apart quietly, and a test
stubbing the seam slightly differently is a test asserting something
slightly different than it appears to.
"""
@@ -356,8 +348,7 @@ def http_sink(reply: bytes = b'{"context":"","note_ids":[]}',
(one dict per request, in order). Lets the shell be tested end to end —
the extraction, the encoding, the URL — without a Scribe instance.
Three test modules each carried their own ``_Sink`` handler before #2904
consolidated them here; pass ``reply`` for the body the hook should see.
Pass ``reply`` for the body the hook should see.
``by_path`` is for a hook that calls more than one route: each request is
answered with the reply for its path (``reply`` when none is listed), and
@@ -406,10 +397,10 @@ def http_sink(reply: bytes = b'{"context":"","note_ids":[]}',
def writepath_cfg(**over):
"""A complete `get_writepath_config` stand-in, built from the registry (#4102).
"""A complete `get_writepath_config` stand-in, built from the registry.
DERIVED, NOT LITERAL, and the reason is a failure mode this file already
warned about in prose without being able to prevent: the write-path hint
DERIVED, NOT LITERAL, because of a failure mode prose cannot prevent: the
write-path hint
drives three arms, each of which reads its numbers out of the config dict
inside a fail-open `except`. A dict missing one key does not raise where a
reader would see it — the arm silently becomes a no-op, which is
@@ -417,9 +408,9 @@ def writepath_cfg(**over):
So the keys come from `retrieval_surfaces.SURFACES`. A seventh surface, or a
rename, changes this helper for free and cannot quietly disable an arm in
ten hand-written dicts that each looked complete on the day they were typed.
hand-written dicts that each looked complete on the day they were typed.
NOT EVERY KEY IS A SURFACE, and that gap already bit once (#4214). The
NOT EVERY KEY IS A SURFACE. The
checkpoint bar has no entry in SURFACES on purpose — everything in that
table is a floor/budget pair belonging to one QUERY, and the checkpoint
runs none, it re-reads hits the rule arms already produced. Adding it there
@@ -458,11 +449,7 @@ def need_tools(*tools):
A skip rather than a stub on purpose. Stubbing `jq` would test the stub —
these tests exist precisely because the shell's behaviour is the thing in
question (#2932), so anything short of the real tool proves nothing.
Consolidated 2026-09-21 from three byte-identical copies in
test_contract_around_the_change, test_hook_json_reader and
test_session_slippage_readout.
question, so anything short of the real tool proves nothing.
"""
import shutil
@@ -482,14 +469,11 @@ async def rule_row(rule_id: int):
map, which can hold a value the database never accepted — a column the
write never reached, a default the ORM supplied rather than the schema.
Opening a new session forces a real read and is the only way `verify_with`
being NULL rather than "" is distinguishable at all (milestone 312).
being NULL rather than "" is distinguishable at all.
Import is lazy because this module is imported by unit tests that have no
database and must not pay for one — the same reason `plugin_config` defers
its service imports.
Consolidated 2026-09-21 from two byte-identical copies in
test_integration_rule_move and test_integration_rule_verification.
"""
from scribe.models import async_session
from scribe.models.rulebook import Rule
@@ -519,7 +503,7 @@ def skill_text(name: str) -> str:
A skill keeps what matters on most turns in SKILL.md and moves what matters
at one moment into sibling files it links (Agent Skills progressive
disclosure, #4398). Both are the skill's own statement, so a test asking
disclosure). Both are the skill's own statement, so a test asking
"does this skill still say X" reads them together rather than pinning X to
whichever file it happens to sit in today.
"""