From 4aae4973f76695c59577312441a46891332d69a2 Mon Sep 17 00:00:00 2001 From: Bryan Van Deusen Date: Thu, 10 Sep 2026 21:31:16 -0400 Subject: [PATCH] fix(tests): the backup stand-ins predate `kind` (#3849 step 1) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two unit tests build a rule with SimpleNamespace rather than the model, so adding a column broke them — the fixture has no `kind` for `_rule_rows` to read. Fixture-only; the export itself was already right, which the existing column-coverage guard confirmed by passing. Adds the guard that coverage check cannot make. `_stand_in` walks `__table__.columns` and proves the KEY is emitted; it cannot prove the VALUE survives. A preference exported as a rule is a silent failure — the restored rule reads fine and simply binds when it was only ever a preference — and a fixture carrying the default would pass against a `_rule_rows` that dropped the field and let the importer's `or "rule"` refill it. So the new test asserts on `preference`, the one value that cannot be reconstructed. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_011cPyzNnegXHr5iRMzzy5KJ --- tests/test_services_backup.py | 28 ++++++++++++++++++++++++++-- 1 file changed, 26 insertions(+), 2 deletions(-) diff --git a/tests/test_services_backup.py b/tests/test_services_backup.py index d85681d..294c7d4 100644 --- a/tests/test_services_backup.py +++ b/tests/test_services_backup.py @@ -388,7 +388,7 @@ def test_rule_rows_carry_the_verification_fields(): row = SimpleNamespace( id=1, topic_id=2, project_id=None, title="t", statement="s", why="w", how_to_apply="h", order_index=0, - when_to_apply="when", tier="conditional", + when_to_apply="when", tier="conditional", kind="rule", verify_with="cat some/file", expires_when="the file grows a shell", verified_at=checked, arose_from_id=99, created_at=checked, updated_at=checked, @@ -415,7 +415,7 @@ def test_rule_rows_keep_an_unverified_rule_unverified(): row = SimpleNamespace( id=1, topic_id=2, project_id=None, title="t", statement="s", why=None, how_to_apply=None, order_index=0, - when_to_apply=None, tier="always_on", + when_to_apply=None, tier="always_on", kind="rule", verify_with=None, expires_when=None, verified_at=None, arose_from_id=None, created_at=datetime(2026, 8, 27, tzinfo=timezone.utc), @@ -426,3 +426,27 @@ def test_rule_rows_keep_an_unverified_rule_unverified(): assert backup._dt_or_none("2026-08-27T12:00:00+00:00") == datetime( 2026, 8, 27, 12, 0, tzinfo=timezone.utc ) + + +def test_rule_rows_carry_the_kind_so_a_preference_does_not_restore_as_a_rule(): + """FORCE has to survive a backup, and the failure would be silent. + + A preference that comes back as a rule is not a missing field anyone would + notice — the rule reads fine, it simply binds when it was only ever meant + to be how the operator prefers things done. Nothing in the restored + rulebook says it used to be softer. + + Asserted with `preference` rather than `rule` on purpose: a fixture + carrying the DEFAULT would pass just as happily against a `_rule_rows` + that dropped the field entirely and let the importer's `or "rule"` fill + the hole back in, which is precisely the bug this guards. + """ + stamp = datetime(2026, 9, 10, tzinfo=timezone.utc) + row = SimpleNamespace( + id=1, topic_id=2, project_id=None, title="t", statement="s", + why=None, how_to_apply=None, order_index=0, + when_to_apply="when", tier="conditional", kind="preference", + verify_with=None, expires_when=None, verified_at=None, + arose_from_id=None, created_at=stamp, updated_at=stamp, + ) + assert backup._rule_rows([row])[0]["kind"] == "preference"