From 877e09e0159ae7f562f7b2fafb0c94aff80d0562 Mon Sep 17 00:00:00 2001 From: cluade-reviewer-andresmgsl Date: Wed, 5 Aug 2026 14:27:40 +0000 Subject: [PATCH 1/4] fix(issueflow): the board discriminator is .pull_request == null, not has() (#210) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit issueflow-reconcile has been blind on this forge since the 0.6.0 merge landed. Run 368 — #198's own post-merge acceptance probe — printed: issueflow: no open issues. issueflow: reconciled. over a board of nine. Every Forgejo entry CARRIES the `pull_request` key, valued null on an issue, so `select(has("pull_request") | not)` selects zero rows. Measured again today: #209 (an issue) has the key valued null; #208 and #207 (PRs) have it valued as objects. This is mine. #188 fixed exactly this and the file's own comment at :1113 states the rule, with :1121 already using it correctly. Resolving hunk 4 of the merge I took upstream's board block wholesale and carried the wrong discriminator into three sites — the gather, the release-body gather, and reconcile_issue_pass — in the PR whose stated purpose was to stop blind sweeps reporting success. Cost while it stood: no issue transitions, no claim reclaims, no nudges, no board flags — and no `post-merge` transitions, which is why #192 and #198 both still read `claimed` after their PRs merged, and why #198's own closure criterion could not complete. Two guards, because a comment did not hold: * A GATHER-LEVEL CASE against a Forgejo-shaped fixture — every entry carrying the key. The existing discriminator cases assert jq expressions in isolation and passed throughout this regression; they never ran the gather that uses them, which is precisely how it survived review. * A SOURCE PIN forbidding has("pull_request") on this surface, so a future sync cannot reintroduce it 40 lines below the comment forbidding it. Reverting the board gather reds both. Reverting reconcile_issue_pass reds the pin. test/run.sh 28/28 under jq 1.7 and jq 1.6; issueflow 503/503; shellcheck 0.10.0 and actionlint clean. Refs #210 --- .../issueflow-reconcile.sh | 10 ++-- changelog.d/210.md | 22 ++++++++ test/issueflow-reconcile.test.sh | 52 +++++++++++++++++++ 3 files changed, 81 insertions(+), 3 deletions(-) create mode 100644 changelog.d/210.md diff --git a/actions/issueflow-reconcile/issueflow-reconcile.sh b/actions/issueflow-reconcile/issueflow-reconcile.sh index fbe3d16..f5386b5 100644 --- a/actions/issueflow-reconcile/issueflow-reconcile.sh +++ b/actions/issueflow-reconcile/issueflow-reconcile.sh @@ -1159,7 +1159,10 @@ reconcile_issue_pass() { # $1 = issue — one issue's whole pass, in its own sub || skip_issue "$n" "could not read the issue: $(read_failure_reason "$READ_FAILURE_STDERR")" issue_payload_valid "$n" <<<"$ISSUE_JSON" \ || skip_issue "$n" "the issue read answered a payload that is not issue #$n carrying a label array" - jq -e 'has("pull_request") | not' <<<"$ISSUE_JSON" >/dev/null || exit 0 + # `.pull_request == null`, never `has("pull_request") | not` (#188, #210): + # every Forgejo entry CARRIES the key, valued null on an issue, so the + # has() form selects zero rows here — silently, forever. + jq -e '.pull_request == null' <<<"$ISSUE_JSON" >/dev/null || exit 0 ISSUE_LABELS="$(jq -r '.labels[].name' <<<"$ISSUE_JSON")" reconcile_issue "$n" || exit $? commit_staged_effects @@ -1273,7 +1276,8 @@ main() { log "could not read the issue board: $(read_failure_reason "$READ_FAILURE_STDERR")" return 1 fi - BOARD_RECORDS="$(jq -r '.[] | select(has("pull_request") | not) + # `.pull_request == null`, never `has("pull_request") | not` (#188, #210). + BOARD_RECORDS="$(jq -r '.[] | select(.pull_request == null) | [(.number | tostring), ((.labels // []) | map(.name) | join(",")), (.title // "")] | @tsv' \ <<<"$board_json")" @@ -1284,7 +1288,7 @@ main() { # is exactly the emptied gate the release's own `blocked` -> `ready` # promotion answers, which is why a `ready` release leaves the flag # dormant rather than flagging the whole board. - release_bodies="$(jq -r '.[] | select(has("pull_request") | not) + release_bodies="$(jq -r '.[] | select(.pull_request == null) | select((.labels // []) | map(.name) | index("release")) | [(.number | tostring), ((.body // "") | gsub("[\t\r\n]"; " "))] | @tsv' \ <<<"$board_json")" diff --git a/changelog.d/210.md b/changelog.d/210.md new file mode 100644 index 0000000..afd7927 --- /dev/null +++ b/changelog.d/210.md @@ -0,0 +1,22 @@ +### Fixed + +- `issueflow-reconcile` sees this forge's issues again. The board gather used + `has("pull_request")`, and every Forgejo entry carries that key — so it + selected zero rows on every sweep while printing `reconciled.` (#210). + +- Three sites take `.pull_request == null`, the discriminator the file's own + comment already specified and that one of its four call sites already used + (#210). + +- `post-merge` transitions can fire again: they could not, because the sweep + saw no issues to transition (#210). + +### Added + +- A gather-level case drives the real board read against a Forgejo-shaped + fixture — every entry carrying the key. The existing discriminator cases + assert `jq` expressions in isolation and passed throughout this regression + (#210). + +- A source pin forbids `has("pull_request")` on this surface, because the rule + was stated in a comment and violated forty lines below it (#210). diff --git a/test/issueflow-reconcile.test.sh b/test/issueflow-reconcile.test.sh index c115695..512658d 100644 --- a/test/issueflow-reconcile.test.sh +++ b/test/issueflow-reconcile.test.sh @@ -1779,6 +1779,58 @@ check "a PR reads as a PR on either shape" 0 "pr" \ check "the old has() test misreads a forgejo issue as a PR" 0 "pr" \ bash -c 'echo "{\"number\":1,\"pull_request\":null}" | jq -e "has(\"pull_request\") | not" >/dev/null && echo issue || echo pr' +# ...AND THE GATHER, because the rows above assert jq expressions in isolation +# and passed all the way through #210 — a sweep that saw zero issues on every +# pass and printed `reconciled.` The 0.6.0 merge reintroduced the has() form in +# the board gather; these cases could not see it because they never ran it. +# +# The fixture is FORGEJO-SHAPED: every entry carries `pull_request`, valued +# null on an issue and an object on a PR. On a GitHub-shaped board (key absent +# on issues) both discriminators agree, which is why this needs its own board. +FORGEJO_BOARD="$TMP/forgejo-board" +mkdir -p "$FORGEJO_BOARD" +cp "$ARRIVAL/labels.conf" "$FORGEJO_BOARD/labels.conf" 2>/dev/null || true +printf '[]\n' >"$FORGEJO_BOARD/repos_owner_repo_pulls_state_open.json" +printf '[]\n' >"$FORGEJO_BOARD/repos_owner_repo_pulls_state_closed.json" +printf '%s\n' \ + '[{"number":60,"pull_request":null,"labels":[{"name":"ready"}],"title":"an issue"}, + {"number":61,"pull_request":{"merged":false},"labels":[],"title":"a pull request"}]' \ + >"$FORGEJO_BOARD/repos_owner_repo_issues_state_open.json" +jq -n --arg at "$(iso_at "$INOW")" \ + '{number:60,user:{login:"triage-one"},created_at:$at,body:"",pull_request:null, + labels:[{name:"ready"}],assignees:[]}' \ + >"$FORGEJO_BOARD/repos_owner_repo_issues_60.json" +printf '[]\n' >"$FORGEJO_BOARD/repos_owner_repo_issues_60_comments.json" +forgejo_board_run() { + : >"$FORGEJO_BOARD/edits" + env PATH="$ARRIVAL/stub:$PATH" CEREMONY_FORGE=github GH_FIXTURES="$FORGEJO_BOARD" \ + ISSUEFLOW_NOW="$INOW" REPO=owner/repo LABELS_CONF="$ARRIVAL/labels.conf" \ + bash "$ROOT/actions/issueflow-reconcile/issueflow-reconcile.sh" 2>&1 +} +fjb_out="$(forgejo_board_run)" +check "a forgejo-shaped board is NOT read as empty" 1 "" \ + grep -qF 'issueflow: no open issues.' <<<"$fjb_out" +check "...the sweep completes over it" 0 "" \ + grep -qF 'issueflow: reconciled.' <<<"$fjb_out" +check "...and the PR row is still excluded from the issue set" 1 "" \ + grep -qE '^issueflow: #61' <<<"$fjb_out" + +# -- the rule is pinned at the source, because a comment did not hold -------- +# `.pull_request == null` is stated in this file's own header AND at +# issueflow-reconcile.sh:1113 — and the merge put `has("pull_request")` back 40 +# lines below that comment, in three places. Prose is not a guard (#210). +# In-process, not `bash -c`: a subshell cannot see this file's functions, and a +# pin that silently inspected nothing would be the same defect one level up. +no_has_pull_request() { + local hits + hits="$(sed 's/[[:space:]]#.*$//; s/^[[:space:]]*#.*$//' \ + "$ROOT/actions/issueflow-reconcile/issueflow-reconcile.sh" \ + | grep -n 'has("pull_request")')" + [ -z "$hits" ] || { printf 'executable has("pull_request") at:\n%s\n' "$hits" >&2; return 1; } +} +check "no executable has(\"pull_request\") survives on this surface" 0 "" \ + no_has_pull_request + # -- the OPEN-pull gather, at main() granularity ---------------------------- # The closed/merged half above proves one REST path; this proves the other, # which is a DIFFERENT pipeline: `.body | @base64` -> base64 -d -> From bada4ffff593625b36ff8bf173cc7b4f93649237 Mon Sep 17 00:00:00 2001 From: cluade-reviewer-andresmgsl Date: Wed, 5 Aug 2026 14:37:37 +0000 Subject: [PATCH 2/4] test(issueflow): each of the three sites is caught by behaviour, not only by the pin (#210) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit @codex-reviewer-andresmgsl's two scope items, applied before the first review round rather than after. 1. THE GUARD IS COMMENT-AWARE, WITH CONTROLS. It already stripped comments — it has to, because the #188 warning that explains why has("pull_request") is wrong contains the string. Without controls that was an untested property, and the pressure it creates is real: a raw grep would push a builder into deleting the very warning that prevents recurrence. Two fixtures now prove it: the explanatory comment is allowed, an executable jq filter is rejected. 2. ALL THREE SITES ARE DRIVEN BY BEHAVIOUR. The pin makes any revert red, but a pin proves a string is absent, not that each replacement means the intended thing: BOARD_RECORDS the forgejo-shaped board is not read as empty release_bodies an open `release` issue whose gate holds an open member makes a claimable NON-member draw a window flag — empty carriers, no flag, so the row discriminates the site instead of merely reaching it reconcile_issue_pass the scalar payload: key-present-null is an issue, object-valued is a PR, key-absent is still an issue The release_bodies row did NOT discriminate on its first write — it asserted an issue number that BOARD_RECORDS also produces, so reverting the site left it green. Caught by mutating each site separately rather than trusting the suite total. Mutation, per site: BOARD_RECORDS 3 red, release_bodies 2 red, reconcile_issue_pass 2 red. test/run.sh 28/28; issueflow 510/510; shellcheck 0.10.0 clean. Refs #210 --- changelog.d/210.md | 7 +++- test/issueflow-reconcile.test.sh | 58 ++++++++++++++++++++++++++++++++ 2 files changed, 64 insertions(+), 1 deletion(-) diff --git a/changelog.d/210.md b/changelog.d/210.md index afd7927..ed46cf2 100644 --- a/changelog.d/210.md +++ b/changelog.d/210.md @@ -19,4 +19,9 @@ (#210). - A source pin forbids `has("pull_request")` on this surface, because the rule - was stated in a comment and violated forty lines below it (#210). + was stated in a comment and violated forty lines below it. It strips comments, + so the #188 warning that explains the trap is allowed to stay (#210). + +- All three sites are covered behaviourally, not only by the pin: the board + gather, the release-body gather through an observable window flag, and the + per-issue payload check (#210). diff --git a/test/issueflow-reconcile.test.sh b/test/issueflow-reconcile.test.sh index 512658d..8dcca61 100644 --- a/test/issueflow-reconcile.test.sh +++ b/test/issueflow-reconcile.test.sh @@ -1815,6 +1815,47 @@ check "...the sweep completes over it" 0 "" \ check "...and the PR row is still excluded from the issue set" 1 "" \ grep -qE '^issueflow: #61' <<<"$fjb_out" +# release_bodies is the THIRD producer and has its own has() site. A `release` +# issue on a forgejo-shaped board must reach the window gather, or the #292 +# flags are decided over an empty set (@codex-reviewer-andresmgsl, #210). +printf '%s\n' \ + '[{"number":60,"pull_request":null,"labels":[{"name":"ready"}],"title":"an issue"}, + {"number":62,"pull_request":null,"labels":[{"name":"release"}],"title":"Release 9.9.9","body":"Blocked by #60."}, + {"number":63,"pull_request":null,"labels":[{"name":"ready"}],"title":"a claimable non-member"}, + {"number":61,"pull_request":{"merged":false},"labels":[],"title":"a pull request"}]' \ + >"$FORGEJO_BOARD/repos_owner_repo_issues_state_open.json" +jq -n --arg at "$(iso_at "$INOW")" \ + '{number:63,user:{login:"triage-one"},created_at:$at,body:"",pull_request:null, + labels:[{name:"ready"}],assignees:[]}' \ + >"$FORGEJO_BOARD/repos_owner_repo_issues_63.json" +printf '[]\n' >"$FORGEJO_BOARD/repos_owner_repo_issues_63_comments.json" +jq -n --arg at "$(iso_at "$INOW")" \ + '{number:62,user:{login:"triage-one"},created_at:$at,body:"Blocked by #60.",pull_request:null, + labels:[{name:"release"}],assignees:[]}' \ + >"$FORGEJO_BOARD/repos_owner_repo_issues_62.json" +printf '[]\n' >"$FORGEJO_BOARD/repos_owner_repo_issues_62_comments.json" +fjb2_out="$(forgejo_board_run)" +# The observable effect of release_bodies being NON-empty: an open `release` +# issue whose gate still holds an open member makes every claimable non-member +# draw a window flag. With that gather empty there are no carriers and no flag, +# so this row discriminates the site rather than merely reaching it. +check "a release issue on a forgejo-shaped board reaches the window gather" 0 "" \ + grep -qE '#63: window flag' <<<"$fjb2_out" +check "...and the sweep still completes" 0 "" \ + grep -qF 'issueflow: reconciled.' <<<"$fjb2_out" + +# The THIRD site is reconcile_issue_pass's per-issue payload check. Same key, +# scalar rather than a list: null means issue, an object means PR. +pass_disc() { # $1 = the payload -> the exit status the guard would take + jq -e '.pull_request == null' <<<"$1" >/dev/null && echo issue || echo pr +} +check "a forgejo issue payload (key present, null) reads as an issue" 0 "issue" \ + pass_disc '{"number":60,"pull_request":null}' +check "...and an object-valued one reads as a PR" 0 "pr" \ + pass_disc '{"number":61,"pull_request":{"merged":false}}' +check "...and a github-shaped payload (key absent) still reads as an issue" 0 "issue" \ + pass_disc '{"number":60}' + # -- the rule is pinned at the source, because a comment did not hold -------- # `.pull_request == null` is stated in this file's own header AND at # issueflow-reconcile.sh:1113 — and the merge put `has("pull_request")` back 40 @@ -1830,6 +1871,23 @@ no_has_pull_request() { } check "no executable has(\"pull_request\") survives on this surface" 0 "" \ no_has_pull_request +# The controls, because this guard MUST tolerate the #188 comment that explains +# why the form is wrong — a raw grep would either fail forever or pressure a +# builder into deleting the very warning that prevents recurrence +# (@codex-reviewer-andresmgsl, #210 review). +DTMP="$(mktemp -d)"; trap 'rm -rf "$DTMP"' EXIT +strip_and_find() { # $1 = file -> 0 when an EXECUTABLE use survives + sed 's/[[:space:]]#.*$//; s/^[[:space:]]*#.*$//' "$1" | grep -q 'has("pull_request")' +} +# shellcheck disable=SC2016 # fixture CONTENT: the literal text a scanned file would hold +printf '%s\n' '#!/usr/bin/env bash' \ + '# `.pull_request == null`, NOT has("pull_request") | not (#188)' \ + 'jq -e ".pull_request == null" <<<"$J"' >"$DTMP/prose.sh" +check "the explanatory #188 comment is allowed" 1 "" strip_and_find "$DTMP/prose.sh" +# single-quoted so the fixture holds the LITERAL form the guard looks for +printf '%s\n' '#!/usr/bin/env bash' \ + "jq -r '.[] | select(has(\"pull_request\") | not)' <<<\"\$J\"" >"$DTMP/exec.sh" +check "...while an executable jq filter is rejected" 0 "" strip_and_find "$DTMP/exec.sh" # -- the OPEN-pull gather, at main() granularity ---------------------------- # The closed/merged half above proves one REST path; this proves the other, From 087ea4a24bd5d948bf91f21cb8ea7e5f680ccb87 Mon Sep 17 00:00:00 2001 From: cluade-reviewer-andresmgsl Date: Wed, 5 Aug 2026 14:44:31 +0000 Subject: [PATCH 3/4] test(issueflow): each site observable through the real path, not through the expression it contains (#210) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit @codex-reviewer-andresmgsl's three, and items 1 and 2 were still open after bada4ff — his review predates that push, but only item 3 (release bodies) was actually answered by it. TRAVERSAL, not a non-empty gather. The board case asserted the board was not read as empty; #60 was `ready` and therefore produced no observable effect, so nothing proved reconcile_issue_pass had run over it. #60 now carries NO queue state, so traversal has a deterministic outcome — needs-triage is minted and logged — and the row asserts that. THE SCALAR SITE, through the real path. My first attempt asserted the jq expression the function contains, which is exactly the shape that let this regression through: the isolated discriminator rows passed the whole time the gather was blind. A hand-wired probe around reconcile_issue_pass needed so much internal setup that it would have been testing my scaffolding, so the same board harness drives it with one row flipped — object-valued must NOT be reconciled as an issue, and the sweep must then correctly report the board as empty OF ISSUES. Per-site mutation, all three now behavioural rather than pin-only: revert BOARD_RECORDS -> 4 red revert release_bodies -> 2 red revert reconcile_issue_pass -> 3 red test/run.sh 28/28; issueflow 510/510; shellcheck 0.10.0 clean. Refs #210 --- test/issueflow-reconcile.test.sh | 38 ++++++++++++++++++++++---------- 1 file changed, 26 insertions(+), 12 deletions(-) diff --git a/test/issueflow-reconcile.test.sh b/test/issueflow-reconcile.test.sh index 8dcca61..f31b58d 100644 --- a/test/issueflow-reconcile.test.sh +++ b/test/issueflow-reconcile.test.sh @@ -1793,12 +1793,12 @@ cp "$ARRIVAL/labels.conf" "$FORGEJO_BOARD/labels.conf" 2>/dev/null || true printf '[]\n' >"$FORGEJO_BOARD/repos_owner_repo_pulls_state_open.json" printf '[]\n' >"$FORGEJO_BOARD/repos_owner_repo_pulls_state_closed.json" printf '%s\n' \ - '[{"number":60,"pull_request":null,"labels":[{"name":"ready"}],"title":"an issue"}, + '[{"number":60,"pull_request":null,"labels":[],"title":"an issue with no queue state"}, {"number":61,"pull_request":{"merged":false},"labels":[],"title":"a pull request"}]' \ >"$FORGEJO_BOARD/repos_owner_repo_issues_state_open.json" jq -n --arg at "$(iso_at "$INOW")" \ '{number:60,user:{login:"triage-one"},created_at:$at,body:"",pull_request:null, - labels:[{name:"ready"}],assignees:[]}' \ + labels:[],assignees:[]}' \ >"$FORGEJO_BOARD/repos_owner_repo_issues_60.json" printf '[]\n' >"$FORGEJO_BOARD/repos_owner_repo_issues_60_comments.json" forgejo_board_run() { @@ -1812,7 +1812,13 @@ check "a forgejo-shaped board is NOT read as empty" 1 "" \ grep -qF 'issueflow: no open issues.' <<<"$fjb_out" check "...the sweep completes over it" 0 "" \ grep -qF 'issueflow: reconciled.' <<<"$fjb_out" -check "...and the PR row is still excluded from the issue set" 1 "" \ +# TRAVERSAL, not merely a non-empty gather: the null-valued row has a +# deterministic outcome — no queue state means needs-triage is minted — so this +# proves reconcile_issue_pass actually ran over it, which "the board is not +# empty" does not (@codex-reviewer-andresmgsl, #210 review). +check "...the null-valued row is TRAVERSED, with an observable outcome" 0 "" \ + grep -qE '^issueflow: #60: needs-triage' <<<"$fjb_out" +check "...and the object-valued PR row is not reconciled as an issue" 1 "" \ grep -qE '^issueflow: #61' <<<"$fjb_out" # release_bodies is the THIRD producer and has its own has() site. A `release` @@ -1846,15 +1852,23 @@ check "...and the sweep still completes" 0 "" \ # The THIRD site is reconcile_issue_pass's per-issue payload check. Same key, # scalar rather than a list: null means issue, an object means PR. -pass_disc() { # $1 = the payload -> the exit status the guard would take - jq -e '.pull_request == null' <<<"$1" >/dev/null && echo issue || echo pr -} -check "a forgejo issue payload (key present, null) reads as an issue" 0 "issue" \ - pass_disc '{"number":60,"pull_request":null}' -check "...and an object-valued one reads as a PR" 0 "pr" \ - pass_disc '{"number":61,"pull_request":{"merged":false}}' -check "...and a github-shaped payload (key absent) still reads as an issue" 0 "issue" \ - pass_disc '{"number":60}' +# The scalar site, driven through the REAL path rather than through the jq +# expression it contains — asserting the expression in isolation is what let +# the regression through (@codex-reviewer-andresmgsl, #210 review). Same board +# harness, one row flipped: null-valued reconciles, object-valued stands down. +printf '%s\n' \ + '[{"number":64,"pull_request":{"merged":false},"labels":[],"title":"a PR wearing an issue row"}]' \ + >"$FORGEJO_BOARD/repos_owner_repo_issues_state_open.json" +jq -n --arg at "$(iso_at "$INOW")" \ + '{number:64,user:{login:"triage-one"},created_at:$at,body:"", + pull_request:{merged:false},labels:[],assignees:[]}' \ + >"$FORGEJO_BOARD/repos_owner_repo_issues_64.json" +printf '[]\n' >"$FORGEJO_BOARD/repos_owner_repo_issues_64_comments.json" +fjb3_out="$(forgejo_board_run)" +check "an object-valued row is not reconciled as an issue by the pass" 1 "" \ + grep -qE '^issueflow: #64' <<<"$fjb3_out" +check "...and the sweep reports the board as empty of ISSUES, correctly" 0 "" \ + grep -qF 'issueflow: no open issues.' <<<"$fjb3_out" # -- the rule is pinned at the source, because a comment did not hold -------- # `.pull_request == null` is stated in this file's own header AND at From 5b78d292015256ae2c19810914cf170c38ed9b6c Mon Sep 17 00:00:00 2001 From: cluade-reviewer-andresmgsl Date: Wed, 5 Aug 2026 14:49:01 +0000 Subject: [PATCH 4/4] =?UTF-8?q?test(issueflow):=20isolate=20the=20scalar?= =?UTF-8?q?=20guard=20=E2=80=94=20the=20list=20row=20admits,=20the=20paylo?= =?UTF-8?q?ad=20stands=20down=20(#210)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit @codex-reviewer-andresmgsl found the subtlety my board fixture could not reach: BOARD_RECORDS filters an object-valued row out of the LIST before the per-issue guard ever sees it, so no board fixture alone can prove the scalar stand-down. My #64 case proved the gather excluded it, not that reconcile_issue_pass did. The fixture that isolates the site is a deliberate mismatch: the LIST row is null-valued, so the board gather admits #65 — and the INDIVIDUAL payload the sweep then fetches is object-valued. Only reconcile_issue_pass's own guard can stand that down. Three rows over the same number, so the guard cannot pass by standing everything down or by admitting everything: payload object-valued -> NOT reconciled payload null-valued -> reconciled payload key absent -> reconciled (the GitHub shape) Mutating ONLY the scalar predicate now reds three BEHAVIOURAL rows plus the pin, where before it red only the pin and a neighbour. pass_disc is gone: it repeated the predicate inside the test helper and never called production — which is the same isolated-expression trap, one layer down, in the fix for it. issueflow 512/512; test/run.sh 28/28; shellcheck 0.10.0 clean. Refs #210 --- test/issueflow-reconcile.test.sh | 45 +++++++++++++++++++++++--------- 1 file changed, 33 insertions(+), 12 deletions(-) diff --git a/test/issueflow-reconcile.test.sh b/test/issueflow-reconcile.test.sh index f31b58d..918b635 100644 --- a/test/issueflow-reconcile.test.sh +++ b/test/issueflow-reconcile.test.sh @@ -1852,23 +1852,44 @@ check "...and the sweep still completes" 0 "" \ # The THIRD site is reconcile_issue_pass's per-issue payload check. Same key, # scalar rather than a list: null means issue, an object means PR. -# The scalar site, driven through the REAL path rather than through the jq -# expression it contains — asserting the expression in isolation is what let -# the regression through (@codex-reviewer-andresmgsl, #210 review). Same board -# harness, one row flipped: null-valued reconciles, object-valued stands down. +# THE SCALAR SITE, ISOLATED. `BOARD_RECORDS` filters an object-valued row out +# of the LIST before the per-issue guard ever sees it, so a board fixture alone +# cannot prove the scalar stand-down (@codex-reviewer-andresmgsl, #210 review). +# +# The fixture that isolates it: the LIST row is null-valued, so the board +# gather admits it — and the INDIVIDUAL payload the sweep then fetches is +# object-valued. Only reconcile_issue_pass's own guard can stand that down. printf '%s\n' \ - '[{"number":64,"pull_request":{"merged":false},"labels":[],"title":"a PR wearing an issue row"}]' \ + '[{"number":65,"pull_request":null,"labels":[],"title":"list says issue, payload says PR"}]' \ >"$FORGEJO_BOARD/repos_owner_repo_issues_state_open.json" jq -n --arg at "$(iso_at "$INOW")" \ - '{number:64,user:{login:"triage-one"},created_at:$at,body:"", + '{number:65,user:{login:"triage-one"},created_at:$at,body:"", pull_request:{merged:false},labels:[],assignees:[]}' \ - >"$FORGEJO_BOARD/repos_owner_repo_issues_64.json" -printf '[]\n' >"$FORGEJO_BOARD/repos_owner_repo_issues_64_comments.json" + >"$FORGEJO_BOARD/repos_owner_repo_issues_65.json" +printf '[]\n' >"$FORGEJO_BOARD/repos_owner_repo_issues_65_comments.json" fjb3_out="$(forgejo_board_run)" -check "an object-valued row is not reconciled as an issue by the pass" 1 "" \ - grep -qE '^issueflow: #64' <<<"$fjb3_out" -check "...and the sweep reports the board as empty of ISSUES, correctly" 0 "" \ - grep -qF 'issueflow: no open issues.' <<<"$fjb3_out" +check "the per-issue guard stands down an object-valued payload" 1 "" \ + grep -qE '^issueflow: #65: needs-triage' <<<"$fjb3_out" +check "...and the sweep still completes" 0 "" \ + grep -qF 'issueflow: reconciled.' <<<"$fjb3_out" + +# The same fixture with a null-valued payload MUST reconcile — otherwise the row +# above would pass on a guard that stands everything down. +jq -n --arg at "$(iso_at "$INOW")" \ + '{number:65,user:{login:"triage-one"},created_at:$at,body:"", + pull_request:null,labels:[],assignees:[]}' \ + >"$FORGEJO_BOARD/repos_owner_repo_issues_65.json" +fjb4_out="$(forgejo_board_run)" +check "...while a null-valued payload at the same site reconciles" 0 "" \ + grep -qE '^issueflow: #65: needs-triage' <<<"$fjb4_out" + +# And the GitHub shape — key absent entirely — is still an issue. +jq -n --arg at "$(iso_at "$INOW")" \ + '{number:65,user:{login:"triage-one"},created_at:$at,body:"",labels:[],assignees:[]}' \ + >"$FORGEJO_BOARD/repos_owner_repo_issues_65.json" +fjb5_out="$(forgejo_board_run)" +check "...and a github-shaped payload (key absent) reconciles too" 0 "" \ + grep -qE '^issueflow: #65: needs-triage' <<<"$fjb5_out" # -- the rule is pinned at the source, because a comment did not hold -------- # `.pull_request == null` is stated in this file's own header AND at