From b6c33fdbcd60df424581787429f694ed3eabbd5e Mon Sep 17 00:00:00 2001 From: "Joshua D. Drake" Date: Thu, 17 Sep 2026 20:49:05 -0600 Subject: [PATCH 1/2] test: the collation guard could not see a process-substituted sort (#1112) `test/selftest/070` required every `sort` feeding a `comm` to carry `LC_ALL=C`, and its pattern was a PIPE. A process-substituted sort is not a pipeline, so `run_all_versions.sh` used `comm` three times through substitutions, matched zero times, and read as compliant. One of the three had been there since #928. Found by @OffgridwithJD reviewing #1110. PINNING THE COMM IS NOT ENOUGH, and that is the half that changes the fix rather than its description. `LC_ALL=C comm <(sort a) <(sort b)` pins only comm's own comparison: the substitutions run in subshells of the PARENT and inherit its locale. A guard accepting `LC_ALL=C` anywhere on the line would bless exactly the form a reader writes after reading the guard's name, so the two halves are separate checks. AND IT READS CODE ONLY. The guard scanned every line, prose included, so a comment explaining the rule violated it: a note reading "reads only the piped form" contained the literal string it grepped for and flagged its own file. A rule that cannot be written down is a rule people stop writing down. Both corpus arms report ZERO on this tree, measured before the change, so the detector is proved by PLANTING: the substituted form, the piped form, a half-pinned line, a pinned comm over unpinned sorts, and the two forms that must not be flagged. The fifth matters as much as the first four, or the detector could be "flag everything" and every other arm still passes. Removal proof: reader reverts to the pipe-only pattern 3 arms red reader stops skipping comments 1 arm red, naming the file comm reader stops requiring a pin 2 arms red, naming three files control 1033 passed + 0 failed A FOURTH MUTATION DELETED A LINE OF MY OWN AND NOTHING CHANGED. The draft substituted the pinned form away before matching; removing that left nine arms green, so it was dead -- both patterns require `sort` immediately after the `|` or the `<(`, which makes a mixed line fail without it. Removed, with the reasoning recorded where the line was. Both harnesses, independent: part 070 and test/pytest/test_collation_pinned.py, which walks the same corpus in Python and additionally pins that `command` is not a `comm`. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01NhwXKAgSmYDUjteWkfajHK --- CHANGELOG.md | 36 ++++ test/pytest/TESTS.md | 40 +++++ test/pytest/expected_tests.txt | 22 ++- test/pytest/test_collation_pinned.py | 161 ++++++++++++++++++ test/pytest/test_harness_deps.py | 4 + .../070-and-comm-s-two-inputs-must.sh | 94 +++++++++- 6 files changed, 348 insertions(+), 9 deletions(-) create mode 100644 test/pytest/test_collation_pinned.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 0ab58d34..411fe535 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -69,6 +69,42 @@ true until the next version shipped. shows a generator read through a subshell losing the seed on this bash, another shows the assigning form keeping it. Three static arms pin `fuzz.sh`'s own form, and putting the printing generator back reddens two of them. +- The collation guard could not see a process-substituted sort (#1112). + + `comm` requires both inputs sorted in ITS collation and does not check. Fed a + mismatch it writes `input is not in sorted order` to stderr and prints a result + anyway, so where stderr lands in a log nobody reads, a wrong set arrives looking + like an answer. The inputs are not collation-insensitive: on real suite names + `pgc_setup`/`pg_dump_roundtrip` and `projections`/`projection_update` both swap + between `C` and `en_US.UTF-8`. + + `test/selftest/070` required every `sort` feeding a `comm` to carry `LC_ALL=C`, + and matched only the PIPED form. A process-substituted sort is not a pipeline, so + `run_all_versions.sh` used `comm` three times through substitutions, matched the + pattern zero times, and read as compliant. One of the three had been there since + #928. Found by OffgridwithJD reviewing #1110. + + PINNING THE COMM IS NOT ENOUGH, and that is the half that changes the fix rather + than its description. `LC_ALL=C comm <(sort a) <(sort b)` pins only comm's own + comparison: the substitutions run in subshells of the PARENT and inherit its + locale. A guard accepting `LC_ALL=C` anywhere on the line would bless exactly the + form a reader writes after reading the guard's name. The two halves are now + separate checks. + + AND IT READS CODE ONLY. The guard scanned every line, prose included, so a comment + explaining the rule violated it: a note reading "reads only the piped form" + contained the literal string it grepped for and flagged its own file. A rule that + cannot be written down is a rule people stop writing down. + + Both corpus arms report zero on this tree, measured before the change, so the + detector is proved by PLANTING rather than by the corpus: the substituted form, + the piped form, a half-pinned line, a pinned comm over unpinned sorts, and the two + forms that must not be flagged. + + Mutation testing deleted a line from the first draft and NOTHING changed, so it + was dead and is gone: both patterns require `sort` immediately after the `|` or + the `<(`, which makes a mixed line fail without the substitution step the draft + performed. - Every PR with a changelog entry conflicted with every other one (#996). diff --git a/test/pytest/TESTS.md b/test/pytest/TESTS.md index 51fd9627..ae5b454b 100644 --- a/test/pytest/TESTS.md +++ b/test/pytest/TESTS.md @@ -95,6 +95,7 @@ behaviour, the source of that number is named. - [47. test_index_fetch_penalty_crossover.py: the correlated range must not fetch](#47-test_index_fetch_penalty_crossoverpy-the-correlated-range-must-not-fetch) - [48. test_parallel_scan_cost.py: a parallel custom scan must not divide I/O](#48-test_parallel_scan_costpy-a-parallel-custom-scan-must-not-divide-io) - [49. test_residual_is_counted.py: a residual must be counted, not subtracted](#49-test_residual_is_countedpy-a-residual-must-be-counted-not-subtracted) +- [50. test_collation_pinned.py: comm's inputs must be sorted the same way](#50-test_collation_pinnedpy-comms-inputs-must-be-sorted-the-same-way) ## 1. How to read a test in here @@ -4524,3 +4525,42 @@ and neither file names the other. The load-bearing assertion is that the printed breakdown takes a count a count can take. It is unreachable while the residual is a subtraction across two populations, and reachable only once it is a set difference over the names. +## 50. test_collation_pinned.py: comm's inputs must be sorted the same way + +#552 established the rule and #1112 found the hole. `comm` requires both inputs +sorted in ITS collation and does not check: fed a mismatch it writes `input is not +in sorted order` to stderr and prints a result anyway, so where stderr lands in a +log nobody reads, a wrong set arrives looking like an answer. + +The inputs are not collation-insensitive. On real suite names, +`pgc_setup`/`pg_dump_roundtrip` and `projections`/`projection_update` both swap +between `C` and `en_US.UTF-8`. + +**Two halves, and the second is the one that was missed.** `LC_ALL=C comm <(sort a) +<(sort b)` pins only comm's own comparison: the substitutions run in subshells of +the **parent** and inherit its locale. A guard accepting `LC_ALL=C` anywhere on the +line would bless exactly the form a reader writes after reading the guard's name. + +Public seam: the shell corpus under `test/*.sh`. Read independently of +`test/selftest/070-and-comm-s-two-inputs-must.sh` -- same corpus, own +implementation, own planted probes, and neither file names the other. + +### Every arm + +| test | what it holds | +| --- | --- | +| `test_the_corpus_has_files_using_comm_so_the_sweep_is_not_vacuous` | the sweep spans more than one file, printed from the data | +| `test_every_sort_feeding_a_comm_pins_its_collation` | no suite using `comm` leaves a sort on the caller's locale | +| `test_every_comm_pins_its_own_comparison` | and the `comm` itself is pinned, which is a separate claim | +| `test_the_detector_catches_the_process_substituted_form` | the hole #1112 names: not a pipeline, so a pipe pattern cannot see it | +| `test_the_detector_still_catches_the_piped_form` | widening did not trade #552's case away | +| `test_a_line_with_one_of_two_sorts_pinned_is_caught` | the half-pinned form, which is what a partial fix produces | +| `test_pinning_only_the_comm_does_not_pin_its_substitutions` | a pinned `comm` over unpinned sorts is still a breach, while its comm half is satisfied | +| `test_a_fully_pinned_line_is_not_flagged` | without which the detector could be "flag everything" and every arm above still passes | +| `test_prose_describing_the_rule_does_not_violate_it` | a comment containing the forbidden form is not a breach -- it flagged `run_all_versions.sh` for its own text before the guard skipped comments | +| `test_an_unpinned_comm_is_caught_and_a_word_containing_comm_is_not` | `command` and an identifier containing `comm` are not comms | + +Both corpus arms report zero on this tree, measured before the file was written, so +the detector is proved by planting rather than by the corpus. The load-bearing arm +is the process-substituted form: unreachable by a pipe pattern, and reachable only +once the detector reads substitutions too. diff --git a/test/pytest/expected_tests.txt b/test/pytest/expected_tests.txt index 983a5189..dc671808 100644 --- a/test/pytest/expected_tests.txt +++ b/test/pytest/expected_tests.txt @@ -173,7 +173,27 @@ # 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 363 +# 355 -> 365 when test_collation_pinned.py landed (#1112): ten arms over the comm +# collation rule -- the corpus sweep in both halves, plus seven planted probes, because +# both corpus arms report ZERO on this tree and an arm that can only report "none" is a +# check that cannot fail. +# +# NOTE FOR WHOEVER MERGES SECOND. #1115 moves this same key to 361 from 355 for six arms +# of its own. Neither number survives and the deltas were measured against different +# trees. Re-derive by collection on the merged tree. +# MERGED with #1115's six arms. This branch derived 365 from a base of 355, and #1115 +# derived 361 from that same 355; neither is the merged truth. Both deltas happen to +# land on 371 from the other's base, which is exactly the coincidence this file warns +# about three times over -- they were measured against different trees and agreeing is +# not evidence. Re-derived by collection on the merged tree: `371 tests collected`. +# RESEATED onto main carrying #1113 and #1119. This branch derived 371 from a base of +# 361 and main now states 362; neither is the merged truth. Re-derived by collection on +# this tree: `372 tests collected`. +# RESEATED again, onto main carrying #1114 and #1120. Main states 363, this branch had +# 372; re-derived by collection on this tree: `373 tests collected`. This key has now +# been re-derived four times on one branch, each time against a different base, and the +# arithmetic would have been right by luck twice and wrong twice. +guard_tests 373 # 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_collation_pinned.py b/test/pytest/test_collation_pinned.py new file mode 100644 index 00000000..370f3067 --- /dev/null +++ b/test/pytest/test_collation_pinned.py @@ -0,0 +1,161 @@ +"""`comm`'s two inputs must be sorted in one collation, and the pin must reach them. + +#552 established the rule and #1112 found the hole. `comm` requires both inputs sorted +in ITS collation and does not check: fed a mismatch it writes `input is not in sorted +order` to stderr and prints a result anyway, so in a harness whose stderr lands in a log +nobody reads, a wrong set arrives looking like an answer. + +The inputs are not collation-insensitive. Measured on real suite names, +`pgc_setup`/`pg_dump_roundtrip` and `projections`/`projection_update` both swap between +`C` and `en_US.UTF-8`. + +TWO HALVES, AND THE SECOND IS THE ONE THAT WAS MISSED. `LC_ALL=C comm <(sort a) <(sort b)` +pins only comm's own comparison: the process substitutions run in subshells of the +PARENT and inherit ITS locale. A guard that accepted `LC_ALL=C` anywhere on the line +would bless exactly the form a reader writes after reading the guard's name. + +CODE LINES ONLY. The shell guard used to scan every line, prose included, so a comment +explaining the rule violated it -- a note reading "reads only the piped form" contained +the literal string the guard grepped for and flagged its own file. + +Read INDEPENDENTLY of `test/selftest/070-and-comm-s-two-inputs-must.sh`: same corpus, +own implementation, own planted probes, and neither file names the other. The shell part +is a set of `grep` pipelines over the same files; this walks them in Python. +""" + +import pathlib +import re + +REPO = pathlib.Path(__file__).resolve().parents[2] +SUITES = REPO / "test" + +# `sort` IMMEDIATELY after a pipe or an opening process substitution. A pinned form +# reads `| LC_ALL=C sort` or `<(LC_ALL=C sort`, which puts text between the two and so +# does not match -- that is what makes a MIXED line fail without further work, since an +# unpinned sort on the same line still matches. +_UNPINNED_SORT = re.compile(r"(\|\s*sort(\s|$)|<\(\s*sort(\s|$))") +_COMM = re.compile(r"(^|[^_\w])comm\s") +_PINNED_COMM = re.compile(r"LC_ALL=C\s+comm\s") + + +def _code_lines(text): + """Every line that is not a comment. Prose about the rule is not a breach of it.""" + return [l for l in text.splitlines() if not l.lstrip().startswith("#")] + + +def _unpinned_sorts(text): + return [l for l in _code_lines(text) if _UNPINNED_SORT.search(l)] + + +def _unpinned_comms(text): + return [l for l in _code_lines(text) + if _COMM.search(l) and not _PINNED_COMM.search(l)] + + +def _files_using_comm(): + out = [] + for p in sorted(SUITES.glob("*.sh")): + t = p.read_text() + if any(_COMM.search(l) for l in _code_lines(t)): + out.append(p) + return out + + +# ---- the corpus ------------------------------------------------------------ + + +def test_the_corpus_has_files_using_comm_so_the_sweep_is_not_vacuous(expect): + """The arms below report "none". Without this they report none of nothing.""" + names = [p.name for p in _files_using_comm()] + expect.at_least(len(names), 1, "at least one suite still uses comm") + # Printed from the data rather than retyped, so this cannot go stale. + expect.text("many" if len(names) >= 2 else "one", "many", + f"and more than one does, so the sweep spans files: {names}") + + +def test_every_sort_feeding_a_comm_pins_its_collation(expect): + offenders = {} + for p in _files_using_comm(): + bad = _unpinned_sorts(p.read_text()) + if bad: + offenders[p.name] = bad + expect.num(len(offenders), 0, + f"no suite using comm leaves a sort on the caller's locale: {offenders}") + + +def test_every_comm_pins_its_own_comparison(expect): + """Separate from the arm above, because the prefix does not reach the inputs. + + Both have to hold: `comm` compares in its own locale, and each substitution sorts + in the parent's. + """ + offenders = {} + for p in _files_using_comm(): + bad = _unpinned_comms(p.read_text()) + if bad: + offenders[p.name] = bad + expect.num(len(offenders), 0, + f"no comm is left on the caller's locale: {offenders}") + + +# ---- the detector, proved by planting -------------------------------------- +# +# Both corpus arms report zero today, measured before this file was written. An arm +# that can only ever report "none" is a check that cannot fail, so every form the +# detector must catch is planted, and the forms it must NOT catch are planted too. + + +def test_the_detector_catches_the_process_substituted_form(expect): + """The hole #1112 names: not a pipeline, so a pipe pattern cannot see it.""" + expect.num(len(_unpinned_sorts('\tcomm -23 <(sort "$1") <(sort "$2")\n')), 1, + "an unpinned process-substituted sort is caught") + + +def test_the_detector_still_catches_the_piped_form(expect): + """The case #552 already covered. Widening must not trade one for the other.""" + expect.num(len(_unpinned_sorts("\tcat a | sort > b\n")), 1, + "an unpinned piped sort is caught") + + +def test_a_line_with_one_of_two_sorts_pinned_is_caught(expect): + """The half-pinned form, which is what a reader writes after a partial fix.""" + expect.num(len(_unpinned_sorts('\tcomm -23 <(LC_ALL=C sort "$1") <(sort "$2")\n')), 1, + "pinning one of two sorts is not pinning the line") + + +def test_pinning_only_the_comm_does_not_pin_its_substitutions(expect): + """The trap inside the trap, and the reason the two arms are separate.""" + text = '\tLC_ALL=C comm -23 <(sort "$1") <(sort "$2")\n' + expect.num(len(_unpinned_sorts(text)), 1, + "a pinned comm over unpinned sorts is still a breach") + expect.num(len(_unpinned_comms(text)), 0, + "while the comm half of that same line is satisfied") + + +def test_a_fully_pinned_line_is_not_flagged(expect): + """Without this the detector could be 'flag everything' and every arm above passes.""" + text = '\tLC_ALL=C comm -23 <(LC_ALL=C sort "$1") <(LC_ALL=C sort "$2")\n' + expect.num(len(_unpinned_sorts(text)), 0, "a fully pinned line is clean") + expect.num(len(_unpinned_comms(text)), 0, "and so is its comm") + + +def test_prose_describing_the_rule_does_not_violate_it(expect): + """A rule that cannot be written down is a rule people stop writing down. + + This exact comment flagged `run_all_versions.sh` for its own text before the + shell guard learned to skip comments. + """ + text = "# the guard reads only the `| sort` form and misses <(sort ...)\n" + expect.num(len(_unpinned_sorts(text)), 0, + "a comment containing the forbidden form is not a breach") + expect.num(len(_unpinned_comms("# comm needs both inputs sorted the same way\n")), 0, + "and neither is a comment mentioning comm") + + +def test_an_unpinned_comm_is_caught_and_a_word_containing_comm_is_not(expect): + """`_COMM` must not fire on `_cm_unpinned_comms` or `command`.""" + expect.num(len(_unpinned_comms("\tcomm -23 a b\n")), 1, "a bare comm is caught") + expect.num(len(_unpinned_comms("\tcommand -v sort\n")), 0, + "and `command` is not a comm") + expect.num(len(_unpinned_comms("\t_my_comm_helper a b\n")), 0, + "nor is an identifier containing it") diff --git a/test/pytest/test_harness_deps.py b/test/pytest/test_harness_deps.py index 8fee9f47..ef7faf16 100644 --- a/test/pytest/test_harness_deps.py +++ b/test/pytest/test_harness_deps.py @@ -121,6 +121,10 @@ # cluster, no driver. The classifier agrees, and this declaration has to say so # before the guard job will collect it. "test_residual_is_counted.py", + # #1112's file. An AST-free text sweep over `test/*.sh` -- no connection, no + # cluster, no driver -- reading the same corpus the shell part reads, with its + # own implementation. + "test_collation_pinned.py", ] diff --git a/test/selftest/070-and-comm-s-two-inputs-must.sh b/test/selftest/070-and-comm-s-two-inputs-must.sh index 679ca8fd..52b89e4e 100644 --- a/test/selftest/070-and-comm-s-two-inputs-must.sh +++ b/test/selftest/070-and-comm-s-two-inputs-must.sh @@ -16,19 +16,97 @@ _cm_files="$(grep -ln 'comm -' "$PGC_TESTDIR"/*.sh 2>/dev/null)" check "premise: some suite still uses comm, or the check below is vacuous" \ "$([ -n "$_cm_files" ] && echo yes || echo no)" "yes" +# ---- widened to reach process substitution, and to read code only (#1112) --- +# +# THE OLD PATTERN WAS A PIPE. `grep -E '\|[[:space:]]*sort'` cannot match +# `comm -23 <(sort "$1") <(sort "$2")`, which is not a pipeline at all. Measured on +# `run_all_versions.sh`: zero matches for the piped form, three process-substituted +# sorts feeding `comm`, and this guard passed the file. One of the three had been +# there since #928. Found by @OffgridwithJD reviewing #1110. +# +# PINNING THE COMM IS NOT ENOUGH, and this is the half that changes the fix rather +# than the description. `LC_ALL=C comm <(sort a) <(sort b)` pins only comm's own +# comparison: the substitutions run in subshells of the PARENT and inherit ITS +# locale. A guard that accepted `LC_ALL=C` anywhere on the line would bless exactly +# the form a reader writes after reading this guard's name. So the two halves are +# asserted separately. +# +# AND IT READS CODE ONLY. The guard used to scan every line, prose included, so a +# comment explaining the rule violates it: a note in `run_all_versions.sh` reading +# "reads only the piped form" contained the literal string this greps for, on a line +# with no `LC_ALL=C`, and flagged the file for its own comment. A rule that cannot be +# written down is a rule people stop writing down. +# +# The readers are functions so the planted fixtures below can drive the same code the +# corpus arm uses. A check that re-implements its subject agrees with it by +# construction. + +# A MIXED LINE FAILS WITHOUT ANY EXTRA WORK, and this reader is deliberately shorter +# than the first draft. Both patterns require `sort` IMMEDIATELY after the `|` or the +# `<(`, so a pinned `<(LC_ALL=C sort ...)` never matches in the first place and an +# unpinned one on the same line still does. The draft also substituted the pinned form +# away before matching; mutation testing removed that line and NOTHING changed -- nine +# arms stayed green -- so it was dead, and a dead line in a guard reads as load-bearing +# to the next person. A line-level `grep -v LC_ALL=C` is the thing that would have +# needed it, and that is the design this does not use. +_cm_unpinned_sorts() { # _cm_unpinned_sorts FILE -> offending lines + grep -vE '^[[:space:]]*#' "$1" \ + | grep -E '(\|[[:space:]]*sort([[:space:]]|$)|<\([[:space:]]*sort([[:space:]]|$))' \ + || true +} + +_cm_unpinned_comms() { # _cm_unpinned_comms FILE -> offending lines + grep -vE '^[[:space:]]*#' "$1" \ + | grep -E '(^|[^_[:alnum:]])comm[[:space:]]' \ + | grep -v 'LC_ALL=C comm[[:space:]]' \ + || true +} + _cm_unpinned="" +_cm_unpinned_c="" for _f in $_cm_files; do - # every `| sort` in a file that uses comm must carry LC_ALL=C - # The second test is grep -c on a captured value, not a pipe into grep -qv; - # see selftest 080. The first reads a FILE and is not a pipeline at all. - _cm_sorts="$(grep -E '\|[[:space:]]*sort' "$_f" || true)" - if [ "$(grep -cE '\|[[:space:]]*sort' "$_f" || true)" -ne 0 ] \ - && [ "$(grep -cv 'LC_ALL=C' <<<"$_cm_sorts" || true)" -ne 0 ]; then - _cm_unpinned="$_cm_unpinned $(basename "$_f")" - fi + [ -n "$(_cm_unpinned_sorts "$_f")" ] && _cm_unpinned="$_cm_unpinned $(basename "$_f")" + [ -n "$(_cm_unpinned_comms "$_f")" ] && _cm_unpinned_c="$_cm_unpinned_c $(basename "$_f")" done check "a file that uses comm pins the collation of every sort feeding it" \ "$(printf '%s' "$_cm_unpinned" | sed 's/^ //')" "" +check "and pins the comm itself, whose prefix does not reach its substitutions" \ + "$(printf '%s' "$_cm_unpinned_c" | sed 's/^ //')" "" + +# ---- the arms above are EMPTY on this corpus, so they are proved by planting ---- +# +# Both readers select nothing today, measured before this was written. An arm that +# can only ever report "none" is a check that cannot fail, so each form it must catch +# is planted here and required to be caught, and the two forms it must NOT catch are +# planted too. +_cm_d="$(mktemp -d)" + +printf 'x() {\n\tcomm -23 <(sort "$1") <(sort "$2")\n}\n' >"$_cm_d/sub.sh" +printf 'x() {\n\tcat a | sort > b\n\tcomm -23 b c\n}\n' >"$_cm_d/pipe.sh" +printf 'x() {\n\tcomm -23 <(LC_ALL=C sort "$1") <(sort "$2")\n}\n' >"$_cm_d/half.sh" +printf 'x() {\n\tLC_ALL=C comm -23 <(sort "$1") <(sort "$2")\n}\n' >"$_cm_d/commonly.sh" +printf 'x() {\n\tLC_ALL=C comm -23 <(LC_ALL=C sort "$1") <(LC_ALL=C sort "$2")\n}\n' >"$_cm_d/good.sh" +printf '# this note explains that a bare `| sort` feeding comm is refused\nx() {\n\tLC_ALL=C comm -23 <(LC_ALL=C sort "$1") <(LC_ALL=C sort "$2")\n}\n' >"$_cm_d/prose.sh" + +check "a process-substituted sort with no pin is caught, which the old pattern missed" \ + "$(_cm_unpinned_sorts "$_cm_d/sub.sh" | grep -c . || true)" "1" +check "and the piped form the old pattern did catch is still caught" \ + "$(_cm_unpinned_sorts "$_cm_d/pipe.sh" | grep -c . || true)" "1" +check "a line with ONE of two sorts pinned is still caught" \ + "$(_cm_unpinned_sorts "$_cm_d/half.sh" | grep -c . || true)" "1" +check "pinning only the comm does not pin its substitutions" \ + "$(_cm_unpinned_sorts "$_cm_d/commonly.sh" | grep -c . || true)" "1" +check "and a fully pinned line is not flagged, so the reader can report none" \ + "$(_cm_unpinned_sorts "$_cm_d/good.sh" | grep -c . || true)" "0" +check "prose describing the rule does not violate it" \ + "$(_cm_unpinned_sorts "$_cm_d/prose.sh" | grep -c . || true)" "0" + +check "an unpinned comm is caught by its own reader" \ + "$(_cm_unpinned_comms "$_cm_d/sub.sh" | grep -c . || true)" "1" +check "and a pinned comm is not" \ + "$(_cm_unpinned_comms "$_cm_d/good.sh" | grep -c . || true)" "0" + +rm -rf "$_cm_d" # A case over the cached list rather than `listed_suites | grep -qx`. The pipe # was the defect: grep -q returns on its match, printf takes EPIPE, and pipefail From 242376a6efab8a56a3ed54f3d62c9f9f730d0eb0 Mon Sep 17 00:00:00 2001 From: "Joshua D. Drake" Date: Thu, 17 Sep 2026 20:50:15 -0600 Subject: [PATCH 2/2] test: ledger rows for the nine checks part 070 adds (#1112) Derived from five majors on one frozen snapshot, all uniform 15;16;17;18;19: PG15..PG19 rc=0 records=1017 namehash=8c2793ef607b verdicthash=402803ad11ec each its own major checks_never_observed_red re-derived 1325 -> 1334 by the budget file's own command and by the gate's census in the same run, which agreed. suites_not_covered unchanged at 249. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01NhwXKAgSmYDUjteWkfajHK --- test/check_ledger.tsv | 9 +++++++++ test/check_ledger_budget.txt | 13 ++++++++++++- 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/test/check_ledger.tsv b/test/check_ledger.tsv index 1c3d56cc..0f1257d5 100644 --- a/test/check_ledger.tsv +++ b/test/check_ledger.tsv @@ -221,12 +221,21 @@ harness_selftest 050-the-list-must-be-read-the premise: the fixture really does harness_selftest 060-the-list-stays-sorted-which-is premise: C collation puts sort_status before sorted_projection 15;16;17;18;19 never - harness_selftest 060-the-list-stays-sorted-which-is the suite list is sorted in C order, so two new suites land in different places 15;16;17;18;19 never - harness_selftest 070-and-comm-s-two-inputs-must a file that uses comm pins the collation of every sort feeding it 15;16;17;18;19 never - +harness_selftest 070-and-comm-s-two-inputs-must a line with ONE of two sorts pinned is still caught 15;16;17;18;19 never - +harness_selftest 070-and-comm-s-two-inputs-must a process-substituted sort with no pin is caught, which the old pattern missed 15;16;17;18;19 never - +harness_selftest 070-and-comm-s-two-inputs-must an unpinned comm is caught by its own reader 15;16;17;18;19 never - +harness_selftest 070-and-comm-s-two-inputs-must and a fully pinned line is not flagged, so the reader can report none 15;16;17;18;19 never - +harness_selftest 070-and-comm-s-two-inputs-must and a pinned comm is not 15;16;17;18;19 never - harness_selftest 070-and-comm-s-two-inputs-must and a prefix of a registered name is not treated as registered 15;16;17;18;19 never - +harness_selftest 070-and-comm-s-two-inputs-must and pins the comm itself, whose prefix does not reach its substitutions 15;16;17;18;19 never - +harness_selftest 070-and-comm-s-two-inputs-must and the piped form the old pattern did catch is still caught 15;16;17;18;19 never - harness_selftest 070-and-comm-s-two-inputs-must every registered suite has a file 15;16;17;18;19 never - harness_selftest 070-and-comm-s-two-inputs-must every suite is registered in run_all_versions.sh 15;16;17;18;19 never - harness_selftest 070-and-comm-s-two-inputs-must negative control: and does not find one that is not 15;16;17;18;19 never - +harness_selftest 070-and-comm-s-two-inputs-must pinning only the comm does not pin its substitutions 15;16;17;18;19 never - harness_selftest 070-and-comm-s-two-inputs-must positive control: the membership test finds a name that is registered 15;16;17;18;19 never - harness_selftest 070-and-comm-s-two-inputs-must premise: some suite still uses comm, or the check below is vacuous 15;16;17;18;19 never - +harness_selftest 070-and-comm-s-two-inputs-must prose describing the rule does not violate it 15;16;17;18;19 never - harness_selftest 080-no-suite-pipes-a-captured-string a comment ending in a pipe is not a producer 15;16;17;18;19 never - harness_selftest 080-no-suite-pipes-a-captured-string a comment naming a TERMINATED tag still opens no heredoc, so the lines below it are seen 15;16;17;18;19 never - harness_selftest 080-no-suite-pipes-a-captured-string a comment naming a heredoc exempts nothing, so the line below it is still seen 15;16;17;18;19 never - diff --git a/test/check_ledger_budget.txt b/test/check_ledger_budget.txt index 2e99987c..e0d8c168 100644 --- a/test/check_ledger_budget.txt +++ b/test/check_ledger_budget.txt @@ -78,4 +78,15 @@ 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 1340 +# 1325 -> 1334: nine arms in selftest part 070, which learned to see a +# process-substituted sort and to read code rather than prose (#1112). Derived by the +# command above on this tree, and by the gate's census in the same run. +# RESEATED onto main carrying #1113 and #1119. Main states 1330, this branch stated +# 1334, and the merged tree is neither. The ledger auto-merged SILENTLY here while the +# budget conflicted loudly, which is the usual pairing and the reason the union was +# checked by KEY rather than trusted: 0 main keys lost, 9 added, all in part 070. +# Re-derived by the command above on the merged tree. +# RESEATED onto main carrying #1114 and #1120. Re-derived by the command above on +# the merged tree; the ledger auto-merged silently again and was checked by KEY: +# 0 main keys lost, 9 added, all in part 070. +checks_never_observed_red 1349