diff --git a/CHANGELOG.md b/CHANGELOG.md index 389038fa..e10e0005 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -297,6 +297,69 @@ true until the next version shipped. Guard half 347 passed, 913 checks; the two cluster-side files touched, 22 passed, 79 checks. `guard_tests` re-derived by collection, 346 -> 347. +- A mutation with two genuine targets could not be recorded at all (#1014). + + `merge --mutation NAME` refused any run in which more than one check failed. The + guard is right about the hazard -- attributing a mutation to a bystander records + collateral damage as evidence -- and wrong about the remedy. A mutation with TWO + GENUINE targets is ordinary, and for it the only permitted merge was + `--reds-are-real`, which writes `-` in the mutation column. + + So the catalogue that column exists to become could never hold the entry it most + exists for: the one that says WHICH CHECKS SHARE A CAUSE. + + `--target CHECK`, repeatable, makes the caller assert the attribution, exactly as + `--reds-are-real` makes them assert that a red is real. The guard keeps its teeth + in both directions: + + a red not named as a target refused, and the refusal names it + a target that did not fail refused, because the claim is wrong + --target without --mutation refused, because it attributes nothing + one red and no --target merges, as every existing caller does + + THE TWO ROWS ARE BACK-FILLED, and the mutation was re-run rather than recalled. + `native_join_runtime_filter`'s two ever-red rows now name + `pgcolumnar.enable_join_runtime_filter boot value true -> false` instead of `-`. + + Getting there cost two wrong attempts, and the reason is a PostgreSQL fact worth + writing down because it is not the obvious one. + + Mutating only the C initializer is INERT: 46 checks, 0 failed. + `DefineCustomBoolVariable` assigns the boot value to the variable at registration, + so for a bool GUC the initializer decides nothing at run time. + + Mutating only the `DefineCustomBoolVariable` boot value will not start at all: + + LOG: GUC (PGC_BOOL) pgcolumnar.enable_join_runtime_filter, boot_val=0, C-var=1 + TRAP: failed Assert("check_GUC_init(variable)"), File: "guc.c", Line: 4944 + + `check_GUC_init` does NOT require the two to agree. Read at the source rather + than inferred from the trap, `src/backend/utils/misc/guc.c`: + + case PGC_BOOL: + if (*conf->variable && !conf->boot_val) /* traps: C-var true, boot false */ + + It is ASYMMETRIC: a C variable left `true` against a `false` boot value traps, and + a `false` initializer against a `true` boot value is accepted silently. `PGC_INT` + and `PGC_REAL` apply the same asymmetry against zero; `PGC_ENUM` requires equality + unconditionally. + + So this mutation is two lines, but not because the two must match -- because one + line alone does nothing and the other alone will not boot. The third attempt + changed both, fingerprinted the `.so` before and after to prove the build took + (`53c3f626889c` -> `7f67e077f26c`), reddened exactly the two checks, and restored + byte-exact with the `.so` back to `53c3f626889c`. + + The census does not move: both rows were already ever-red, so + `checks_never_observed_red` stays at 1277 and the ledger stays 1285 rows, all + carrying `15;16;17;18;19`. + + Both harnesses, independently: nine arms in `selftest/410` and six in + `test_mutation_ledger.py`, each with its own fixture and its own names. Every + refusal arm greps its MESSAGE rather than only its status -- `--target` did not + exist before this change, so argparse exited 2 for an unknown flag, and three arms + written against the status alone passed against the absent feature. Measured + before the implementation, which is why they are written the other way. - Four secret-leak claims over the PG server log could pass having read nothing (#1032). diff --git a/test/check_ledger.tsv b/test/check_ledger.tsv index ac8d2c73..1c3d56cc 100644 --- a/test/check_ledger.tsv +++ b/test/check_ledger.tsv @@ -880,6 +880,7 @@ harness_selftest 410-a-check-must-have-been-red --mutation across two failing ch harness_selftest 410-a-check-must-have-been-red --prune REFUSES the whole prune when any orphan carries history 15;16;17;18;19 never - harness_selftest 410-a-check-must-have-been-red --prune removes NOTHING from a part that skipped 15;16;17;18;19 never - harness_selftest 410-a-check-must-have-been-red --prune removes a historyless orphan and says which 15;16;17;18;19 never - +harness_selftest 410-a-check-must-have-been-red --target without --mutation is refused, because it attributes nothing 15;16;17;18;19 never - harness_selftest 410-a-check-must-have-been-red a PG15 run names no orphan, having seen every check the ledger claims for 15 15;16;17;18;19 never - harness_selftest 410-a-check-must-have-been-red a before-log and an after-log together are refused, as rename-scan refuses them 15;16;17;18;19 never - harness_selftest 410-a-check-must-have-been-red a before-log and an after-log together are refused, not silently empty 15;16;17;18;19 never - @@ -921,6 +922,7 @@ harness_selftest 410-a-check-must-have-been-red a second major is ADDED to the s harness_selftest 410-a-check-must-have-been-red a second mutation ACCUMULATES rather than replacing the first 15;16;17;18;19 never - harness_selftest 410-a-check-must-have-been-red a skipped part is NOT an orphan, so --orphans-only returns 0 15;16;17;18;19 never - harness_selftest 410-a-check-must-have-been-red a stale prior is named WITH its distance from HEAD 15;16;17;18;19 never - +harness_selftest 410-a-check-must-have-been-red a target that did not fail is refused, because the claim is wrong 15;16;17;18;19 never - harness_selftest 410-a-check-must-have-been-red a verdict pgc_record cannot emit is an integrity failure 15;16;17;18;19 never - harness_selftest 410-a-check-must-have-been-red an absolute budget path resolves against git rather than shrugging 15;16;17;18;19 never - harness_selftest 410-a-check-must-have-been-red an empty check name is one too, because it names no check 15;16;17;18;19 never - @@ -963,6 +965,8 @@ harness_selftest 410-a-check-must-have-been-red and one that stayed green keeps harness_selftest 410-a-check-must-have-been-red and only when there is a base, so a push build does not fail on it 15;16;17;18;19 never - harness_selftest 410-a-check-must-have-been-red and records neither as ever having been red 15;16;17;18;19 never - harness_selftest 410-a-check-must-have-been-red and that is not a failure, because a run sees ONE major 15;16;17;18;19 never - +harness_selftest 410-a-check-must-have-been-red and that refusal names the target that stayed green 15;16;17;18;19 never - +harness_selftest 410-a-check-must-have-been-red and that refusal says --target needs --mutation 15;16;17;18;19 never - harness_selftest 410-a-check-must-have-been-red and the PG18-only row is one it cannot speak about, not one that vanished 15;16;17;18;19 never - harness_selftest 410-a-check-must-have-been-red and the addition in the other part is not called a rename 15;16;17;18;19 never - harness_selftest 410-a-check-must-have-been-red and the census line it always printed is still there 15;16;17;18;19 never - @@ -973,10 +977,12 @@ harness_selftest 410-a-check-must-have-been-red and the history it is about to l harness_selftest 410-a-check-must-have-been-red and the history it would lose is printed with it 15;16;17;18;19 never - harness_selftest 410-a-check-must-have-been-red and the ledger is one row shorter afterwards 15;16;17;18;19 never - harness_selftest 410-a-check-must-have-been-red and the message says how to fix it, because regenerating is the intended action 15;16;17;18;19 never - +harness_selftest 410-a-check-must-have-been-red and the mutation is recorded against both of them 15;16;17;18;19 never - harness_selftest 410-a-check-must-have-been-red and the prior policy is chosen from a closed set, not taken as a ref 15;16;17;18;19 never - harness_selftest 410-a-check-must-have-been-red and the refusal names both values 15;16;17;18;19 never - harness_selftest 410-a-check-must-have-been-red and the refusal names how many failed, so the author can narrow the run 15;16;17;18;19 never - harness_selftest 410-a-check-must-have-been-red and the refusal names the check that reddened 15;16;17;18;19 never - +harness_selftest 410-a-check-must-have-been-red and the refusal names the check that was not claimed as a target 15;16;17;18;19 never - harness_selftest 410-a-check-must-have-been-red and the refusal names the raise 15;16;17;18;19 never - harness_selftest 410-a-check-must-have-been-red and the refusal quotes the claim beside the measurement 15;16;17;18;19 never - harness_selftest 410-a-check-must-have-been-red and the runner names no remote at that call site 15;16;17;18;19 never - @@ -997,10 +1003,12 @@ harness_selftest 410-a-check-must-have-been-red control: a well-formed log still harness_selftest 410-a-check-must-have-been-red control: all-clean logs leave both flags down 15;16;17;18;19 never - harness_selftest 410-a-check-must-have-been-red control: an all-PASS log still merges with no flag at all 15;16;17;18;19 never - harness_selftest 410-a-check-must-have-been-red control: and a clean run still returns 0 under it 15;16;17;18;19 never - +harness_selftest 410-a-check-must-have-been-red control: and still records the mutation against the check that reddened 15;16;17;18;19 never - harness_selftest 410-a-check-must-have-been-red control: and that one did prune, so 0 is not a refusal in disguise 15;16;17;18;19 never - harness_selftest 410-a-check-must-have-been-red control: and the ledger really is shorter afterwards 15;16;17;18;19 never - harness_selftest 410-a-check-must-have-been-red control: on a covered major a new check is named and refused 15;16;17;18;19 never - harness_selftest 410-a-check-must-have-been-red control: one orphan among clean logs raises only the orphan flag 15;16;17;18;19 never - +harness_selftest 410-a-check-must-have-been-red control: one red and no --target still merges 15;16;17;18;19 never - harness_selftest 410-a-check-must-have-been-red control: the row in the part the run never mentioned SURVIVES the prune 15;16;17;18;19 never - harness_selftest 410-a-check-must-have-been-red control: the same log merges with a different reason, so the refusal above is --mutation-across-two-checks and not the log 15;16;17;18;19 never - harness_selftest 410-a-check-must-have-been-red control: the same row with a real major is read without complaint 15;16;17;18;19 never - @@ -1011,6 +1019,8 @@ harness_selftest 410-a-check-must-have-been-red every row has six fields and no harness_selftest 410-a-check-must-have-been-red it is still a finding, so the scan does not return success 15;16;17;18;19 never - harness_selftest 410-a-check-must-have-been-red lowering it is allowed, which is the direction the burn-down goes 15;16;17;18;19 never - harness_selftest 410-a-check-must-have-been-red merging a green run records both checks 15;16;17;18;19 never - +harness_selftest 410-a-check-must-have-been-red naming both failing checks as targets permits the merge 15;16;17;18;19 never - +harness_selftest 410-a-check-must-have-been-red naming only one of two failing checks is still refused 15;16;17;18;19 never - harness_selftest 410-a-check-must-have-been-red nor is one merely removed 15;16;17;18;19 never - harness_selftest 410-a-check-must-have-been-red once the suite is covered, a new check in it IS refused 15;16;17;18;19 never - harness_selftest 410-a-check-must-have-been-red one --mutation cannot be attributed across several runs at once 15;16;17;18;19 never - @@ -1266,7 +1276,7 @@ native_join_runtime_filter native_join_runtime_filter collation-mismatch answer native_join_runtime_filter native_join_runtime_filter collation-mismatch bloom rejects most non-matches 15;16;17;18;19 never - native_join_runtime_filter native_join_runtime_filter collation-mismatch interval stays off 15;16;17;18;19 never - native_join_runtime_filter native_join_runtime_filter collation-mismatch plan has coordinator 15;16;17;18;19 never - -native_join_runtime_filter native_join_runtime_filter default plan has runtime coordinator 15;16;17;18;19 2026-09-12 - +native_join_runtime_filter native_join_runtime_filter default plan has runtime coordinator 15;16;17;18;19 2026-09-18 pgcolumnar.enable_join_runtime_filter boot value true -> false native_join_runtime_filter native_join_runtime_filter early limit equals heap 15;16;17;18;19 never - native_join_runtime_filter native_join_runtime_filter early limit has coordinator 15;16;17;18;19 never - native_join_runtime_filter native_join_runtime_filter empty-build answer equals heap 15;16;17;18;19 never - @@ -1278,7 +1288,7 @@ native_join_runtime_filter native_join_runtime_filter int4/int8 answer equals he native_join_runtime_filter native_join_runtime_filter int4/int8 bloom rejects most non-matches 15;16;17;18;19 never - native_join_runtime_filter native_join_runtime_filter int4/int8 interval stays off 15;16;17;18;19 never - native_join_runtime_filter native_join_runtime_filter int4/int8 plan has coordinator 15;16;17;18;19 never - -native_join_runtime_filter native_join_runtime_filter join runtime filter defaults on 15;16;17;18;19 2026-09-12 - +native_join_runtime_filter native_join_runtime_filter join runtime filter defaults on 15;16;17;18;19 2026-09-18 pgcolumnar.enable_join_runtime_filter boot value true -> false native_join_runtime_filter native_join_runtime_filter plan has build tap 15;16;17;18;19 never - native_join_runtime_filter native_join_runtime_filter plan has runtime coordinator 15;16;17;18;19 never - native_join_runtime_filter native_join_runtime_filter plan retains core Hash Join 15;16;17;18;19 never - diff --git a/test/check_ledger_budget.txt b/test/check_ledger_budget.txt index a5d6482f..2e99987c 100644 --- a/test/check_ledger_budget.txt +++ b/test/check_ledger_budget.txt @@ -78,4 +78,4 @@ suites_not_covered 249 # clean union and only the COUNT had to be re-derived. Both files conflicted loudly # here, which is the safe half: the ledger is always right and only sometimes speaks. # Re-derived by the command above on the merged tree, never by adding the deltas: -checks_never_observed_red 1330 +checks_never_observed_red 1340 diff --git a/test/pgc_ledger.py b/test/pgc_ledger.py index 53440cf0..0aac5f3b 100755 --- a/test/pgc_ledger.py +++ b/test/pgc_ledger.py @@ -459,17 +459,65 @@ def cmd_merge(args): # kills that check, which is the opposite of what this column is for. # Measured on #918: a two-FAIL log merged with --mutation MUTATION_A recorded # it against both. Reported by @linuxhikerpm. + # --target NAMES WHAT THE MUTATION KILLED, and means nothing on its own (#1014). + if getattr(args, "target", None) and not args.mutation: + raise LedgerError( + "--target names what a mutation killed, so it needs --mutation to say " + "which mutation. On its own it attributes nothing.") + if args.mutation: failed = sorted({key for _p, seen in runs for key, vs in seen.items() if any(v == "FAIL" for v, _m in vs)}) - if len(failed) > 1: + named = list(dict.fromkeys(getattr(args, "target", None) or [])) + if named: + # A MUTATION WITH TWO GENUINE TARGETS IS ORDINARY, not exotic (#1014). + # Reverting the `enable_join_runtime_filter` boot value reddens both + # `join runtime filter defaults on` and `default plan has runtime + # coordinator`; neither is collateral, because both read the default + # directly, which is why one change kills both. + # + # The tool cannot tell that from "one target and one bystander", and it + # used to resolve the ambiguity by recording NOTHING: the only permitted + # merge was --reds-are-real, which writes `-` in the mutation column. So + # the catalogue this column exists to become could never hold the entry + # it most exists for -- the one that says WHICH CHECKS SHARE A CAUSE. + # + # The caller ASSERTS the attribution instead, exactly as --reds-are-real + # makes them assert that a red is real. The guard keeps its teeth in the + # case it was written for: a red not named is still collateral. + # + # Matched on the check NAME, which is what an author knows and what the + # PR body will say. The ledger key carries suite and part as well, so a + # name shared by two parts names both -- deliberately: they are the same + # assertion made twice, and a mutation that kills one kills the other. + failed_names = {n for _s, _p, n in failed} + unhit = [t for t in named if t not in failed_names] + if unhit: + listed = "\n".join(f" {t}" for t in unhit[:6]) + more = "" if len(unhit) <= 6 else f"\n ... and {len(unhit) - 6} more" + raise LedgerError( + f"{len(unhit)} check(s) were named as a target but did not fail in " + f"this run:\n{listed}{more}\n A mutation that does not redden " + f"what you aimed at is a finding about the mutation, not a row to " + f"write down. Re-run, or drop the target you did not hit.") + unnamed = [k for k in failed if k[2] not in set(named)] + if unnamed: + listed = "\n".join(f" {s}\t{p}\t{n}" for s, p, n in unnamed[:6]) + more = "" if len(unnamed) <= 6 else f"\n ... and {len(unnamed) - 6} more" + raise LedgerError( + f"{len(unnamed)} check(s) failed but was not named as a target:\n" + f"{listed}{more}\n An unnamed red is collateral damage, and " + f"recording the mutation against it would be evidence that the " + f"mutation kills that check. Name it with --target if it is a real " + f"target, or narrow the run.") + elif len(failed) > 1: listed = "\n".join(f" {s}\t{p}\t{n}" for s, p, n in failed[:6]) more = "" if len(failed) <= 6 else f"\n ... and {len(failed) - 6} more" raise LedgerError( f"--mutation names one check, but {len(failed)} checks failed in this " f"run:\n{listed}{more}\n Attributing it to all of them would record " - f"collateral damage as evidence. Merge without --mutation, or narrow the " - f"run to the check the mutation targets.") + f"collateral damage as evidence. Name every one you meant to hit with " + f"--target, merge without --mutation, or narrow the run.") # A RED NEEDS A REASON (#946). `merge` already refuses a log that does not # RECONCILE, and reconciliation is not the property that matters: both logs that @@ -1252,6 +1300,10 @@ def main(argv=None): m.add_argument("--ledger", required=True) m.add_argument("--date", default="unknown") m.add_argument("--mutation", default="") + m.add_argument("--target", action="append", default=[], metavar="CHECK", + help="a check name this mutation was aimed at; repeatable. " + "Required when a mutation reddens more than one check, so " + "the attribution is asserted rather than inferred (#1014)") m.add_argument("--reds-are-real", action="store_true", help="the FAIL records in these logs are a genuine observation " "of the code under test, not an artifact of the environment") diff --git a/test/pytest/TESTS.md b/test/pytest/TESTS.md index 15194215..51fd9627 100644 --- a/test/pytest/TESTS.md +++ b/test/pytest/TESTS.md @@ -2359,6 +2359,18 @@ Last-write-wins records the most recent attack rather than the catalogue the col exists to become. One `--mutation` copied across several logs attributes a deliberate change to failures it had nothing to do with, and is refused. +### `test_a_mutation_with_two_targets_can_be_recorded_when_both_are_named` + +A mutation with two GENUINE targets is ordinary: reverting the +`enable_join_runtime_filter` boot value reddens two checks, and neither is collateral +because both read the default directly. The tool could not tell that from "one target +and one bystander" and resolved the ambiguity by recording NOTHING, so the column +could never hold the entry it most exists for -- the one saying which checks share a +cause. `--target` makes the caller assert the attribution, as `--reds-are-real` makes +them assert that a red is real. Six arms: both named permits it and records both; one +named of two is still refused and names the unclaimed check; a target that did not +fail is refused and names it; and `--target` without `--mutation` is refused. + ### `test_a_log_that_does_not_parse_is_not_evidence` `read_records` accepted `len(f) >= 5`, so a record missing its reason, a verdict diff --git a/test/pytest/expected_tests.txt b/test/pytest/expected_tests.txt index adb4d0bf..983a5189 100644 --- a/test/pytest/expected_tests.txt +++ b/test/pytest/expected_tests.txt @@ -173,7 +173,7 @@ # 361 here, which is a coincidence of this merge rather than a method: the deltas were # measured against different trees. Re-derived by collection on the merged tree, which # is the only resolution this number has. -guard_tests 362 +guard_tests 363 # The complement: tests that need the driver and a throwaway cluster. Until #1016 these ran # in no CI job at all -- a quarter of the corpus, green when somebody ran them by hand and diff --git a/test/pytest/test_mutation_ledger.py b/test/pytest/test_mutation_ledger.py index 31a87325..397de721 100644 --- a/test/pytest/test_mutation_ledger.py +++ b/test/pytest/test_mutation_ledger.py @@ -125,6 +125,67 @@ def test_the_mutation_column_accumulates_rather_than_overwriting(tmp_path, expec "one mutation cannot be attributed across several runs at once") +def test_a_mutation_with_two_targets_can_be_recorded_when_both_are_named(tmp_path, expect): + """#1014. A mutation with TWO GENUINE TARGETS is ordinary, not exotic. + + Reverting the `enable_join_runtime_filter` boot value reddens both `join runtime + filter defaults on` and `default plan has runtime coordinator`. Neither is + collateral -- both read the default directly, which is why one change kills both. + + The tool could not tell that from "one target and one bystander" and resolved the + ambiguity by recording NOTHING: the only permitted merge was `--reds-are-real`, + which writes `-` in the mutation column. So the catalogue this column exists to + become could never hold the entry it most exists for, the one saying WHICH CHECKS + SHARE A CAUSE. + + Its own fixture and its own names, sharing nothing with the shell part. + """ + two = _w(tmp_path, "two.log", + "RESULT\tdemo\tpart1\taimed at\tFAIL\t18\t\n" + "RESULT\tdemo\tpart1\talso aimed at\tFAIL\t18\t\n" + "checks run: 2\n") + + out, rc = _run("merge", "--ledger", _w(tmp_path, "a.tsv", ""), "--date", "2026-09-12", + "--mutation", "M", two) + expect.num(rc, 2, "two reds and no targets is still refused") + expect.text("--target" if "--target" in out else f"absent; got {out[:200]!r}", + "--target", "and the refusal offers --target as the way through") + + led = _w(tmp_path, "b.tsv", "") + out, rc = _run("merge", "--ledger", led, "--date", "2026-09-12", "--mutation", "M", + "--target", "aimed at", "--target", "also aimed at", two) + expect.num(rc, 0, "naming both permits the merge") + expect.text(",".join(sorted(r[5] for r in _rows(led))), "M,M", + "and the mutation is recorded against both of them") + + # THE GUARD KEEPS ITS TEETH. A red nobody claimed is collateral, and recording + # the mutation against it would be evidence that the mutation kills that check. + out, rc = _run("merge", "--ledger", _w(tmp_path, "c.tsv", ""), "--date", "2026-09-12", + "--mutation", "M", "--target", "aimed at", two) + expect.num(rc, 2, "naming only one of the two is still refused") + expect.text("failed but was not named as a target" if "failed but was not named as a target" in out else f"absent; got {out[:200]!r}", + "failed but was not named as a target", "and the refusal says which kind of mistake this is") + expect.text("also aimed at" if "also aimed at" in out else f"absent; got {out[:200]!r}", + "also aimed at", "naming the check nobody claimed") + + # THE OTHER DIRECTION: a target that did not redden means the mutation did not do + # what the author believes, which is a finding rather than a row. + out, rc = _run("merge", "--ledger", _w(tmp_path, "d.tsv", ""), "--date", "2026-09-12", + "--mutation", "M", "--target", "aimed at", "--target", "also aimed at", + "--target", "never ran", two) + expect.num(rc, 2, "a target that did not fail is refused") + expect.text("named as a target but did not fail" if "named as a target but did not fail" in out else f"absent; got {out[:200]!r}", + "named as a target but did not fail", "and the refusal says so in those terms") + expect.text("never ran" if "never ran" in out else f"absent; got {out[:200]!r}", + "never ran", "naming the target that stayed green") + + out, rc = _run("merge", "--ledger", _w(tmp_path, "e.tsv", ""), "--date", "2026-09-12", + "--target", "aimed at", two) + expect.num(rc, 2, "--target without --mutation is refused") + expect.text("needs --mutation" if "needs --mutation" in out else f"absent; got {out[:200]!r}", + "needs --mutation", "because on its own it attributes nothing") + + def test_two_runs_of_a_check_are_not_a_duplicate_of_it(tmp_path, expect): """Merging the logs first cannot tell "the same check in two runs" from "the same name twice in one run", and reported the first as the second.""" diff --git a/test/selftest/410-a-check-must-have-been-red.sh b/test/selftest/410-a-check-must-have-been-red.sh index 40115299..edd33d3b 100644 --- a/test/selftest/410-a-check-must-have-been-red.sh +++ b/test/selftest/410-a-check-must-have-been-red.sh @@ -121,6 +121,67 @@ check "and the refusal names how many failed, so the author can narrow the run" check "control: the same log merges with a different reason, so the refusal above is --mutation-across-two-checks and not the log" \ "$(_led_rc merge --reds-are-real --ledger "$_lw/m2.tsv" --date 2026-09-10 "$_lw/twofail.log")" "0" +# ---- ...unless the caller NAMES them (#1014) -------------------------------- +# +# A MUTATION WITH TWO GENUINE TARGETS IS ORDINARY. #1008 is the live case: +# reverting the `enable_join_runtime_filter` boot value reddens both `join runtime +# filter defaults on` and `default plan has runtime coordinator`. Neither is +# collateral -- both read the default directly, which is why one change kills both. +# +# The tool could not tell that from "one target and one bystander", and resolved +# the ambiguity by recording NOTHING: the only permitted merge was +# `--reds-are-real`, which writes `-` in the mutation column. So the catalogue this +# column exists to become could never hold the entry it most exists for -- the +# mutation that tells a reader WHICH CHECKS SHARE A CAUSE. +# +# The caller asserts the attribution, exactly as `--reds-are-real` makes them +# assert that a red is real. A red not named is still collateral and still refused. +# +# EVERY REFUSAL ARM BELOW GREPS ITS MESSAGE, not just the status. `--target` did +# not exist before this change, so argparse exited 2 for an unknown flag -- and an +# arm asserting only `rc=2` would have passed against the absent feature, which is +# the shape this repository keeps finding. +check "naming both failing checks as targets permits the merge" \ + "$(_led_rc merge --ledger "$_lw/m3.tsv" --date 2026-09-10 --mutation M \ + --target 'the target' --target 'collateral' "$_lw/twofail.log")" "0" +check "and the mutation is recorded against both of them" \ + "$(awk -F'\t' '$6=="M"' "$_lw/m3.tsv" | grep -c .)" "2" + +check "naming only one of two failing checks is still refused" \ + "$(_led_rc merge --ledger "$_lw/m4.tsv" --date 2026-09-10 --mutation M \ + --target 'the target' "$_lw/twofail.log")" "2" +check "and the refusal names the check that was not claimed as a target" \ + "$(_led_run merge --ledger "$_lw/m4.tsv" --date 2026-09-10 --mutation M \ + --target 'the target' "$_lw/twofail.log" \ + | grep -c 'failed but was not named as a target')" "1" + +# THE OTHER DIRECTION. A target that did not redden means the author believes the +# mutation kills a check and it does not. That is a finding about the mutation, +# not a row to write down. +check "a target that did not fail is refused, because the claim is wrong" \ + "$(_led_rc merge --ledger "$_lw/m5.tsv" --date 2026-09-10 --mutation M \ + --target 'the target' --target 'collateral' --target 'never ran' \ + "$_lw/twofail.log")" "2" +check "and that refusal names the target that stayed green" \ + "$(_led_run merge --ledger "$_lw/m5.tsv" --date 2026-09-10 --mutation M \ + --target 'the target' --target 'collateral' --target 'never ran' \ + "$_lw/twofail.log" | grep -c 'named as a target but did not fail')" "1" + +check "--target without --mutation is refused, because it attributes nothing" \ + "$(_led_rc merge --ledger "$_lw/m6.tsv" --date 2026-09-10 \ + --target 'the target' "$_lw/twofail.log")" "2" +check "and that refusal says --target needs --mutation" \ + "$(_led_run merge --ledger "$_lw/m6.tsv" --date 2026-09-10 \ + --target 'the target' "$_lw/twofail.log" \ + | grep -c 'target names what a mutation killed')" "1" + +# EVERY EXISTING CALLER. One red and no --target is the shape every merge in this +# tree uses today, and it must not have moved. +check "control: one red and no --target still merges" \ + "$(_led_rc merge --ledger "$_lw/m7.tsv" --date 2026-09-10 --mutation M "$_lw/red.log")" "0" +check "control: and still records the mutation against the check that reddened" \ + "$(awk -F'\t' '$3=="first check"{print $6}' "$_lw/m7.tsv")" "M" + # The three must be distinguishable from a REAL refusal, or fail-closed just # renames every outcome. check "a real refusal is a different status from an integrity failure" \