refactor(tests): derive the two duplicated test helpers into tests/helpers.py (#4276)
CI & Build / Python lint (push) Successful in 2s
CI & Build / Plugin hooks (push) Successful in 9s
CI & Build / integration (push) Successful in 48s
CI & Build / TypeScript typecheck (push) Successful in 52s
CI & Build / Python tests (push) Successful in 1m37s
CI & Build / Build & push image (push) Successful in 15s

Derive-first, ahead of the tests shape pay-down. An AST pass over all 2628
definitions under `tests/` found exactly three helper bodies duplicated
across files. Two are real copies and are consolidated here; the third is
not, and is left alone.

`need_tools(*tools)` — byte-identical in three hook-test modules, each
skipping when `jq`/`awk`/`git` is absent from PATH. Now snippet #4277. The
`import shutil` each file carried existed only to serve it and goes with it.

`rule_row(rule_id)` — byte-identical in two integration modules, reading a
Rule back through a SEPARATE session so the assertion is about what Postgres
holds rather than what the writing session's identity map remembers. Now
snippet #4278. Its imports are lazy, because `tests/helpers.py` is imported
by unit tests that have no database, which is the same reason
`plugin_config` defers its service imports.

NOT consolidated: `_side(uid, k, d="")` in test_services_plugin_context and
test_write_path_trigger. The body is identical but it closes over a
module-local `stored` dict, so it is not self-contained and "moving" it
would mean inventing a parameter neither call site wants. That is convention
plumbing — two tests independently writing the same one-line side_effect —
and it is dismissed in the ledger rather than lifted.

Worth recording for the next pass: a repeated NAME is not a family. `_row`
is defined in five modules and only two of those share a body; the other
three (test_list_rows_brief, test_calibration_stamp, test_shape_ledger)
build entirely different objects. Grouping by name would have consolidated
three things that have nothing in common.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01821k5B3Ysecp9fNYs92Kuy
This commit is contained in:
2026-09-21 18:27:05 -04:00
co-authored by Claude Opus 5
parent b47dc97ca8
commit d49e4ad106
6 changed files with 81 additions and 54 deletions
+52
View File
@@ -418,3 +418,55 @@ def writepath_cfg(**over):
}
cfg.update(over)
return cfg
def need_tools(*tools):
"""Skip unless every named executable is on PATH.
For the hook tests, which drive `plugin/hooks/*.sh` through a real shell
and therefore depend on whatever that shell reaches for — `jq`, `awk`,
`git`, `curl`. Those are present on the CI image and routinely absent from
a developer's box, and the honest answer there is "not exercised", not a
failure: a red test would say the hook is broken when nothing about the
hook was ever run.
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.
"""
import shutil
import pytest
for t in tools:
if shutil.which(t) is None:
pytest.skip(f"hook runtime tool {t!r} not installed")
async def rule_row(rule_id: int):
"""Read a Rule straight from Postgres, outside whatever session the code
under test used.
THE POINT IS THE SEPARATE SESSION. An integration test that asserts on the
object the service just returned is asserting on that session's identity
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).
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
async with async_session() as s:
return await s.get(Rule, rule_id)