forked from heavy-duty/ceremony
Term 1 completed. All 52 runtime gh call sites in the three reconcilers and
lib/ruling.sh now go through forge_* verbs; the three remaining matches in
labels-reconcile are prose in comments. lib/facts.sh is deliberately
untouched — it is the release door, and the ruling keeps release.yml out of
this issue.
The CEREMONY_FORGE_CLIENT:-gh wrappers die here, in the same commit as the
sites they described, so the tree is never in a state where the declaration
lies. main() now runs forge_preflight then forge_select "".
Two sites needed judgment rather than substitution:
- labels-scope's write is forge_labels_add, a genuine additive POST on
both backends, NOT forge_issue_edit --add-label. ceremony#128 turns on
that write not being a read-modify-PUT: the labeler action computed
(labels-at-job-start union derived) and PUT the whole set, silently
dropping a label applied while the job ran. Routing it through a generic
edit verb would have quietly reopened that.
- the human-review request is forge_request_reviewer. Contrary to my
earlier reading, POST /pulls/{n}/requested_reviewers DOES exist on
Forgejo — 422 naming the reviewer's access without it, 201 with it. The
earlier 404 was a GET, which the endpoint does not serve, plus a
username that did not exist.
Test churn, all of it the term-5 boundary move:
- the suites select the github backend, so their existing gh() stubs stay
the boundary and keep intercepting;
- stubs strip the paging the shim injects, so fixtures stay keyed on the
logical endpoint (inlined in the PATH stub, which is a standalone
executable and cannot see a shell function);
- fixtures renamed off the per_page suffix for the same reason;
- recorded-mutation assertions now match the verb, not the raw gh line;
- gh() stubs carry SC2317: they are reached through the backend now, so
shellcheck can no longer see the call path.
Refs #188
385 lines
22 KiB
Bash
385 lines
22 KiB
Bash
#!/usr/bin/env bash
|
|
set -u
|
|
|
|
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
|
# shellcheck source=test/harness.sh
|
|
source "$ROOT/test/harness.sh"
|
|
# The suite drives the GITHUB backend: its gh() stubs ARE the forge boundary
|
|
# now, and forge_api/forge_issue_edit/... resolve to the gh invocations those
|
|
# stubs already intercept (#188). Without this the verbs are undefined.
|
|
# shellcheck source=lib/forge.sh
|
|
. "$ROOT/lib/forge.sh"
|
|
forge_select github
|
|
# shellcheck source=lib/ruling.sh
|
|
source "$ROOT/lib/ruling.sh"
|
|
|
|
TMP="$(mktemp -d)"
|
|
trap 'rm -rf "$TMP"' EXIT
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Pure decisions. Epochs are arbitrary fixed numbers — no wall clock anywhere.
|
|
# ---------------------------------------------------------------------------
|
|
|
|
check "needs-ruling is stale-exempt" 0 "EXEMPT" ruling_stale_exempt <<< $'claimed\nneeds-ruling'
|
|
check "a flag-free issue sweeps normally" 0 "SWEEP" ruling_stale_exempt <<< $'claimed'
|
|
|
|
L=100000 # the labeled event, in every window case below
|
|
|
|
check "escalation 14 minutes before the flag accompanies it" 0 "ACCOMPANIED" \
|
|
ruling_bare_decision setter "$L" <<<"setter $((L - 840))"
|
|
check "escalation exactly 15 minutes before still accompanies (no earlier than)" 0 "ACCOMPANIED" \
|
|
ruling_bare_decision setter "$L" <<<"setter $((L - 900))"
|
|
check "escalation after the flag accompanies it" 0 "ACCOMPANIED" \
|
|
ruling_bare_decision setter "$L" <<<"setter $((L + 60))"
|
|
check "escalation 16 minutes before is bare" 0 "BARE" \
|
|
ruling_bare_decision setter "$L" <<<"setter $((L - 960))"
|
|
check "somebody else's comment does not satisfy the contract" 0 "BARE" \
|
|
ruling_bare_decision setter "$L" <<<"bystander $((L - 60))"
|
|
check "no comment at all is bare" 0 "BARE" \
|
|
ruling_bare_decision setter "$L" </dev/null
|
|
|
|
# Re-flag: every fact anchors to the NEWEST labeled event, so an escalation
|
|
# that accompanied the first flag does not satisfy the second.
|
|
L2=$((L + 7200))
|
|
newest="$(ruling_newest_flag <<< "$(printf 'setter\t2020-01-01T00:00:00Z\nsetter\t2020-01-01T02:00:00Z\n')")"
|
|
check "the newest labeled event wins" 0 "" test "$newest" = "$(printf 'setter\t2020-01-01T02:00:00Z')"
|
|
check "first-flag escalation does not satisfy a re-flag" 0 "BARE" \
|
|
ruling_bare_decision setter "$L2" <<<"setter $((L - 60))"
|
|
|
|
check "no marked comment yet posts" 0 "POST" ruling_bare_comment_needed "$L" ""
|
|
check "a marked comment newer than the event skips" 0 "SKIP" ruling_bare_comment_needed "$L" $((L + 300))
|
|
check "a marked comment from an earlier flag episode re-posts" 0 "POST" ruling_bare_comment_needed "$L2" $((L + 300))
|
|
|
|
NOW=2000000000
|
|
check "8 days of silence nudges" 0 "NUDGE" ruling_nudge_decision "$NOW" $((NOW - 8 * 86400))
|
|
check "6 days of silence holds" 0 "KEEP" ruling_nudge_decision "$NOW" $((NOW - 6 * 86400))
|
|
check "exactly 7 days holds — strictly past the horizon, like the stale sweep" 0 "KEEP" \
|
|
ruling_nudge_decision "$NOW" $((NOW - 7 * 86400))
|
|
check "fresh activity holds" 0 "KEEP" ruling_nudge_decision "$NOW" $((NOW - 60))
|
|
|
|
# -- the ladder's rungs (#50 D13): anchored to the labeled event ------------
|
|
|
|
check "11h59m is rung 0" 0 "RUNG0" ruling_deadline_decision "$NOW" $((NOW - 12 * 3600 + 60))
|
|
check "exactly 12h starts the rung — at means at, unlike the nudge horizon" 0 "RUNG12" \
|
|
ruling_deadline_decision "$NOW" $((NOW - 12 * 3600))
|
|
check "12h01m is rung 12" 0 "RUNG12" ruling_deadline_decision "$NOW" $((NOW - 12 * 3600 - 60))
|
|
check "23h59m is still rung 12" 0 "RUNG12" ruling_deadline_decision "$NOW" $((NOW - 24 * 3600 + 60))
|
|
check "exactly 24h starts the last rung" 0 "RUNG24" ruling_deadline_decision "$NOW" $((NOW - 24 * 3600))
|
|
check "25h is rung 24 — past-24h has no fourth timer" 0 "RUNG24" \
|
|
ruling_deadline_decision "$NOW" $((NOW - 25 * 3600))
|
|
|
|
# -- the Default: line, parsed for wording only (#50 D14) -------------------
|
|
|
|
check "a timed default parses to its deadline" 0 "DEADLINE 2026-07-23T21:00Z" \
|
|
ruling_default_decision <<<$'Blocked: x\nDefault: A at 2026-07-23T21:00Z if no ruling'
|
|
check "a bold default line with seconds parses" 0 "DEADLINE 2026-07-23T21:00:30Z" \
|
|
ruling_default_decision <<<$'**Default:** A at 2026-07-23T21:00:30Z if quiet'
|
|
check "none is a hard block" 0 "HARDBLOCK" ruling_default_decision <<<$'Default: none — hard block'
|
|
check "prose is unparseable, never guessed" 0 "UNPARSEABLE" \
|
|
ruling_default_decision <<<$'Default: when it feels right'
|
|
check "a missing default line is unparseable" 0 "UNPARSEABLE" \
|
|
ruling_default_decision <<<$'Options: A\nRecommend: A.'
|
|
check "a timestamp off the default line is somebody's prose" 0 "UNPARSEABLE" \
|
|
ruling_default_decision <<<$'the deadline 2026-07-23T21:00Z came up above\nDefault: soonish'
|
|
|
|
rows="$(printf 'setter %s https://x/first\nsetter %s https://x/late\nbystander %s https://x/other\nsetter %s https://x/early-out\n' \
|
|
"$((L - 300))" "$((L + 600))" "$((L - 60))" "$((L - 5000))")"
|
|
check "the nudge links the earliest in-window escalation by the setter" 0 "https://x/first" \
|
|
ruling_escalation_url setter "$L" <<<"$rows"
|
|
check "no qualifying escalation yields no link" 0 "" \
|
|
ruling_escalation_url setter "$L" <<<"bystander $((L - 60)) https://x/other"
|
|
check "the url survives the base64 body column" 0 "https://x/first" \
|
|
ruling_escalation_url setter "$L" <<<"setter $((L - 300)) https://x/first $(printf 'Options: A' | base64)"
|
|
check "the row carries the body for the shape check" 0 "https://x/first $(printf 'Options: A' | base64)" \
|
|
ruling_escalation_row setter "$L" <<<"setter $((L - 300)) https://x/first $(printf 'Options: A' | base64)"
|
|
|
|
# -- the escalation comment's shape (#50 D12): presence only, line-anchored --
|
|
|
|
TPL=$'🧭 needs-ruling — fixture decision\nOptions: A — on B — off\nRecommend: A, because the drill says so.\nBlocked: the fixture stops; everything else continues\nDefault: A at 2026-07-23T21:00Z if no ruling'
|
|
TPL_BOLD=$'🧭 needs-ruling — fixture decision\n**Options:** A — on B — off\n**Recommend:** A, because the drill says so.\n**Blocked:** the fixture stops\n**Default:** none — hard block'
|
|
|
|
check "all four field labels present is shaped" 0 "SHAPED" ruling_shape_decision <<<"$TPL"
|
|
check "bold field labels are shaped — the live escalations write them bold" 0 "SHAPED" \
|
|
ruling_shape_decision <<<"$TPL_BOLD"
|
|
check "labels inside a details fold are shaped — line-anchored, not fold-aware" 0 "SHAPED" \
|
|
ruling_shape_decision <<<$'<details><summary>fold</summary>\nOptions: A — x B — y\nRecommend: A.\nBlocked: nothing\nDefault: none\n</details>'
|
|
check "leading whitespace is tolerated" 0 "SHAPED" \
|
|
ruling_shape_decision <<<$' Options: A B\n Recommend: A.\n Blocked: x\n Default: none'
|
|
check "one missing label is named" 0 "MALFORMED Recommend:" \
|
|
ruling_shape_decision <<<$'Options: A — x B — y\nBlocked: z\nDefault: none — hard block'
|
|
check "two missing labels are both named" 0 "MALFORMED Recommend: Default:" \
|
|
ruling_shape_decision <<<$'Options: A — x B — y\nBlocked: z'
|
|
check "labels only mid-sentence are malformed — line-anchoring is the rule" 0 \
|
|
"MALFORMED Options: Recommend: Blocked: Default:" \
|
|
ruling_shape_decision <<<$'we should Options: A or B, and I Recommend: A; Blocked: no; Default: none'
|
|
check "an empty body is missing everything" 0 "MALFORMED Options: Recommend: Blocked: Default:" \
|
|
ruling_shape_decision </dev/null
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# The orchestrator, against a recording gh stub. The stub serves fixture JSON
|
|
# per endpoint (missing file = unreadable read), applies the caller's --jq
|
|
# with real jq, appends posted comments back into the fixture (so a second
|
|
# sweep sees the first sweep's writes, exactly like the live board), and
|
|
# records every `gh issue edit` — which must never happen from this code.
|
|
# ---------------------------------------------------------------------------
|
|
|
|
REPO=owner/repo
|
|
log() { printf 'test-sweep: %s\n' "$*"; }
|
|
run() { "$@"; }
|
|
|
|
iso() { date -u -d "@$1" +%Y-%m-%dT%H:%M:%SZ; }
|
|
|
|
# shellcheck disable=SC2317 # reached through the forge backend, not called directly (#188)
|
|
gh() {
|
|
if [ "$1" = api ]; then
|
|
shift
|
|
local jqexpr="" endpoint="" file
|
|
while [ $# -gt 0 ]; do
|
|
case "$1" in
|
|
--jq) jqexpr="$2"; shift ;;
|
|
-*) ;;
|
|
*) [ -n "$endpoint" ] || endpoint="$1" ;;
|
|
esac
|
|
shift
|
|
done
|
|
endpoint="$(forge_stub_path "$endpoint")"
|
|
file="$TMP/$(printf '%s' "$endpoint" | tr '/' '_').json"
|
|
[ -f "$file" ] || return 1
|
|
if [ -n "$jqexpr" ]; then jq -r "$jqexpr" "$file"; else cat "$file"; fi
|
|
elif [ "$1" = issue ] && [ "$2" = comment ]; then
|
|
local n="$3" body="" file
|
|
shift 3
|
|
while [ $# -gt 0 ]; do
|
|
case "$1" in --body) body="$2"; shift ;; esac
|
|
shift
|
|
done
|
|
printf '%s\n----\n' "$body" >>"$TMP/posted-$n"
|
|
file="$TMP/repos_${REPO%%/*}_${REPO#*/}_issues_${n}_comments.json"
|
|
[ -f "$file" ] || printf '[]\n' >"$file"
|
|
jq --arg b "$body" --arg at "$(iso "$NOW")" \
|
|
'. + [{"user":{"login":"sweep-bot"},"created_at":$at,"html_url":"https://x/posted","body":$b}]' \
|
|
"$file" >"$file.tmp" && mv "$file.tmp" "$file"
|
|
elif [ "$1" = issue ] && [ "$2" = edit ]; then
|
|
printf '%s\n' "$*" >>"$TMP/edits"
|
|
fi
|
|
}
|
|
|
|
posts() { [ -f "$TMP/posted-$1" ] && grep -c '^----$' "$TMP/posted-$1" || echo 0; }
|
|
timeline_file() { printf '%s/repos_owner_repo_issues_%s_timeline.json' "$TMP" "$1"; }
|
|
comments_file() { printf '%s/repos_owner_repo_issues_%s_comments.json' "$TMP" "$1"; }
|
|
|
|
# -- bare flag, swept twice: exactly one comment -----------------------------
|
|
T=$((NOW - 3600))
|
|
jq -n --arg at "$(iso "$T")" \
|
|
'[{"event":"labeled","label":{"name":"needs-ruling"},"actor":{"login":"setter"},"created_at":$at}]' \
|
|
>"$(timeline_file 9)"
|
|
printf '[]\n' >"$(comments_file 9)"
|
|
check "a bare flag is commented on" 0 "ruling flag is bare" reconcile_ruling 9 "$T" "$NOW"
|
|
check "...and the sweep never posted twice" 0 "" reconcile_ruling 9 "$T" "$NOW"
|
|
check "one bare comment across two sweeps" 0 "1" posts 9
|
|
check "the bare comment carries its marker" 0 "" \
|
|
grep -qF '<!-- ceremony:needs-ruling-bare -->' "$TMP/posted-9"
|
|
check "the bare comment names the missing contract" 0 "" \
|
|
grep -q 'question' "$TMP/posted-9"
|
|
check "a bare flag draws no shape or rung comment — bare stops here" 1 "" \
|
|
grep -qE 'needs-ruling-(shape|rung)' "$TMP/posted-9"
|
|
|
|
# -- accompanied, conforming (bold, the live spelling): silence --------------
|
|
jq -n --arg at "$(iso "$T")" \
|
|
'[{"event":"labeled","label":{"name":"needs-ruling"},"actor":{"login":"setter"},"created_at":$at}]' \
|
|
>"$(timeline_file 10)"
|
|
jq -n --arg at "$(iso "$((T - 840))")" --arg b "$TPL_BOLD" \
|
|
'[{"user":{"login":"setter"},"created_at":$at,"html_url":"https://x/esc","body":$b}]' \
|
|
>"$(comments_file 10)"
|
|
reconcile_ruling 10 "$T" "$NOW" >/dev/null
|
|
check "an accompanied conforming flag posts nothing" 0 "0" posts 10
|
|
|
|
# -- re-flag: judged on its own escalation, marker scoped per event ----------
|
|
T1=$((NOW - 86400)) T2=$((NOW - 7200))
|
|
jq -n --arg t1 "$(iso "$T1")" --arg t2 "$(iso "$T2")" \
|
|
'[{"event":"labeled","label":{"name":"needs-ruling"},"actor":{"login":"setter"},"created_at":$t1},
|
|
{"event":"unlabeled","label":{"name":"needs-ruling"},"actor":{"login":"setter"},"created_at":$t2},
|
|
{"event":"labeled","label":{"name":"needs-ruling"},"actor":{"login":"setter"},"created_at":$t2}]' \
|
|
>"$(timeline_file 11)"
|
|
jq -n --arg esc "$(iso "$((T1 - 60))")" --arg marked "$(iso "$((T1 + 300))")" \
|
|
'[{"user":{"login":"setter"},"created_at":$esc,"html_url":"https://x/esc1","body":"the first escalation"},
|
|
{"user":{"login":"sweep-bot"},"created_at":$marked,"html_url":"https://x/bare1","body":"<!-- ceremony:needs-ruling-bare -->\nolder episode"}]' \
|
|
>"$(comments_file 11)"
|
|
reconcile_ruling 11 "$T2" "$NOW" >/dev/null
|
|
check "a re-flag is re-checked against its own escalation" 0 "1" posts 11
|
|
|
|
# -- nudge: fires past 7 quiet days, links the escalation, resets itself -----
|
|
# The escalation is conforming and the rung markers are pre-seeded (by 8 days
|
|
# in, both rungs fired long ago — the realistic board), so the nudge's
|
|
# behavior is observed alone.
|
|
T0=$((NOW - 8 * 86400))
|
|
jq -n --arg at "$(iso "$T0")" \
|
|
'[{"event":"labeled","label":{"name":"needs-ruling"},"actor":{"login":"setter"},"created_at":$at}]' \
|
|
>"$(timeline_file 12)"
|
|
jq -n --arg at "$(iso "$((T0 - 60))")" --arg b "$TPL" \
|
|
--arg r12 "$(iso "$((T0 + 13 * 3600))")" --arg r24 "$(iso "$((T0 + 25 * 3600))")" \
|
|
'[{"user":{"login":"setter"},"created_at":$at,"html_url":"https://x/esc12","body":$b},
|
|
{"user":{"login":"sweep-bot"},"created_at":$r12,"html_url":"https://x/r12","body":"<!-- ceremony:needs-ruling-rung12 -->\nrung"},
|
|
{"user":{"login":"sweep-bot"},"created_at":$r24,"html_url":"https://x/r24","body":"<!-- ceremony:needs-ruling-rung24 -->\nrung"}]' \
|
|
>"$(comments_file 12)"
|
|
check "8 quiet days nudge" 0 "ruling nudge" reconcile_ruling 12 "$T0" "$NOW"
|
|
check "one nudge posted" 0 "1" posts 12
|
|
check "the nudge links the escalation comment" 0 "" grep -qF 'https://x/esc12' "$TMP/posted-12"
|
|
check "the nudge addresses the decider" 0 "" grep -qF '@danmt' "$TMP/posted-12"
|
|
check "the nudge does not tag the flag-setter" 1 "" grep -qF '@setter' "$TMP/posted-12"
|
|
check "the nudge carries no marker — the comment itself resets the window" 1 "" \
|
|
grep -qF "$RULING_BARE_MARKER" "$TMP/posted-12"
|
|
# The reset, through the surfaces' own activity computation: the nudge the
|
|
# stub appended is the newest comment, so the recomputed last-activity is NOW.
|
|
newest_at="$(jq -r 'map(.created_at) | max' "$(comments_file 12)")"
|
|
check "the posted nudge is now the newest activity" 0 "" \
|
|
test "$(date -d "$newest_at" +%s)" = "$NOW"
|
|
reconcile_ruling 12 "$(date -d "$newest_at" +%s)" "$NOW" >/dev/null
|
|
check "a sweep right after the nudge holds its silence" 0 "1" posts 12
|
|
|
|
# -- 6 quiet days: silence. The flag itself is fresh (label churn is not
|
|
# activity, so a quiet item can be freshly flagged): rung 0, no nudge. ------
|
|
jq -n --arg at "$(iso "$((NOW - 3600))")" \
|
|
'[{"event":"labeled","label":{"name":"needs-ruling"},"actor":{"login":"setter"},"created_at":$at}]' \
|
|
>"$(timeline_file 13)"
|
|
jq -n --arg at "$(iso "$((NOW - 3660))")" --arg b "$TPL" \
|
|
'[{"user":{"login":"setter"},"created_at":$at,"html_url":"https://x/esc13","body":$b}]' \
|
|
>"$(comments_file 13)"
|
|
reconcile_ruling 13 $((NOW - 6 * 86400)) "$NOW" >/dev/null
|
|
check "6 quiet days do not nudge, and a fresh flag sits on rung 0" 0 "0" posts 13
|
|
|
|
# -- unreadable timeline: nothing happens ------------------------------------
|
|
check "an unreadable timeline invents no verdict" 0 "timeline unreadable" \
|
|
reconcile_ruling 14 "$T" "$NOW"
|
|
check "...and posts nothing" 0 "0" posts 14
|
|
|
|
# -- malformed escalation, rung 0: the shape comment, exactly once -----------
|
|
T15=$((NOW - 3600))
|
|
jq -n --arg at "$(iso "$T15")" \
|
|
'[{"event":"labeled","label":{"name":"needs-ruling"},"actor":{"login":"setter"},"created_at":$at}]' \
|
|
>"$(timeline_file 15)"
|
|
jq -n --arg at "$(iso "$((T15 - 60))")" \
|
|
--arg b $'Options: A — x B — y\nBlocked: z\nDefault: none — hard block' \
|
|
'[{"user":{"login":"setter"},"created_at":$at,"html_url":"https://x/esc15","body":$b}]' \
|
|
>"$(comments_file 15)"
|
|
check "a malformed escalation is commented on" 0 "escalation malformed" \
|
|
reconcile_ruling 15 "$T15" "$NOW"
|
|
reconcile_ruling 15 "$T15" "$NOW" >/dev/null
|
|
check "one shape comment across two sweeps" 0 "1" posts 15
|
|
check "the shape comment names exactly the missing label" 0 "" \
|
|
grep -qF 'missing required field labels: **Recommend:**' "$TMP/posted-15"
|
|
check "the shape comment links the escalation" 0 "" grep -qF 'https://x/esc15' "$TMP/posted-15"
|
|
check "the shape comment quotes the template location" 0 "" \
|
|
grep -qF 'BUILDER.md#the-ruling-ask' "$TMP/posted-15"
|
|
check "no bare comment beside the shape comment" 1 "" \
|
|
grep -qF "$RULING_BARE_MARKER" "$TMP/posted-15"
|
|
check "the shape comment carries its marker" 0 "" \
|
|
grep -qF "$RULING_SHAPE_MARKER" "$TMP/posted-15"
|
|
|
|
# -- 13h in, activity minutes old: the 12h rung fires anyway (D14) -----------
|
|
T16=$((NOW - 13 * 3600))
|
|
jq -n --arg at "$(iso "$T16")" \
|
|
'[{"event":"labeled","label":{"name":"needs-ruling"},"actor":{"login":"setter"},"created_at":$at}]' \
|
|
>"$(timeline_file 16)"
|
|
jq -n --arg at "$(iso "$((T16 - 60))")" --arg b "$TPL_BOLD" \
|
|
'[{"user":{"login":"setter"},"created_at":$at,"html_url":"https://x/esc16","body":$b}]' \
|
|
>"$(comments_file 16)"
|
|
check "the 12h rung fires despite recent activity — rungs never reset" 0 "12h rung" \
|
|
reconcile_ruling 16 $((NOW - 60)) "$NOW"
|
|
reconcile_ruling 16 $((NOW - 60)) "$NOW" >/dev/null
|
|
check "one 12h rung comment, once per episode" 0 "1" posts 16
|
|
check "the rung comment is addressed to the flag-setter" 0 "" grep -qF '@setter' "$TMP/posted-16"
|
|
check "the rung comment names the hard block" 0 "" grep -qF 'Default: none' "$TMP/posted-16"
|
|
check "no nudge rode along — activity is recent and the nudge does reset" 1 "" \
|
|
grep -qF '@danmt' "$TMP/posted-16"
|
|
check "the rung comment carries its marker" 0 "" \
|
|
grep -qF "$RULING_RUNG12_MARKER" "$TMP/posted-16"
|
|
|
|
# -- the ladder walked on the cron: 12h rung at 13h, 24h rung at 25h ---------
|
|
L17=$((NOW - 25 * 3600))
|
|
jq -n --arg at "$(iso "$L17")" \
|
|
'[{"event":"labeled","label":{"name":"needs-ruling"},"actor":{"login":"setter"},"created_at":$at}]' \
|
|
>"$(timeline_file 17)"
|
|
jq -n --arg at "$(iso "$((L17 - 60))")" --arg b "$TPL" \
|
|
'[{"user":{"login":"setter"},"created_at":$at,"html_url":"https://x/esc17","body":$b}]' \
|
|
>"$(comments_file 17)"
|
|
reconcile_ruling 17 "$L17" $((L17 + 13 * 3600)) >/dev/null
|
|
check "a sweep at 13h posts the 12h rung" 0 "1" posts 17
|
|
check "the rung comment names the stated deadline" 0 "" \
|
|
grep -qF '2026-07-23T21:00Z' "$TMP/posted-17"
|
|
check "a sweep at 25h posts the 24h rung — one comment per rung" 0 "24h rung" \
|
|
reconcile_ruling 17 "$L17" "$NOW"
|
|
check "two rung comments total" 0 "2" posts 17
|
|
check "the 24h comment names triage's past-24h authority" 0 "" \
|
|
grep -qF 'triage picks the option' "$TMP/posted-17"
|
|
reconcile_ruling 17 "$L17" "$NOW" >/dev/null
|
|
check "...and never a third within the episode" 0 "2" posts 17
|
|
|
|
# -- first observed past 24h: the missed 12h moment is not paged after the
|
|
# fact — the 24h comment carries the whole remaining duty ---------------------
|
|
T18=$((NOW - 25 * 3600))
|
|
jq -n --arg at "$(iso "$T18")" \
|
|
'[{"event":"labeled","label":{"name":"needs-ruling"},"actor":{"login":"setter"},"created_at":$at}]' \
|
|
>"$(timeline_file 18)"
|
|
jq -n --arg at "$(iso "$((T18 - 60))")" \
|
|
--arg b $'Options: A — x B — y\nRecommend: A, because x.\nBlocked: z\nDefault: when it feels right' \
|
|
'[{"user":{"login":"setter"},"created_at":$at,"html_url":"https://x/esc18","body":$b}]' \
|
|
>"$(comments_file 18)"
|
|
reconcile_ruling 18 "$T18" "$NOW" >/dev/null
|
|
check "a rung first observed past 24h pages once, not retroactively" 0 "1" posts 18
|
|
check "only the 24h comment fired" 1 "" grep -qF "$RULING_RUNG12_MARKER" "$TMP/posted-18"
|
|
check "the unparseable default is reported, not guessed" 0 "" \
|
|
grep -qF 'unparseable' "$TMP/posted-18"
|
|
|
|
# -- a re-flag climbs its own ladder: old rung markers belong to the old
|
|
# episode -------------------------------------------------------------------
|
|
TA=$((NOW - 3 * 86400)) TB=$((NOW - 13 * 3600))
|
|
jq -n --arg ta "$(iso "$TA")" --arg tb "$(iso "$TB")" \
|
|
'[{"event":"labeled","label":{"name":"needs-ruling"},"actor":{"login":"setter"},"created_at":$ta},
|
|
{"event":"unlabeled","label":{"name":"needs-ruling"},"actor":{"login":"setter"},"created_at":$tb},
|
|
{"event":"labeled","label":{"name":"needs-ruling"},"actor":{"login":"setter"},"created_at":$tb}]' \
|
|
>"$(timeline_file 19)"
|
|
jq -n --arg esc "$(iso "$((TB - 60))")" --arg b "$TPL" \
|
|
--arg r12 "$(iso "$((TA + 13 * 3600))")" --arg r24 "$(iso "$((TA + 25 * 3600))")" \
|
|
'[{"user":{"login":"sweep-bot"},"created_at":$r12,"html_url":"https://x/r12","body":"<!-- ceremony:needs-ruling-rung12 -->\nold episode"},
|
|
{"user":{"login":"sweep-bot"},"created_at":$r24,"html_url":"https://x/r24","body":"<!-- ceremony:needs-ruling-rung24 -->\nold episode"},
|
|
{"user":{"login":"setter"},"created_at":$esc,"html_url":"https://x/esc19","body":$b}]' \
|
|
>"$(comments_file 19)"
|
|
reconcile_ruling 19 "$TB" "$NOW" >/dev/null
|
|
check "a re-flag climbs its own ladder — old rung markers do not stick" 0 "1" posts 19
|
|
check "the re-flag's rung comment is the 12h rung" 0 "" \
|
|
grep -qF "$RULING_RUNG12_MARKER" "$TMP/posted-19"
|
|
|
|
# -- unreadable comment list: nothing happens --------------------------------
|
|
jq -n --arg at "$(iso "$T")" \
|
|
'[{"event":"labeled","label":{"name":"needs-ruling"},"actor":{"login":"setter"},"created_at":$at}]' \
|
|
>"$(timeline_file 20)"
|
|
check "an unreadable comment list invents no verdict" 0 "comments unreadable" \
|
|
reconcile_ruling 20 "$T" "$NOW"
|
|
check "...and it posts nothing" 0 "0" posts 20
|
|
|
|
# -- malformed AND on a rung: the shape is owed and the ladder still climbs --
|
|
T21=$((NOW - 13 * 3600))
|
|
jq -n --arg at "$(iso "$T21")" \
|
|
'[{"event":"labeled","label":{"name":"needs-ruling"},"actor":{"login":"setter"},"created_at":$at}]' \
|
|
>"$(timeline_file 21)"
|
|
jq -n --arg at "$(iso "$((T21 - 60))")" \
|
|
--arg b $'Options: A — x B — y\nRecommend: A, because x.\nBlocked: z' \
|
|
'[{"user":{"login":"setter"},"created_at":$at,"html_url":"https://x/esc21","body":$b}]' \
|
|
>"$(comments_file 21)"
|
|
reconcile_ruling 21 "$T21" "$NOW" >/dev/null
|
|
check "a malformed escalation still climbs the ladder — two comments" 0 "2" posts 21
|
|
check "the shape half fired" 0 "" grep -qF "$RULING_SHAPE_MARKER" "$TMP/posted-21"
|
|
check "the rung half fired" 0 "" grep -qF "$RULING_RUNG12_MARKER" "$TMP/posted-21"
|
|
reconcile_ruling 21 "$T21" "$NOW" >/dev/null
|
|
check "both halves are once-per-episode" 0 "2" posts 21
|
|
|
|
# -- across every scenario above: not one label write ------------------------
|
|
check "the ruling sweep never wrote a label" 1 "" test -f "$TMP/edits"
|
|
|
|
# Grep-level pin for #50 D9: no mutation call in the sweep code names the
|
|
# flag. The only writes reconcile_ruling makes are comments.
|
|
check "no add/remove-label mutation names the ruling flag" 1 "" \
|
|
grep -rEn -- '(add|remove)-label[^"]*needs-ruling' "$ROOT/actions" "$ROOT/lib"
|
|
|
|
summary
|