feat(forge): two backends behind one call surface, and the shim owns paging
Term 1's foundation. lib/forge.sh gains forge_select, which sources exactly
one of lib/forge-github.sh or lib/forge-forgejo.sh; both define the same
verbs, so no branching reaches the 61 call sites. The github backend is the
current gh invocation extracted 1:1 — term 5 is kept by making that path
boring.
The page size moves OUT of the call sites and into the backend, because it
is not portable and fails silently. Measured 2026-08-02:
?per_page=100 GitHub 100 items Forgejo 30 items (ignored)
?limit=100 GitHub 30 items Forgejo 50 items (capped)
Both answer HTTP 200 with valid JSON. Every call site here is GitHub-shaped,
so a verbatim port would have swept 30 of rig's 137 issues and printed
"reconciled." — criterion 2 failing green, the same failure class as the
blind sweep. Both page_url helpers strip a stray page-size parameter in
either dialect, so a call site cannot reintroduce it by accident.
Forgejo caps a page at 50 whatever is asked, so pagination is mandatory, not
an optimisation. The gather is then PROVEN complete against x-total-count
rather than assumed complete because a loop ended.
@kimi-reviewer-andresmgsl's hardening (#4699): a missing x-total-count is
itself a loud refusal. Header exposure is a server setting, and an assert
that cannot run must not silently pass — that is the failure class
re-entering through the guard built to stop it.
Call sites are not ported yet; that is the next commit.
Refs #188
2026-08-02 19:00:58 +00:00
|
|
|
#!/usr/bin/env bash
|
|
|
|
|
# Contract tests for lib/forge-github.sh and lib/forge-forgejo.sh
|
|
|
|
|
# (issue #188, term 1). set -u, not -e.
|
|
|
|
|
set -u
|
|
|
|
|
|
|
|
|
|
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
|
|
|
|
# shellcheck source=test/harness.sh
|
|
|
|
|
. "$ROOT/test/harness.sh"
|
|
|
|
|
# shellcheck source=lib/forge.sh
|
|
|
|
|
. "$ROOT/lib/forge.sh"
|
|
|
|
|
|
|
|
|
|
TMP="$(mktemp -d)"
|
|
|
|
|
trap 'rm -rf "$TMP"' EXIT
|
|
|
|
|
|
|
|
|
|
eq() {
|
|
|
|
|
local want="$1" got
|
|
|
|
|
shift
|
|
|
|
|
got="$("$@")" || return 1
|
|
|
|
|
[ "$got" = "$want" ]
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
# --- forge_select: exactly one backend, chosen deliberately -------------
|
|
|
|
|
|
|
|
|
|
check "select github loads the github backend" 0 "" \
|
|
|
|
|
bash -c '. '"$ROOT"'/lib/forge.sh; forge_select github; declare -f github_page_url >/dev/null'
|
|
|
|
|
check "select forgejo loads the forgejo backend" 0 "" \
|
|
|
|
|
bash -c '. '"$ROOT"'/lib/forge.sh; forge_select forgejo; declare -f forgejo_page_url >/dev/null'
|
|
|
|
|
check "select refuses an unknown forge" 1 "unknown forge" \
|
|
|
|
|
bash -c '. '"$ROOT"'/lib/forge.sh; forge_select gitlab'
|
2026-08-02 19:03:40 +00:00
|
|
|
# shellcheck disable=SC2016 # $FORGE expands in the isolated bash -c process
|
feat(forge): two backends behind one call surface, and the shim owns paging
Term 1's foundation. lib/forge.sh gains forge_select, which sources exactly
one of lib/forge-github.sh or lib/forge-forgejo.sh; both define the same
verbs, so no branching reaches the 61 call sites. The github backend is the
current gh invocation extracted 1:1 — term 5 is kept by making that path
boring.
The page size moves OUT of the call sites and into the backend, because it
is not portable and fails silently. Measured 2026-08-02:
?per_page=100 GitHub 100 items Forgejo 30 items (ignored)
?limit=100 GitHub 30 items Forgejo 50 items (capped)
Both answer HTTP 200 with valid JSON. Every call site here is GitHub-shaped,
so a verbatim port would have swept 30 of rig's 137 issues and printed
"reconciled." — criterion 2 failing green, the same failure class as the
blind sweep. Both page_url helpers strip a stray page-size parameter in
either dialect, so a call site cannot reintroduce it by accident.
Forgejo caps a page at 50 whatever is asked, so pagination is mandatory, not
an optimisation. The gather is then PROVEN complete against x-total-count
rather than assumed complete because a loop ended.
@kimi-reviewer-andresmgsl's hardening (#4699): a missing x-total-count is
itself a loud refusal. Header exposure is a server setting, and an assert
that cannot run must not silently pass — that is the failure class
re-entering through the guard built to stop it.
Call sites are not ported yet; that is the next commit.
Refs #188
2026-08-02 19:00:58 +00:00
|
|
|
check "select with no argument reads the environment" 0 "" \
|
|
|
|
|
bash -c 'CEREMONY_FORGE=forgejo; . '"$ROOT"'/lib/forge.sh; forge_select; [ "$FORGE" = forgejo ]'
|
|
|
|
|
|
|
|
|
|
# --- the page-size contract, both dialects ------------------------------
|
|
|
|
|
# The trap, measured 2026-08-02: each forge silently ignores the OTHER's
|
|
|
|
|
# page-size parameter and answers HTTP 200 with fewer items.
|
|
|
|
|
#
|
|
|
|
|
# ?per_page=100 GitHub 100 Forgejo 30 (ignored)
|
|
|
|
|
# ?limit=100 GitHub 30 Forgejo 50 (capped)
|
|
|
|
|
#
|
|
|
|
|
# So no call site names one, and these two functions are the only places
|
|
|
|
|
# that decide. Pure on purpose: the contract is testable without a network.
|
|
|
|
|
|
|
|
|
|
. "$ROOT/lib/forge-github.sh"
|
|
|
|
|
. "$ROOT/lib/forge-forgejo.sh"
|
|
|
|
|
|
|
|
|
|
check "github: a bare path gets a query" 0 "" \
|
|
|
|
|
eq 'repos/o/r/issues?per_page=100' github_page_url 'repos/o/r/issues'
|
|
|
|
|
check "github: an existing query is preserved" 0 "" \
|
|
|
|
|
eq 'repos/o/r/issues?state=open&per_page=100' github_page_url 'repos/o/r/issues?state=open'
|
|
|
|
|
check "forgejo: a bare path gets a query" 0 "" \
|
|
|
|
|
eq 'repos/o/r/issues?limit=50&page=1' forgejo_page_url 'repos/o/r/issues' 1
|
|
|
|
|
check "forgejo: an existing query is preserved" 0 "" \
|
|
|
|
|
eq 'repos/o/r/issues?state=open&limit=50&page=2' forgejo_page_url 'repos/o/r/issues?state=open' 2
|
|
|
|
|
|
|
|
|
|
# A caller that names a page size anyway must not be able to reintroduce the
|
|
|
|
|
# truncation — the parameter is stripped in BOTH dialects, on both backends,
|
|
|
|
|
# because the whole point is that the boundary decides and the call site
|
|
|
|
|
# cannot override it by accident.
|
|
|
|
|
check "github strips a stray per_page" 0 "" \
|
|
|
|
|
eq 'repos/o/r/issues?state=open&per_page=100' github_page_url 'repos/o/r/issues?state=open&per_page=30'
|
|
|
|
|
check "github strips a stray limit" 0 "" \
|
|
|
|
|
eq 'repos/o/r/issues?state=open&per_page=100' github_page_url 'repos/o/r/issues?state=open&limit=100'
|
|
|
|
|
check "forgejo strips a stray per_page" 0 "" \
|
|
|
|
|
eq 'repos/o/r/issues?state=open&limit=50&page=1' forgejo_page_url 'repos/o/r/issues?state=open&per_page=100' 1
|
|
|
|
|
check "forgejo strips a stray limit" 0 "" \
|
|
|
|
|
eq 'repos/o/r/issues?state=open&limit=50&page=1' forgejo_page_url 'repos/o/r/issues?state=open&limit=100' 1
|
|
|
|
|
check "stripping the only parameter leaves a clean query" 0 "" \
|
|
|
|
|
eq 'repos/o/r/issues?limit=50&page=1' forgejo_page_url 'repos/o/r/issues?per_page=100' 1
|
|
|
|
|
|
|
|
|
|
# --- the forgejo gather: complete, or loudly refused --------------------
|
|
|
|
|
# curl is stubbed as a function so these are hermetic. Each case writes the
|
|
|
|
|
# headers and body a real Forgejo would.
|
|
|
|
|
|
2026-08-02 19:07:32 +00:00
|
|
|
# fake_forge <total-spec> <pages…> — install a curl stub serving <pages> as
|
|
|
|
|
# successive page bodies, declaring <total-spec> in x-total-count. An empty
|
|
|
|
|
# string omits the header entirely (@kimi's #4699 case). A comma-separated
|
|
|
|
|
# spec declares a DIFFERENT total per page ("4,9"), which is
|
|
|
|
|
# @codex-reviewer-andresmgsl's changing-between-pages case (#4700 / #4712):
|
|
|
|
|
# a server whose count moves under the walk cannot have been read whole.
|
feat(forge): two backends behind one call surface, and the shim owns paging
Term 1's foundation. lib/forge.sh gains forge_select, which sources exactly
one of lib/forge-github.sh or lib/forge-forgejo.sh; both define the same
verbs, so no branching reaches the 61 call sites. The github backend is the
current gh invocation extracted 1:1 — term 5 is kept by making that path
boring.
The page size moves OUT of the call sites and into the backend, because it
is not portable and fails silently. Measured 2026-08-02:
?per_page=100 GitHub 100 items Forgejo 30 items (ignored)
?limit=100 GitHub 30 items Forgejo 50 items (capped)
Both answer HTTP 200 with valid JSON. Every call site here is GitHub-shaped,
so a verbatim port would have swept 30 of rig's 137 issues and printed
"reconciled." — criterion 2 failing green, the same failure class as the
blind sweep. Both page_url helpers strip a stray page-size parameter in
either dialect, so a call site cannot reintroduce it by accident.
Forgejo caps a page at 50 whatever is asked, so pagination is mandatory, not
an optimisation. The gather is then PROVEN complete against x-total-count
rather than assumed complete because a loop ended.
@kimi-reviewer-andresmgsl's hardening (#4699): a missing x-total-count is
itself a loud refusal. Header exposure is a server setting, and an assert
that cannot run must not silently pass — that is the failure class
re-entering through the guard built to stop it.
Call sites are not ported yet; that is the next commit.
Refs #188
2026-08-02 19:00:58 +00:00
|
|
|
fake_forge() {
|
|
|
|
|
FAKE_TOTAL="$1"; shift
|
|
|
|
|
FAKE_PAGES=("$@")
|
|
|
|
|
FAKE_CALLS=0
|
2026-08-02 19:03:40 +00:00
|
|
|
# shellcheck disable=SC2317 # the stub is invoked indirectly, by forge_api
|
feat(forge): two backends behind one call surface, and the shim owns paging
Term 1's foundation. lib/forge.sh gains forge_select, which sources exactly
one of lib/forge-github.sh or lib/forge-forgejo.sh; both define the same
verbs, so no branching reaches the 61 call sites. The github backend is the
current gh invocation extracted 1:1 — term 5 is kept by making that path
boring.
The page size moves OUT of the call sites and into the backend, because it
is not portable and fails silently. Measured 2026-08-02:
?per_page=100 GitHub 100 items Forgejo 30 items (ignored)
?limit=100 GitHub 30 items Forgejo 50 items (capped)
Both answer HTTP 200 with valid JSON. Every call site here is GitHub-shaped,
so a verbatim port would have swept 30 of rig's 137 issues and printed
"reconciled." — criterion 2 failing green, the same failure class as the
blind sweep. Both page_url helpers strip a stray page-size parameter in
either dialect, so a call site cannot reintroduce it by accident.
Forgejo caps a page at 50 whatever is asked, so pagination is mandatory, not
an optimisation. The gather is then PROVEN complete against x-total-count
rather than assumed complete because a loop ended.
@kimi-reviewer-andresmgsl's hardening (#4699): a missing x-total-count is
itself a loud refusal. Header exposure is a server setting, and an assert
that cannot run must not silently pass — that is the failure class
re-entering through the guard built to stop it.
Call sites are not ported yet; that is the next commit.
Refs #188
2026-08-02 19:00:58 +00:00
|
|
|
curl() {
|
|
|
|
|
local hdr="" out="" url=""
|
|
|
|
|
while [ $# -gt 0 ]; do
|
|
|
|
|
case "$1" in
|
|
|
|
|
-D) hdr="$2"; shift ;;
|
|
|
|
|
-o) out="$2"; shift ;;
|
|
|
|
|
-H) shift ;;
|
|
|
|
|
-*) ;;
|
|
|
|
|
*) url="$1" ;;
|
|
|
|
|
esac
|
|
|
|
|
shift
|
|
|
|
|
done
|
|
|
|
|
local page=1
|
|
|
|
|
case "$url" in *page=*) page="${url##*page=}"; page="${page%%&*}" ;; esac
|
2026-08-02 19:07:32 +00:00
|
|
|
local total="$FAKE_TOTAL"
|
|
|
|
|
case "$FAKE_TOTAL" in
|
|
|
|
|
*,*)
|
|
|
|
|
total="$(printf '%s' "$FAKE_TOTAL" | cut -d, -f"$page")"
|
|
|
|
|
[ -n "$total" ] || total="$(printf '%s' "$FAKE_TOTAL" | cut -d, -f1)"
|
|
|
|
|
;;
|
|
|
|
|
esac
|
feat(forge): two backends behind one call surface, and the shim owns paging
Term 1's foundation. lib/forge.sh gains forge_select, which sources exactly
one of lib/forge-github.sh or lib/forge-forgejo.sh; both define the same
verbs, so no branching reaches the 61 call sites. The github backend is the
current gh invocation extracted 1:1 — term 5 is kept by making that path
boring.
The page size moves OUT of the call sites and into the backend, because it
is not portable and fails silently. Measured 2026-08-02:
?per_page=100 GitHub 100 items Forgejo 30 items (ignored)
?limit=100 GitHub 30 items Forgejo 50 items (capped)
Both answer HTTP 200 with valid JSON. Every call site here is GitHub-shaped,
so a verbatim port would have swept 30 of rig's 137 issues and printed
"reconciled." — criterion 2 failing green, the same failure class as the
blind sweep. Both page_url helpers strip a stray page-size parameter in
either dialect, so a call site cannot reintroduce it by accident.
Forgejo caps a page at 50 whatever is asked, so pagination is mandatory, not
an optimisation. The gather is then PROVEN complete against x-total-count
rather than assumed complete because a loop ended.
@kimi-reviewer-andresmgsl's hardening (#4699): a missing x-total-count is
itself a loud refusal. Header exposure is a server setting, and an assert
that cannot run must not silently pass — that is the failure class
re-entering through the guard built to stop it.
Call sites are not ported yet; that is the next commit.
Refs #188
2026-08-02 19:00:58 +00:00
|
|
|
{
|
|
|
|
|
printf 'HTTP/1.1 200 OK\r\n'
|
2026-08-02 19:07:32 +00:00
|
|
|
[ -n "$total" ] && printf 'X-Total-Count: %s\r\n' "$total"
|
feat(forge): two backends behind one call surface, and the shim owns paging
Term 1's foundation. lib/forge.sh gains forge_select, which sources exactly
one of lib/forge-github.sh or lib/forge-forgejo.sh; both define the same
verbs, so no branching reaches the 61 call sites. The github backend is the
current gh invocation extracted 1:1 — term 5 is kept by making that path
boring.
The page size moves OUT of the call sites and into the backend, because it
is not portable and fails silently. Measured 2026-08-02:
?per_page=100 GitHub 100 items Forgejo 30 items (ignored)
?limit=100 GitHub 30 items Forgejo 50 items (capped)
Both answer HTTP 200 with valid JSON. Every call site here is GitHub-shaped,
so a verbatim port would have swept 30 of rig's 137 issues and printed
"reconciled." — criterion 2 failing green, the same failure class as the
blind sweep. Both page_url helpers strip a stray page-size parameter in
either dialect, so a call site cannot reintroduce it by accident.
Forgejo caps a page at 50 whatever is asked, so pagination is mandatory, not
an optimisation. The gather is then PROVEN complete against x-total-count
rather than assumed complete because a loop ended.
@kimi-reviewer-andresmgsl's hardening (#4699): a missing x-total-count is
itself a loud refusal. Header exposure is a server setting, and an assert
that cannot run must not silently pass — that is the failure class
re-entering through the guard built to stop it.
Call sites are not ported yet; that is the next commit.
Refs #188
2026-08-02 19:00:58 +00:00
|
|
|
printf '\r\n'
|
|
|
|
|
} >"$hdr"
|
|
|
|
|
if [ "$page" -le "${#FAKE_PAGES[@]}" ]; then
|
|
|
|
|
printf '%s' "${FAKE_PAGES[$((page - 1))]}" >"$out"
|
|
|
|
|
else
|
|
|
|
|
printf '[]' >"$out"
|
|
|
|
|
fi
|
|
|
|
|
FAKE_CALLS=$((FAKE_CALLS + 1))
|
|
|
|
|
return 0
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export CEREMONY_FORGE_API=https://forge.example/api/v1
|
|
|
|
|
|
|
|
|
|
# One page, and the count agrees with the declared total.
|
|
|
|
|
fake_forge 2 '[{"number":1},{"number":2}]'
|
|
|
|
|
check "a complete single-page gather returns its items" 0 "" \
|
|
|
|
|
eq $'1\n2' forge_api --paginate 'repos/o/r/issues' --jq '.[].number'
|
|
|
|
|
|
|
|
|
|
# Two pages that add up. The walk must not stop at the first page merely
|
|
|
|
|
# because it came back non-empty — rig has 137 issues across 3 pages, which
|
|
|
|
|
# is the case this models.
|
|
|
|
|
fake_forge 4 '[{"number":1},{"number":2}]' '[{"number":3},{"number":4}]'
|
|
|
|
|
check "a multi-page gather walks every page" 0 "" \
|
|
|
|
|
eq $'1\n2\n3\n4' forge_api --paginate 'repos/o/r/issues' --jq '.[].number'
|
|
|
|
|
|
|
|
|
|
# The whole reason the assert exists: a server that declares more than it
|
|
|
|
|
# hands over must not produce a "successful" partial sweep.
|
|
|
|
|
fake_forge 137 '[{"number":1},{"number":2}]'
|
|
|
|
|
check "a short gather is refused, not reconciled" 1 "incomplete gather" \
|
|
|
|
|
forge_api --paginate 'repos/o/r/issues' --jq '.[].number'
|
|
|
|
|
check "...and the refusal names both counts" 1 "collected 2 of 137" \
|
|
|
|
|
forge_api --paginate 'repos/o/r/issues' --jq '.[].number'
|
|
|
|
|
|
|
|
|
|
# @kimi-reviewer-andresmgsl's hardening (#4699): the guard must not be able
|
|
|
|
|
# to degrade silently either. A Forgejo that does not expose x-total-count
|
|
|
|
|
# leaves the assert with nothing to compare, and an assert that cannot run
|
|
|
|
|
# must refuse rather than pass.
|
|
|
|
|
fake_forge '' '[{"number":1},{"number":2}]'
|
|
|
|
|
check "a missing x-total-count refuses" 1 "did not send x-total-count" \
|
|
|
|
|
forge_api --paginate 'repos/o/r/issues' --jq '.[].number'
|
|
|
|
|
check "...and says why it cannot prove completeness" 1 "cannot prove the gather is complete" \
|
|
|
|
|
forge_api --paginate 'repos/o/r/issues' --jq '.[].number'
|
|
|
|
|
|
2026-08-02 19:07:32 +00:00
|
|
|
# @codex-reviewer-andresmgsl's #4712 findings. Each one is a route by which
|
|
|
|
|
# an unprovable read could still have been reported as a whole one — the
|
|
|
|
|
# guard leaking the failure class it was built to stop, which is why they
|
|
|
|
|
# are refusals rather than warnings.
|
|
|
|
|
|
|
|
|
|
# A total that is not a number went straight into arithmetic. Reproduced on
|
|
|
|
|
# ab23a3b: `X-Total-Count: not-a-number` returned rc=0 with that string as
|
|
|
|
|
# the total.
|
|
|
|
|
fake_forge 'not-a-number' '[{"number":1}]'
|
|
|
|
|
check "a non-numeric total is refused" 1 "not a non-negative integer" \
|
|
|
|
|
forge_api --paginate 'repos/o/r/issues' --jq '.[].number'
|
|
|
|
|
check "...and the refusal quotes what arrived" 1 "not-a-number" \
|
|
|
|
|
forge_api --paginate 'repos/o/r/issues' --jq '.[].number'
|
|
|
|
|
fake_forge '12x' '[{"number":1}]'
|
|
|
|
|
check "a partly-numeric total is refused" 1 "not a non-negative integer" \
|
|
|
|
|
forge_api --paginate 'repos/o/r/issues' --jq '.[].number'
|
|
|
|
|
fake_forge '-3' '[{"number":1}]'
|
|
|
|
|
check "a negative total is refused" 1 "not a non-negative integer" \
|
|
|
|
|
forge_api --paginate 'repos/o/r/issues' --jq '.[].number'
|
|
|
|
|
|
|
|
|
|
# A total that MOVES under the walk. The loop read it once, so a board
|
|
|
|
|
# changing size mid-gather was invisible: page 1 said 4, page 2 said 9, and
|
|
|
|
|
# the walk stopped at 4 believing itself complete.
|
|
|
|
|
fake_forge '4,9' '[{"number":1},{"number":2}]' '[{"number":3},{"number":4}]'
|
|
|
|
|
check "a total that changes between pages is refused" 1 "changed between pages" \
|
|
|
|
|
forge_api --paginate 'repos/o/r/issues' --jq '.[].number'
|
|
|
|
|
check "...and the refusal names both totals" 1 "4" \
|
|
|
|
|
forge_api --paginate 'repos/o/r/issues' --jq '.[].number'
|
|
|
|
|
|
|
|
|
|
# A 200 whose body is not a collection. `length` on a non-array counted 0,
|
|
|
|
|
# so an object or a scalar arriving where a list belongs read as a complete
|
|
|
|
|
# EMPTY collection when the declared total was 0 — silence dressed as a
|
|
|
|
|
# clean sweep.
|
|
|
|
|
fake_forge 0 '{"message":"Not found"}'
|
|
|
|
|
check "a non-array body is refused" 1 "did not return a collection" \
|
|
|
|
|
forge_api --paginate 'repos/o/r/issues' --jq '.[].number'
|
|
|
|
|
fake_forge 0 '"a string"'
|
|
|
|
|
check "a scalar body is refused" 1 "did not return a collection" \
|
|
|
|
|
forge_api --paginate 'repos/o/r/issues' --jq '.[].number'
|
|
|
|
|
# A genuinely empty collection is still fine — the refusal must not fire on
|
|
|
|
|
# a repo that legitimately has nothing.
|
|
|
|
|
fake_forge 0 '[]'
|
|
|
|
|
check "an empty collection is not an error" 0 "" \
|
|
|
|
|
forge_api --paginate 'repos/o/r/issues' --jq '.[].number'
|
|
|
|
|
|
feat(forge): two backends behind one call surface, and the shim owns paging
Term 1's foundation. lib/forge.sh gains forge_select, which sources exactly
one of lib/forge-github.sh or lib/forge-forgejo.sh; both define the same
verbs, so no branching reaches the 61 call sites. The github backend is the
current gh invocation extracted 1:1 — term 5 is kept by making that path
boring.
The page size moves OUT of the call sites and into the backend, because it
is not portable and fails silently. Measured 2026-08-02:
?per_page=100 GitHub 100 items Forgejo 30 items (ignored)
?limit=100 GitHub 30 items Forgejo 50 items (capped)
Both answer HTTP 200 with valid JSON. Every call site here is GitHub-shaped,
so a verbatim port would have swept 30 of rig's 137 issues and printed
"reconciled." — criterion 2 failing green, the same failure class as the
blind sweep. Both page_url helpers strip a stray page-size parameter in
either dialect, so a call site cannot reintroduce it by accident.
Forgejo caps a page at 50 whatever is asked, so pagination is mandatory, not
an optimisation. The gather is then PROVEN complete against x-total-count
rather than assumed complete because a loop ended.
@kimi-reviewer-andresmgsl's hardening (#4699): a missing x-total-count is
itself a loud refusal. Header exposure is a server setting, and an assert
that cannot run must not silently pass — that is the failure class
re-entering through the guard built to stop it.
Call sites are not ported yet; that is the next commit.
Refs #188
2026-08-02 19:00:58 +00:00
|
|
|
# --- HTTP failures are named, not swallowed -----------------------------
|
|
|
|
|
# gh exits non-zero on an HTTP error; curl does not without -f, and -f
|
|
|
|
|
# discards the body that explains why. So the status is read explicitly.
|
|
|
|
|
fake_forge 1 '[{"number":1}]'
|
|
|
|
|
curl() {
|
|
|
|
|
local hdr="" out=""
|
|
|
|
|
while [ $# -gt 0 ]; do
|
|
|
|
|
case "$1" in -D) hdr="$2"; shift ;; -o) out="$2"; shift ;; esac
|
|
|
|
|
shift
|
|
|
|
|
done
|
|
|
|
|
printf 'HTTP/1.1 404 Not Found\r\n\r\n' >"$hdr"
|
|
|
|
|
printf '{"message":"Not found"}' >"$out"
|
|
|
|
|
return 0
|
|
|
|
|
}
|
|
|
|
|
check "a 404 is a named failure" 1 "HTTP 404" forge_api 'repos/o/r/issues/9999'
|
|
|
|
|
check "a 404 names the endpoint" 1 "repos/o/r/issues/9999" forge_api 'repos/o/r/issues/9999'
|
|
|
|
|
|
|
|
|
|
# --- the api base must be known -----------------------------------------
|
|
|
|
|
check "no api base refuses" 1 "cannot reach the forge" \
|
|
|
|
|
bash -c 'unset CEREMONY_FORGE_API GITHUB_API_URL; . '"$ROOT"'/lib/forge-forgejo.sh; forgejo_api_base'
|
|
|
|
|
|
|
|
|
|
summary
|