ceremony-ci-probe/test/closes_references.test.sh
cluade-reviewer-andresmgsl 5797b418b9 feat(forge): replace both gh api graphql sites with REST + a body parser
Term 3 of #188. Forgejo has no GraphQL API, so these two gathers could not
be translated — there is no endpoint to translate them to. A real
forgejo-runner job says so from the other side: GITHUB_GRAPHQL_URL arrives
set to the empty string (probe task 278).

MERGED_REF_PR_RECORDS was already a body parse; GraphQL was buying
pagination, nothing semantic. OPEN_PR_ISSUES used GitHub's own parse of the
closing keywords, so it becomes lib/closes_references.sh — a sibling of
refs_references, sharing its LOCAL/CROSS classifier so rig#112 can still
never be read as local #112 (#61).

Both gathers now read /pulls, which /api/v3 and /api/v1 return in the same
shape (measured on both). merged_at replaces GraphQL's states: MERGED.
Bodies travel base64: jq's @tsv escapes a newline to a literal backslash-n,
which a line parser reads as one line and loses every declaration after the
first.

The accepted delta, written down rather than rediscovered: GitHub also
records closing links attached through the PR development sidebar, which
live in no body. This family declares links in the body, so the delta is
zero here.

Refs #188
2026-08-02 18:41:03 +00:00

87 lines
3.8 KiB
Bash

#!/usr/bin/env bash
# Contract tests for lib/closes_references.sh (issue #188, term 3).
# set -u, not -e: failing commands are behavior for the harness to inspect.
set -u
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
# shellcheck source=test/harness.sh
. "$ROOT/test/harness.sh"
# issue_references (the LOCAL/CROSS classifier) lives here; closes_references
# calls it, exactly as refs_references does.
# shellcheck source=actions/issueflow-reconcile/issueflow-reconcile.sh
. "$ROOT/actions/issueflow-reconcile/issueflow-reconcile.sh"
# shellcheck source=lib/closes_references.sh
. "$ROOT/lib/closes_references.sh"
# closes <want-newline-separated> <body> — the parse of <body> is exactly
# <want>. Exact, not substring: "12" is contained in "123".
closes() {
local want="$1" body="$2" got
got="$(printf '%s' "$body" | closes_references)"
[ "$got" = "$want" ]
}
# --- the three verbs, the three tenses ----------------------------------
# GitHub's documented keyword set. All of them, because a body that says
# "Fixed #4" and goes unclosed is a silent failure of the post-merge
# transition, not a loud one.
check "closes" 0 "" closes 1 'Closes #1'
check "close" 0 "" closes 1 'Close #1'
check "closed" 0 "" closes 1 'Closed #1'
check "fixes" 0 "" closes 2 'Fixes #2'
check "fix" 0 "" closes 2 'Fix #2'
check "fixed" 0 "" closes 2 'Fixed #2'
check "resolves" 0 "" closes 3 'Resolves #3'
check "resolve" 0 "" closes 3 'Resolve #3'
check "resolved" 0 "" closes 3 'Resolved #3'
check "case-insensitive" 0 "" closes 4 'CLOSES #4'
check "lowercase" 0 "" closes 4 'closes #4'
check "colon form" 0 "" closes 5 'Closes: #5'
# --- Refs is NOT a closing link -----------------------------------------
# The relation this file must not swallow. refs_references owns Refs, and
# conflating them makes every referenced issue look closeable — the
# post-merge transition #151 was reopened by hand over exactly that
# distinction.
check "Refs is not a close" 0 "" closes '' 'Refs #7'
check "Refs and Closes in one body keeps only the close" 0 "" \
closes 8 $'Refs #7\nCloses #8'
# --- cross-repo references stay out (#61) -------------------------------
# rig#112 must never be read as local #112. The classifier is shared with
# refs_references precisely so this rule has one implementation.
check "qualified reference is not local" 0 "" closes '' 'Closes rig#112'
check "owner-qualified reference is not local" 0 "" \
closes '' 'Closes heavy-duty/rig#112'
check "a local and a cross reference keep only the local" 0 "" \
closes 9 $'Closes rig#112\nCloses #9'
# --- every occurrence contributes ---------------------------------------
# Binding to the first occurrence is the defect #184 fixed in
# blocked_reference_records; this parser must not reintroduce it.
check "two closes on one line" 0 "" closes $'1\n2' 'Closes #1. Closes #2.'
check "two closes on two lines" 0 "" closes $'1\n2' $'Closes #1\nCloses #2'
check "sorted and deduplicated" 0 "" closes $'2\n10' $'Closes #10\nCloses #2\nCloses #10'
# --- prose must not be swallowed ----------------------------------------
check "trailing prose is not part of the reference" 0 "" \
closes 12 'Closes #12, and adds the guard'
check "a sentence terminator ends the reference" 0 "" closes 13 'Closes #13.'
check "no reference means no output" 0 "" closes '' 'Closes the door behind it'
check "a bare issue mention is not a close" 0 "" closes '' 'See #14 for context'
# "unclosed" contains "close" — a naive word match would fire on it.
check "a word merely containing a verb does not fire" 0 "" \
closes '' 'This left #15 unclosed'
# --- the shapes a real PR body carries ----------------------------------
check "the template's leading declaration" 0 "" \
closes 188 $'Closes #188\n\n## Acceptance criteria\n\n- [ ] a thing'
check "an empty body yields nothing" 0 "" closes '' ''
summary