From a4918a5a4693ab012107c520a1c1119ae56ebc84 Mon Sep 17 00:00:00 2001 From: claude-bot-andresmgsl Date: Thu, 23 Jul 2026 15:49:31 +0000 Subject: [PATCH] test: cover the shape check and the ladder's rungs Pure decisions (shape presence, rung boundaries, Default: parse for wording only), sweep probes for every AC path (malformed-once, conforming silence, rungs despite activity, cron progression, missed-moment skip, re-flag episode, unreadable comment list, malformed+rung same pass), and the existing nudge fixtures updated to conforming escalations with pre-seeded rung markers so each probe observes one behavior alone. Part of #73. Co-Authored-By: Claude Fable 5 --- test/issueflow-reconcile.test.sh | 11 +- test/labels-reconcile.test.sh | 18 ++- test/ruling.test.sh | 203 +++++++++++++++++++++++++++++-- 3 files changed, 216 insertions(+), 16 deletions(-) diff --git a/test/issueflow-reconcile.test.sh b/test/issueflow-reconcile.test.sh index 52e65a5..6b0d708 100644 --- a/test/issueflow-reconcile.test.sh +++ b/test/issueflow-reconcile.test.sh @@ -253,8 +253,17 @@ cfix() { printf '%s/repos_owner_repo_issues_%s_comments.json' "$TMP" "$1"; } jq -n --arg l "$(iso_at $((INOW - 10 * 86400)))" \ '[{"event":"labeled","label":{"name":"needs-ruling"},"actor":{"login":"setter"},"created_at":$l}, {"event":"assigned","created_at":$l}]' >"$(tfix 21)" +# The escalation is conforming and the rung markers are pre-seeded — by 10 +# days in both rungs fired long ago (#73), so this probe observes the nudge +# wiring alone; shape and rung behavior have their own probes in +# test/ruling.test.sh. jq -n --arg at "$(iso_at $((INOW - 10 * 86400 - 60)))" \ - '[{"user":{"login":"setter"},"created_at":$at,"html_url":"https://x/esc21","body":"question, options, recommendation"}]' \ + --arg b $'Options: A — x B — y\nRecommend: A, because x.\nBlocked: z\nDefault: none — hard block' \ + --arg r12 "$(iso_at $((INOW - 10 * 86400 + 13 * 3600)))" \ + --arg r24 "$(iso_at $((INOW - 10 * 86400 + 25 * 3600)))" \ + '[{"user":{"login":"setter"},"created_at":$at,"html_url":"https://x/esc21","body":$b}, + {"user":{"login":"sweep-bot"},"created_at":$r12,"html_url":"https://x/r12","body":"\nrung"}, + {"user":{"login":"sweep-bot"},"created_at":$r24,"html_url":"https://x/r24","body":"\nrung"}]' \ >"$(cfix 21)" exempt="$(issue_probe 21 $'claimed\nneeds-ruling')" check "a 10-day-quiet claim under a ruling is not reclaimed" 1 "" \ diff --git a/test/labels-reconcile.test.sh b/test/labels-reconcile.test.sh index fa89f25..6f5765e 100755 --- a/test/labels-reconcile.test.sh +++ b/test/labels-reconcile.test.sh @@ -557,12 +557,22 @@ ruling_sweep_probe() { # $1 = the PR's labels → reconcile_pr's log lines ) } -# The flag went up 8 days ago with its escalation posted seconds earlier. -jq -n --arg at "$(iso_at $((RNOW - 8 * 86400)))" \ +# The flag went up 10 days ago with its escalation posted seconds earlier; +# the newest activity is the reviews at 8 days. The escalation is conforming +# and the rung markers are pre-seeded — by now both rungs fired long ago +# (#73), older than the reviews so the quiet window still reads 8 days — +# and this probe observes the nudge wiring alone; shape and rung behavior +# have their own probes in test/ruling.test.sh. +jq -n --arg at "$(iso_at $((RNOW - 10 * 86400)))" \ '[{"event":"labeled","label":{"name":"needs-ruling"},"actor":{"login":"setter"},"created_at":$at}]' \ >"$RTMP/repos_owner_repo_issues_77_timeline.json" -jq -n --arg at "$(iso_at $((RNOW - 8 * 86400 - 60)))" \ - '[{"user":{"login":"setter"},"created_at":$at,"html_url":"https://x/esc77","body":"question, options, recommendation"}]' \ +jq -n --arg at "$(iso_at $((RNOW - 10 * 86400 - 60)))" \ + --arg b $'Options: A — x B — y\nRecommend: A, because x.\nBlocked: z\nDefault: none — hard block' \ + --arg r12 "$(iso_at $((RNOW - 10 * 86400 + 13 * 3600)))" \ + --arg r24 "$(iso_at $((RNOW - 10 * 86400 + 25 * 3600)))" \ + '[{"user":{"login":"setter"},"created_at":$at,"html_url":"https://x/esc77","body":$b}, + {"user":{"login":"sweep-bot"},"created_at":$r12,"html_url":"https://x/r12","body":"\nrung"}, + {"user":{"login":"sweep-bot"},"created_at":$r24,"html_url":"https://x/r24","body":"\nrung"}]' \ >"$RTMP/repos_owner_repo_issues_77_comments.json" wired="$(ruling_sweep_probe "needs-ruling")" diff --git a/test/ruling.test.sh b/test/ruling.test.sh index fa3a3ed..2eaffb7 100644 --- a/test/ruling.test.sh +++ b/test/ruling.test.sh @@ -51,12 +51,63 @@ check "exactly 7 days holds — strictly past the horizon, like the stale sweep" 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 <<<$'
fold\nOptions: A — x B — y\nRecommend: A.\nBlocked: nothing\nDefault: none\n
' +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 ' "$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 flag: silence ---------------------------------------------- +# -- 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))")" \ - '[{"user":{"login":"setter"},"created_at":$at,"html_url":"https://x/esc","body":"question, options, recommendation"}]' \ +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 flag posts nothing" 0 "0" posts 10 +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)) @@ -148,12 +201,18 @@ 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))")" \ - '[{"user":{"login":"setter"},"created_at":$at,"html_url":"https://x/esc12","body":"question, options, recommendation"}]' \ +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":"\nrung"}, + {"user":{"login":"sweep-bot"},"created_at":$r24,"html_url":"https://x/r24","body":"\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 @@ -170,21 +229,143 @@ check "the posted nudge is now the newest activity" 0 "" \ 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 --------------------------------------------------- -jq -n --arg at "$(iso "$((NOW - 6 * 86400))")" \ +# -- 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 - 6 * 86400))")" \ - '[{"user":{"login":"setter"},"created_at":$at,"html_url":"https://x/esc13","body":"question, options, recommendation"}]' \ +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" 0 "0" posts 13 +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":"\nold episode"}, + {"user":{"login":"sweep-bot"},"created_at":$r24,"html_url":"https://x/r24","body":"\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"