4"""Both-direction selftests for sanctioned suppression ledger operations.
6Imported by suppression_rebind.selftest() through a module binding so
7neither direction executes partially initialized names at import time.
10from __future__
import annotations
13from dataclasses
import replace
15import suppression_rebind
as core
16from suppression_identity
import binding_payload
17from suppression_ledger
import LedgerRow
18from suppression_model
import Suppression
21def _fixture_row(site: str, binding: str, ref: str, state: str =
"retain") -> LedgerRow:
22 """Build one synthetic ledger row."""
23 return LedgerRow(2, site, binding, state,
"r",
"batch-1", ref)
26def _fixture_evidence(raw: object) -> tuple[str, ...]:
27 """Coerce fixture evidence to a string tuple."""
28 if isinstance(raw, tuple)
and all(isinstance(item, str)
for item
in raw):
30 return (
"decision-line:11",
"standard:x")
33def _fixture_record(**fields: object) -> Suppression:
34 """Build one synthetic inventory record with a consistent binding."""
36 path=str(fields.get(
"path",
"src/a.c")),
37 line=int(fields.get(
"line", 10)),
38 column=int(fields.get(
"column", 1)),
39 family=str(fields.get(
"family",
"mcdc-deactivation")),
40 tool=str(fields.get(
"tool",
"llvm-cov")),
41 rule=str(fields.get(
"rule",
"deactivated-condition")),
42 directive=str(fields.get(
"directive",
"mcdc-deactivated")),
43 scope=str(fields.get(
"scope",
"decision-line:11")),
44 provenance=str(fields.get(
"provenance",
"inline-comment")),
45 reason=str(fields.get(
"reason",
"operands cannot vary")),
46 owner=str(fields.get(
"owner",
"first-party")),
48 evidence=_fixture_evidence(fields.get(
"evidence")),
49 match_count=int(fields.get(
"match_count", 1)),
50 recommendation=str(fields.get(
"recommendation",
"revalidate-invariant")),
51 anchor=str(fields.get(
"anchor",
"// mcdc-deactivated:")),
53 binding = hashlib.sha256(binding_payload(record, record.anchor)).hexdigest()
54 return replace(record, site_id=str(fields.get(
"site_id",
"a" * 64)), binding_sha256=binding)
57def _selftest_move() -> list[str]:
58 """Prove a harmless move rebinds and unrelated rows survive."""
59 failures: list[str] = []
60 record = _fixture_record()
61 old_binding = hashlib.sha256(
65 scope=
"decision-line:19",
66 evidence=(
"decision-line:19",
"standard:x"),
71 ref =
"review-decision src/a.c:18 mcdc-deactivated: operands cannot vary."
72 row = _fixture_row(record.site_id, old_binding, ref)
73 other = _fixture_row(
"b" * 64,
"c" * 64,
"review-decision other.")
74 plan, reason = core.plan_rebind([record], [row, other], record.site_id)
76 return [f
"harmless move refused: {reason}"]
77 if (plan.old_refs, plan.new_refs) != ((
"19",), (
"11",)):
78 failures.append(
"move plan reports wrong decision refs")
79 if (plan.marker_old, plan.marker_new) != (
"18",
"10"):
80 failures.append(
"move plan reports wrong marker refs")
81 if plan.new_binding != record.binding_sha256
or plan.old_binding != old_binding:
82 failures.append(
"move plan reports wrong bindings")
83 header =
"site_id\tbinding_sha256\tstate\trationale_id\tbatch_id\tevidence_ref\n"
86 f
"{record.site_id}\t{old_binding}\tretain\tr\tbatch-1\t{ref}\n"
87 f
"{'b' * 64}\t{'c' * 64}\tretain\tr\tbatch-1\treview-decision other.\n"
92 " authority: repository-suppression-review\n"
94 " identity_schema: 2-durable-site-identity\n"
96 f
" rows_sha256: {core.batch_digest([row, other])}\n"
98 result = core.apply_plan(ledger, batches, plan)
99 if isinstance(result, str):
100 return [f
"harmless move apply refused: {result}"]
101 new_ledger, new_batches = result
102 if f
"b{'b' * 63}" not in new_ledger
or "review-decision other." not in new_ledger:
103 failures.append(
"unrelated ledger row changed")
104 old_lines = ledger.split(
"\n")
105 new_lines = new_ledger.split(
"\n")
106 if [line
for line
in new_lines
if record.site_id
not in line] != [
107 line
for line
in old_lines
if record.site_id
not in line
109 failures.append(
"non-target ledger bytes changed")
110 if record.binding_sha256
not in new_ledger
or "src/a.c:10" not in new_ledger:
111 failures.append(
"rebound row missing new binding or line ref")
112 if "assigned_rows: 2" not in new_batches:
113 failures.append(
"batch member count changed")
117def _selftest_retire() -> list[str]:
118 """Prove stale successions retire and bad ones refuse."""
119 failures: list[str] = []
120 live = _fixture_record(
121 path=
"scripts/checks/suppression_rebind_selftest.py", line=20, scope=
"decision-line:20"
124 "review-decision scripts/checks/suppression_rebind_selftest.py:19 "
125 "mcdc-deactivated: operands vary."
127 stale = LedgerRow(2,
"d" * 64, live.binding_sha256,
"retain",
"r",
"batch-1", stale_ref)
128 req = core.RetireRequest(
131 "scripts/checks/suppression_rebind_selftest.py",
135 plan, reason = core.plan_retire([live], [stale], req)
137 return [f
"stale succession refused: {reason}"]
138 if plan.successor != live.site_id
or plan.batch_id !=
"batch-1":
139 failures.append(
"retire plan misreports successor or batch")
140 probe_req = core.RetireRequest(
143 "scripts/checks/suppression_ledger.py",
147 probe, _ = core.plan_retire([live], [stale], probe_req)
148 if probe
is not None:
149 failures.append(
"coords mismatch retires")
151 "site_id\tbinding_sha256\tstate\trationale_id\tbatch_id\tevidence_ref\n"
152 f
"{'d' * 64}\t{live.binding_sha256}\tretain\tr\tbatch-1\t{stale_ref}\n"
157 " authority: repository-suppression-review\n"
158 " date: 2026-09-12\n"
159 " identity_schema: 2-durable-site-identity\n"
160 " assigned_rows: 1\n"
161 f
" rows_sha256: {core.batch_digest([stale])}\n"
163 result = core.apply_retire(ledger, batches, plan)
164 if isinstance(result, str):
165 return [f
"stale apply refused: {result}"]
166 new_ledger, _ = result
167 if "\tsuperseded\tsuperseded-identity-rebinding\t" not in new_ledger:
168 failures.append(
"retire did not flip state and rationale")
169 tokens = new_ledger.split()
170 if f
"replaced-by:{live.site_id}" not in tokens:
171 failures.append(
"retire did not record a clean successor link")
172 live_req = core.RetireRequest(live.site_id, live.site_id,
"src/a.c",
"m",
"x")
173 probe, _ = core.plan_retire([live], [stale], live_req)
174 if probe
is not None:
175 failures.append(
"live site retires")
179def _selftest_relink_legacy(
180 failures: list[str], case: tuple[Suppression, LedgerRow, LedgerRow, str, str]
182 """Prove legacy separators normalize during relink."""
183 live, row, dead, ref, batches = case
184 legacy_ref = ref +
";;;"
185 legacy_row = _fixture_row(
"e" * 64, live.binding_sha256, legacy_ref, state=
"superseded")
186 legacy_req = core.RelinkRequest(
187 "e" * 64,
"d" * 64, live.site_id,
"src/a.c",
"mcdc-deactivated",
"x"
189 legacy_plan, legacy_reason = core.plan_relink([live], [legacy_row, dead], legacy_req)
190 if legacy_plan
is None:
191 failures.append(f
"legacy separators refuse to plan: {legacy_reason}")
194 "site_id\tbinding_sha256\tstate\trationale_id\tbatch_id\tevidence_ref\n"
195 f
"{'e' * 64}\t{live.binding_sha256}\tsuperseded\tr\tbatch-1\t{legacy_ref}\n"
197 legacy_batches = batches.replace(core.batch_digest([row]), core.batch_digest([legacy_row]))
198 legacy_result = core.apply_relink(legacy_ledger, legacy_batches, legacy_plan)
199 if isinstance(legacy_result, str):
200 failures.append(f
"legacy separators refuse to apply: {legacy_result}")
201 elif f
"replaced-by:{live.site_id}" not in legacy_result[0].split():
202 failures.append(
"legacy separators did not normalize to a clean link")
205def _selftest_relink() -> list[str]:
206 """Prove dangling links refresh and bad ones refuse."""
207 failures: list[str] = []
208 live = _fixture_record()
210 f
"review-decision src/a.c:19 mcdc-deactivated: operands cannot vary. replaced-by:{'d' * 64}"
212 row = _fixture_row(
"e" * 64, live.binding_sha256, ref, state=
"superseded")
213 dead = _fixture_row(
"d" * 64, live.binding_sha256, ref)
215 req = core.RelinkRequest(
"e" * 64,
"d" * 64, live.site_id,
"src/a.c",
"mcdc-deactivated",
"x")
216 plan, reason = core.plan_relink([live], rows, req)
218 return [f
"dangling link refused: {reason}"]
219 if plan.new_target != live.site_id:
220 failures.append(
"relink plan misreports the successor")
222 "site_id\tbinding_sha256\tstate\trationale_id\tbatch_id\tevidence_ref\n"
223 f
"{'e' * 64}\t{live.binding_sha256}\tsuperseded\tr\tbatch-1\t{ref}\n"
228 " authority: repository-suppression-review\n"
229 " date: 2026-09-12\n"
230 " identity_schema: 2-durable-site-identity\n"
231 " assigned_rows: 1\n"
232 f
" rows_sha256: {core.batch_digest([row])}\n"
234 result = core.apply_relink(ledger, batches, plan)
235 if isinstance(result, str):
236 return [f
"relink apply refused: {result}"]
237 new_ledger, _ = result
238 if f
"replaced-by:{live.site_id}" not in new_ledger:
239 failures.append(
"relink did not refresh the successor link")
240 if f
"replaced-by:{'d' * 64}" in new_ledger:
241 failures.append(
"relink left the dead link behind")
242 _selftest_relink_legacy(failures, (live, row, dead, ref, batches))
243 stray = _fixture_row(
246 "review scripts/checks/suppression_model.py:9 directive:other-kind rule:x.",
248 stray_req = core.RelinkRequest(
"e" * 64,
"d" * 64, live.site_id,
"src/a.c",
"m",
"x")
249 plan, _ = core.plan_relink([live], [row, stray], stray_req)
251 failures.append(
"stale old-target coordinates relink")
255def _selftest_restore() -> list[str]:
256 """Prove live resolved rows reinstate and drift refuses."""
257 failures: list[str] = []
258 record = _fixture_record()
259 legacy = f
"history replaced-by:{'e' * 64}; retirement note."
260 row = LedgerRow(2, record.site_id, record.binding_sha256,
"resolved",
"r",
"batch-1", legacy)
261 rationales = {
"r": {
"state":
"retain"}}
262 plan, reason = core.plan_restore(
263 [record], [row], rationales, core.RestoreRequest(record.site_id,
"r",
"back")
266 return [f
"live resolved row refused: {reason}"]
268 "site_id\tbinding_sha256\tstate\trationale_id\tbatch_id\tevidence_ref\n"
269 f
"{record.site_id}\t{record.binding_sha256}\tresolved\tr\tbatch-1\t{legacy}\n"
274 " authority: repository-suppression-review\n"
275 " date: 2026-09-12\n"
276 " identity_schema: 2-durable-site-identity\n"
277 " assigned_rows: 1\n"
278 f
" rows_sha256: {core.batch_digest([row])}\n"
280 result = core.apply_restore(ledger, batches, plan)
281 if isinstance(result, str):
282 return [f
"restore apply refused: {result}"]
283 new_ledger, _ = result
284 if "\tretain\tr\t" not in new_ledger
or "reinstated: back" not in new_ledger:
285 failures.append(
"restore did not flip state and rationale")
286 if "retirement note. reinstated: back" not in new_ledger:
287 failures.append(
"restore note glued to trailing prose")
288 drifted = replace(record, binding_sha256=
"0" * 64)
289 drift_req = core.RestoreRequest(record.site_id,
"r",
"back")
290 plan, _ = core.plan_restore([drifted], [row], rationales, drift_req)
292 failures.append(
"drifted binding restores")
296def _selftest_refusals() -> list[str]:
297 """Prove semantic changes and scope violations fail closed."""
298 failures: list[str] = []
299 record = _fixture_record()
300 ref =
"review-decision src/a.c:19 mcdc-deactivated: operands cannot vary."
301 old_binding = hashlib.sha256(
305 scope=
"decision-line:19",
306 evidence=(
"decision-line:19",
"standard:x"),
311 row = _fixture_row(record.site_id, old_binding, ref)
312 changed = replace(record, reason=
"totally different justification")
313 changed = replace(changed, site_id=record.site_id, binding_sha256=
"0" * 64)
314 plan, _ = core.plan_rebind([changed], [row], record.site_id)
316 failures.append(
"semantic reason change rebinds")
317 wrong_kind = replace(record, directive=
"other-kind", site_id=record.site_id)
318 plan, _ = core.plan_rebind([wrong_kind], [row], record.site_id)
320 failures.append(
"different suppression kind rebinds")
321 plan, _ = core.plan_rebind([record], [row],
"d" * 64)
323 failures.append(
"unknown site_id rebinds")
324 twin = replace(record, line=11)
325 plan, _ = core.plan_rebind([record, twin], [row], record.site_id)
327 failures.append(
"ambiguous live site rebinds")
328 draft = LedgerRow(2, record.site_id, old_binding,
"unreviewed",
"r",
"batch-1", ref)
329 plan, _ = core.plan_rebind([record], [draft], record.site_id)
331 failures.append(
"unreviewed site rebinds")
335def run_selftests() -> list[str]:
336 """Run every rebind selftest; return failure strings."""
337 failures: list[str] = []
338 failures.extend(_selftest_move())
339 failures.extend(_selftest_refusals())
340 failures.extend(_selftest_retire())
341 failures.extend(_selftest_relink())
342 failures.extend(_selftest_restore())