forked from heavy-duty/ceremony
Merge pull request #263 from andriujoseba/build/232-attention-diagnostics
feat: diagnose malformed attention targets
This commit is contained in:
commit
db63b677bb
8 changed files with 335 additions and 10 deletions
|
|
@ -175,9 +175,11 @@ take. It is hand-set: the machine never sets `attention`, never assigns
|
|||
anyone to receive one, and never decides that one has been answered — the
|
||||
assignee's removal is the only ack. It writes the label in exactly one
|
||||
place, the derived `claimed` → `post-merge` transition below, and nowhere
|
||||
else; where it reads the flag it reads it to diagnose, and a diagnosis is a
|
||||
comment that leaves the label alone. No reconciler enforces the assignee
|
||||
requirement either: an unassigned flag may be reported, never repaired.
|
||||
else; where it reads the flag it reads it to diagnose. The PR sweep comments
|
||||
when `attention` is put on a pull request, and the issue sweep comments when
|
||||
it is put on an issue with no assignee. Both diagnoses leave the label and
|
||||
assignees alone; the machine never infers the claim issue, decides that the
|
||||
demand was answered, or repairs either malformed shape.
|
||||
An `attention` issue without an assignee is therefore a board bug, not a
|
||||
demand; anyone may assign it or remove the flag. It never composes with
|
||||
`post-merge`, whose released claim has no assignee to answer the demand. The
|
||||
|
|
|
|||
|
|
@ -27,6 +27,9 @@ TRIAGE_ACTORS=()
|
|||
# The needs-ruling invariants (#52) — one implementation for both surfaces.
|
||||
# shellcheck source=lib/ruling.sh
|
||||
. "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/../../lib/ruling.sh"
|
||||
# The attention target invariants (#232) — diagnosis only, both surfaces.
|
||||
# shellcheck source=lib/attention.sh
|
||||
. "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/../../lib/attention.sh"
|
||||
# The guarded read and its reason line (#101, #247) — one implementation for
|
||||
# both surfaces.
|
||||
# shellcheck source=lib/read.sh
|
||||
|
|
@ -452,6 +455,7 @@ reconcile_issue() {
|
|||
local n="$1" decision refs cross_refs states age created assignees open_pr=false label owners
|
||||
local merged_ref_pr="" transition_marker="" transition_handled=false
|
||||
local unchecked="" remove_claimed=claimed
|
||||
local attention_active=true attention_suppression=""
|
||||
decision="$(queue_decision <<<"$ISSUE_LABELS")"
|
||||
case "$decision" in
|
||||
ADD_NEEDS_TRIAGE)
|
||||
|
|
@ -495,6 +499,7 @@ The merge releases the claim; no builder owes a draft. Triage owes completion in
|
|||
--remove-label "$remove_claimed" --add-label post-merge >/dev/null
|
||||
fi
|
||||
log "#$n: merged Refs PR -> post-merge; claim released"
|
||||
attention_active=false
|
||||
else
|
||||
created="$(jq -r '.created_at' <<<"$ISSUE_JSON")"
|
||||
guarded_read age last_issue_activity "$n" "$created" \
|
||||
|
|
@ -526,6 +531,7 @@ The merge releases the claim; no builder owes a draft. Triage owes completion in
|
|||
fi
|
||||
log "#$n: stale claim reclaimed -> ready" ;;
|
||||
esac
|
||||
[ "$decision" != FLAG_UNASSIGNED ] || attention_suppression=claimed-unassigned
|
||||
if has_issue_label offsite; then
|
||||
local timeline
|
||||
if timeline="$(offsite_timeline "$n")"; then
|
||||
|
|
@ -546,6 +552,7 @@ The merge releases the claim; no builder owes a draft. Triage owes completion in
|
|||
'This `post-merge` issue has an assignee or `attention`. The sweep will not undo hand-set intent; triage must clear the invalid composition or move the issue back into buildable queue state.'
|
||||
log "#$n: assigned or attention-bearing post-merge issue flagged"
|
||||
fi
|
||||
attention_suppression=post-merge-assigned
|
||||
elif has_issue_label blocked; then
|
||||
refs="$(blocked_references <<<"$(jq -r '.body // ""' <<<"$ISSUE_JSON")")"
|
||||
cross_refs="$(blocked_cross_references <<<"$(jq -r '.body // ""' <<<"$ISSUE_JSON")")"
|
||||
|
|
@ -574,6 +581,14 @@ The merge releases the claim; no builder owes a draft. Triage owes completion in
|
|||
fi
|
||||
fi
|
||||
|
||||
# The flag composes with every build queue state, but requires an assignee.
|
||||
# Existing post-merge/claimed diagnostics take precedence so one board bug
|
||||
# draws one comment (#232 D5); the shared helper still logs the suppression.
|
||||
if [ "$attention_active" = true ] && has_issue_label attention; then
|
||||
[ -n "${assignees:-}" ] || assignees="$(jq '.assignees | length' <<<"$ISSUE_JSON")"
|
||||
reconcile_attention "$n" issue "$assignees" "$attention_suppression"
|
||||
fi
|
||||
|
||||
# ---- the ruling invariants (#52), on any queue state ----
|
||||
# The flag composes with the queue labels (#50 D8), so this runs after the
|
||||
# queue branches rather than inside one of them. The FLAG_CONFLICT return
|
||||
|
|
|
|||
|
|
@ -74,6 +74,9 @@ SELF_WORKFLOW="${SELF_WORKFLOW:-${GITHUB_WORKFLOW:-}}"
|
|||
# The needs-ruling invariants (#52) — one implementation for both surfaces.
|
||||
# shellcheck source=lib/ruling.sh
|
||||
. "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/../../lib/ruling.sh"
|
||||
# The attention target invariants (#232) — diagnosis only, both surfaces.
|
||||
# shellcheck source=lib/attention.sh
|
||||
. "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/../../lib/attention.sh"
|
||||
# The guarded read and its reason line (#101) — one implementation for both
|
||||
# surfaces. read_failure_reason lived here until the issue surface needed the
|
||||
# identical rule (#247); a second copy of it is the failure lib/ruling.sh's
|
||||
|
|
@ -921,6 +924,12 @@ reconcile_pr() { # $1 = PR number; relies on the globals set from its fetch
|
|||
if has_label needs-ruling; then
|
||||
reconcile_ruling "$n" "$last_activity_epoch" "$NOW"
|
||||
fi
|
||||
|
||||
# `attention` belongs on the assigned issue that owns the claim, never on
|
||||
# a pull request (#232). Behind the label gate so ordinary PRs pay no read.
|
||||
if has_label attention; then
|
||||
reconcile_attention "$n" pr "$(jq '.assignees | length' <<<"$PR_JSON")" ""
|
||||
fi
|
||||
}
|
||||
|
||||
main() {
|
||||
|
|
|
|||
5
changelog.d/232.md
Normal file
5
changelog.d/232.md
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
### Added
|
||||
|
||||
- The label and issue-flow sweeps now comment once per episode when
|
||||
`attention` targets a pull request or an unassigned issue, without
|
||||
retargeting the demand or changing labels or assignees (#232).
|
||||
97
lib/attention.sh
Normal file
97
lib/attention.sh
Normal file
|
|
@ -0,0 +1,97 @@
|
|||
#!/usr/bin/env bash
|
||||
# lib/attention.sh — the `attention` target invariants (#232, epic #229).
|
||||
#
|
||||
# Both reconcilers source this file. Pure decisions sit above the divider;
|
||||
# the impure orchestrator below reads the current label episode and comments
|
||||
# through the sourcing script's run()/log(). The machine diagnoses only: it
|
||||
# never sets, clears, retargets or assigns anything on the strength of these
|
||||
# checks (#229 D2).
|
||||
|
||||
ATTENTION_MARKER_PREFIX='<!-- ceremony:attention-malformed:'
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Pure decisions. Facts in, verdict out. No gh, no clock.
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
attention_target_decision() { # $1 pr|issue, $2 assignee count → MALFORMED_* | KEEP
|
||||
case "$1" in
|
||||
pr) echo MALFORMED_PR ;;
|
||||
issue)
|
||||
if [ "$2" -eq 0 ]; then echo MALFORMED_UNASSIGNED; else echo KEEP; fi ;;
|
||||
*) return 2 ;;
|
||||
esac
|
||||
}
|
||||
|
||||
attention_comment_decision() { # $1 target verdict, $2 suppression → POST | SUPPRESS | KEEP
|
||||
case "$1" in
|
||||
KEEP) echo KEEP ;;
|
||||
MALFORMED_UNASSIGNED)
|
||||
if [ -n "$2" ]; then echo SUPPRESS; else echo POST; fi ;;
|
||||
MALFORMED_PR) echo POST ;;
|
||||
*) return 2 ;;
|
||||
esac
|
||||
}
|
||||
|
||||
attention_newest_flag() { # labeled-event ISO-8601 timestamps on stdin → newest
|
||||
sort | tail -n1
|
||||
}
|
||||
|
||||
attention_episode_marker() { # $1 current episode's labeled timestamp
|
||||
printf '%s%s -->\n' "$ATTENTION_MARKER_PREFIX" "$1"
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# The impure orchestrator. Called only behind a has-attention gate.
|
||||
# Needs REPO; uses the caller's run() and log().
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
reconcile_attention() { # $1 item, $2 pr|issue, $3 assignees, $4 suppression
|
||||
local n="$1" surface="$2" assignees="$3" suppression="${4:-}"
|
||||
local target comment labeled_events labeled_at marker comments body
|
||||
: "${REPO:?reconcile_attention: REPO is required}"
|
||||
|
||||
target="$(attention_target_decision "$surface" "$assignees")"
|
||||
comment="$(attention_comment_decision "$target" "$suppression")"
|
||||
[ "$comment" != KEEP ] || return 0
|
||||
|
||||
if [ "$comment" = SUPPRESS ]; then
|
||||
log "#$n: malformed attention detected; comment suppressed by $suppression precedence"
|
||||
return 0
|
||||
fi
|
||||
|
||||
if ! labeled_events="$(gh api --paginate "repos/$REPO/issues/$n/timeline" \
|
||||
--jq '.[] | select(.event == "labeled" and .label.name == "attention")
|
||||
| .created_at' 2>/dev/null)"; then
|
||||
log "#$n: attention timeline unreadable — no verdict invented this pass"
|
||||
return 0
|
||||
fi
|
||||
if [ -z "$labeled_events" ]; then
|
||||
log "#$n: attention flag has no visible labeled event — no verdict invented this pass"
|
||||
return 0
|
||||
fi
|
||||
labeled_at="$(attention_newest_flag <<<"$labeled_events")"
|
||||
marker="$(attention_episode_marker "$labeled_at")"
|
||||
|
||||
if ! comments="$(gh api --paginate "repos/$REPO/issues/$n/comments" \
|
||||
--jq '.[].body // ""' 2>/dev/null)"; then
|
||||
log "#$n: attention comments unreadable — no verdict invented this pass"
|
||||
return 0
|
||||
fi
|
||||
grep -qF "$marker" <<<"$comments" && return 0
|
||||
|
||||
case "$target" in
|
||||
MALFORMED_PR)
|
||||
body="$marker
|
||||
This pull request carries \`attention\`, but that label is issue-only. Put
|
||||
the label on the assigned issue that owns the claim. The sweep cannot infer
|
||||
which issue that is, so it reports the malformed target without removing or
|
||||
retargeting the label." ;;
|
||||
MALFORMED_UNASSIGNED)
|
||||
body="$marker
|
||||
This issue carries \`attention\` but has no assignee to receive the demand.
|
||||
Assign the intended builder or remove the flag. The sweep reports the board
|
||||
bug without assigning anyone or changing the label." ;;
|
||||
esac
|
||||
run gh issue comment "$n" -R "$REPO" --body "$body" >/dev/null
|
||||
log "#$n: malformed attention ($surface) — commented; no label or assignee changed"
|
||||
}
|
||||
47
test/attention.test.sh
Normal file
47
test/attention.test.sh
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
#!/usr/bin/env bash
|
||||
set -u
|
||||
|
||||
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
# shellcheck source=test/harness.sh
|
||||
source "$ROOT/test/harness.sh"
|
||||
# shellcheck source=lib/attention.sh
|
||||
source "$ROOT/lib/attention.sh"
|
||||
|
||||
check "attention on an unassigned PR is malformed" 0 "MALFORMED_PR" \
|
||||
attention_target_decision pr 0
|
||||
check "attention on an assigned PR is still malformed" 0 "MALFORMED_PR" \
|
||||
attention_target_decision pr 1
|
||||
check "attention on an unassigned issue is malformed" 0 "MALFORMED_UNASSIGNED" \
|
||||
attention_target_decision issue 0
|
||||
check "attention on an assigned issue is healthy" 0 "KEEP" \
|
||||
attention_target_decision issue 1
|
||||
check "an unknown surface is rejected" 2 "" attention_target_decision discussion 0
|
||||
|
||||
check "a malformed PR target is commented" 0 "POST" \
|
||||
attention_comment_decision MALFORMED_PR ""
|
||||
check "an unassigned issue target is commented without precedence" 0 "POST" \
|
||||
attention_comment_decision MALFORMED_UNASSIGNED ""
|
||||
check "claimed-unassigned precedence suppresses the second comment" 0 "SUPPRESS" \
|
||||
attention_comment_decision MALFORMED_UNASSIGNED claimed-unassigned
|
||||
check "post-merge-assigned precedence suppresses the second comment" 0 "SUPPRESS" \
|
||||
attention_comment_decision MALFORMED_UNASSIGNED post-merge-assigned
|
||||
check "a healthy target stays silent" 0 "KEEP" \
|
||||
attention_comment_decision KEEP ""
|
||||
|
||||
check "the newest labeled event defines the episode" 0 "2026-08-03T12:00:00Z" \
|
||||
attention_newest_flag <<'EOF'
|
||||
2026-08-03T10:00:00Z
|
||||
2026-08-03T12:00:00Z
|
||||
2026-08-03T11:00:00Z
|
||||
EOF
|
||||
check "the marker names the label episode" 0 \
|
||||
'<!-- ceremony:attention-malformed:2026-08-03T12:00:00Z -->' \
|
||||
attention_episode_marker 2026-08-03T12:00:00Z
|
||||
|
||||
# Diagnosis is the only write this library may own. Pin the absence of every
|
||||
# label/assignee mutation spelling so a later refactor cannot quietly turn a
|
||||
# report into a repair (#229 D2).
|
||||
check "the attention library contains no issue/PR edit mutation" 1 "" \
|
||||
grep -E 'gh (issue|pr) edit|--(add|remove)-(label|assignee)' "$ROOT/lib/attention.sh"
|
||||
|
||||
summary
|
||||
|
|
@ -393,6 +393,95 @@ issue_probe() { # $1 issue, $2 labels, $3 assignees, $4 false|closing|refs, $5 m
|
|||
tfix() { printf '%s/repos_owner_repo_issues_%s_timeline.json' "$TMP" "$1"; }
|
||||
cfix() { printf '%s/repos_owner_repo_issues_%s_comments.json' "$TMP" "$1"; }
|
||||
|
||||
# -- malformed attention targets are diagnosed, never repaired (#232) -------
|
||||
attention_episode() { # $1 issue, $2 labeled timestamp
|
||||
jq -n --arg at "$2" \
|
||||
'[{"event":"labeled","label":{"name":"attention"},"actor":{"login":"setter"},"created_at":$at}]' \
|
||||
>"$(tfix "$1")"
|
||||
printf '[]\n' >"$(cfix "$1")"
|
||||
}
|
||||
|
||||
: >"$TMP/issue-edits"
|
||||
attention_edits_before="$(wc -l <"$TMP/issue-edits")"
|
||||
for n in 61 62 63; do
|
||||
attention_episode "$n" "$(iso_at $((INOW - 60)))"
|
||||
done
|
||||
# The assignment event is the claim clock's own activity fact. The live issue
|
||||
# has since lost its assignee, which is exactly the claimed-unassigned shape.
|
||||
jq --arg at "$(iso_at $((INOW - 60)))" \
|
||||
'. + [{"event":"assigned","created_at":$at}]' \
|
||||
"$(tfix 63)" >"$(tfix 63).tmp" && mv "$(tfix 63).tmp" "$(tfix 63)"
|
||||
printf '{"state":"open"}\n' >"$TMP/repos_owner_repo_issues_999.json"
|
||||
|
||||
issue_probe 61 $'ready\nattention' 0 >/dev/null
|
||||
check "unassigned attention under ready is diagnosed once" 0 "1" \
|
||||
grep -cF '<!-- ceremony:attention-malformed:' "$TMP/posted-61"
|
||||
|
||||
issue_probe 62 $'blocked\nattention' 0 false "" 'Blocked by #999' >/dev/null
|
||||
check "unassigned attention under blocked is diagnosed once" 0 "1" \
|
||||
grep -cF '<!-- ceremony:attention-malformed:' "$TMP/posted-62"
|
||||
|
||||
claimed_attention="$(issue_probe 63 $'claimed\nattention' 0)"
|
||||
check "claimed-unassigned owns the only board comment" 0 "1" \
|
||||
grep -c -- '^----$' "$TMP/posted-63"
|
||||
check "the claimed-unassigned marker, not attention's, was posted" 0 "1" \
|
||||
grep -cF '<!-- issueflow:claimed-unassigned -->' "$TMP/posted-63"
|
||||
check "the suppressed attention detection remains in the log" 0 "1" \
|
||||
grep -cF 'comment suppressed by claimed-unassigned precedence' <<<"$claimed_attention"
|
||||
|
||||
attention_episode 64 "$(iso_at $((INOW - 60)))"
|
||||
issue_probe 64 $'ready\nattention' 1 >/dev/null
|
||||
check "assigned attention under ready is healthy" 1 "" test -f "$TMP/posted-64"
|
||||
attention_episode 65 "$(iso_at $((INOW - 60)))"
|
||||
issue_probe 65 $'claimed\nattention' 1 true >/dev/null
|
||||
check "assigned attention under claimed is healthy" 1 "" test -f "$TMP/posted-65"
|
||||
attention_episode 66 "$(iso_at $((INOW - 60)))"
|
||||
issue_probe 66 $'blocked\nattention' 1 false "" 'Blocked by #999' >/dev/null
|
||||
check "assigned attention under blocked is healthy" 1 "" test -f "$TMP/posted-66"
|
||||
|
||||
attention_episode 67 "$(iso_at $((INOW - 60)))"
|
||||
post_merge_attention="$(issue_probe 67 $'post-merge\nattention' 0)"
|
||||
check "post-merge precedence leaves exactly its existing comment" 0 "1" \
|
||||
grep -c -- '^----$' "$TMP/posted-67"
|
||||
check "post-merge's existing diagnostic wins" 0 "1" \
|
||||
grep -cF '<!-- issueflow:post-merge-assigned -->' "$TMP/posted-67"
|
||||
check "the post-merge suppression remains in the log" 0 "1" \
|
||||
grep -cF 'comment suppressed by post-merge-assigned precedence' <<<"$post_merge_attention"
|
||||
|
||||
attention_episode 68 "$(iso_at $((INOW - 120)))"
|
||||
issue_probe 68 $'ready\nattention' 0 >/dev/null
|
||||
issue_probe 68 $'ready\nattention' 0 >/dev/null
|
||||
check "two sweeps in one malformed episode post once" 0 "1" \
|
||||
grep -cF '<!-- ceremony:attention-malformed:' "$TMP/posted-68"
|
||||
jq --arg at "$(iso_at $((INOW - 30)))" \
|
||||
'. + [{"event":"labeled","label":{"name":"attention"},"actor":{"login":"setter"},"created_at":$at}]' \
|
||||
"$(tfix 68)" >"$(tfix 68).tmp" && mv "$(tfix 68).tmp" "$(tfix 68)"
|
||||
issue_probe 68 $'ready\nattention' 0 >/dev/null
|
||||
check "a re-set flag creates a second episode comment" 0 "2" \
|
||||
grep -cF '<!-- ceremony:attention-malformed:' "$TMP/posted-68"
|
||||
# shellcheck disable=SC2016 # positional parameter belongs to the isolated shell
|
||||
check "the two comments carry distinct episode markers" 0 "2" \
|
||||
bash -c 'grep -F "<!-- ceremony:attention-malformed:" "$1" | sort -u | wc -l' _ \
|
||||
"$TMP/posted-68"
|
||||
|
||||
: >"$(tfix 69).error"
|
||||
printf '[]\n' >"$(cfix 69)"
|
||||
unreadable_attention="$(issue_probe 69 $'ready\nattention' 0)"
|
||||
check "an unreadable attention timeline posts nothing" 1 "" test -f "$TMP/posted-69"
|
||||
check "the unreadable fact is visible in the log" 0 "1" \
|
||||
grep -cF 'attention timeline unreadable' <<<"$unreadable_attention"
|
||||
|
||||
: >"$TMP/api-calls"
|
||||
printf '[]\n' >"$(tfix 70)"
|
||||
printf '[]\n' >"$(cfix 70)"
|
||||
issue_probe 70 ready 0 >/dev/null
|
||||
check "a flag-free issue performs no attention episode read" 1 "" \
|
||||
grep -qF 'repos/owner/repo/issues/70/timeline' "$TMP/api-calls"
|
||||
|
||||
# shellcheck disable=SC2016 # positional parameter belongs to the isolated shell
|
||||
check "the attention sweep probes perform no issue edits" 0 "$attention_edits_before" \
|
||||
bash -c 'wc -l <"$1"' _ "$TMP/issue-edits"
|
||||
|
||||
# -- the reclaim clock stops under a pending ruling (48h quiet, no PR) -------
|
||||
jq -n --arg l "$(iso_at $((INOW - 10 * 86400)))" \
|
||||
'[{"event":"labeled","label":{"name":"needs-ruling"},"actor":{"login":"setter"},"created_at":$l},
|
||||
|
|
|
|||
|
|
@ -701,19 +701,24 @@ trap 'rm -rf "$RTMP"' EXIT
|
|||
iso_at() { date -u -d "@$1" +%Y-%m-%dT%H:%M:%SZ; }
|
||||
RNOW=2000000000
|
||||
|
||||
ruling_sweep_probe() { # $1 = the PR's labels → reconcile_pr's log lines
|
||||
ruling_sweep_probe() { # $1 labels, $2 PR, $3 assignees, $4 requested, $5 activity age days
|
||||
(
|
||||
local n="${2:-77}" assignees="${3:-0}" requested="${4:-}"
|
||||
local activity_days="${5:-8}"
|
||||
local assignee_json='[]'
|
||||
[ "$assignees" -eq 0 ] || assignee_json='[{"login":"owner-bot"}]'
|
||||
REPO_LABELS="$(printf 'state:addressing\nstate:needs-human\nmerge-next\nstale\nneeds-ruling')"
|
||||
REPO=owner/repo NOW="$RNOW"
|
||||
LABELS="$1"
|
||||
DRAFT=false HEAD_SHA=head1 REQUESTED=""
|
||||
DRAFT=false HEAD_SHA=head1 REQUESTED="$requested"
|
||||
# Approvals submitted 8 days ago — the newest real activity anywhere.
|
||||
REVIEWS_JSON="$(reviews \
|
||||
"$(rev "$BOT1" APPROVED head1 "" "$(iso_at $((RNOW - 8 * 86400)))")" \
|
||||
"$(rev "$BOT2" APPROVED head1 "" "$(iso_at $((RNOW - 8 * 86400)))")" \
|
||||
"$(rev "$BOT3" APPROVED head1 "" "$(iso_at $((RNOW - 8 * 86400)))")")"
|
||||
"$(rev "$BOT1" APPROVED head1 "" "$(iso_at $((RNOW - activity_days * 86400)))")" \
|
||||
"$(rev "$BOT2" APPROVED head1 "" "$(iso_at $((RNOW - activity_days * 86400)))")" \
|
||||
"$(rev "$BOT3" APPROVED head1 "" "$(iso_at $((RNOW - activity_days * 86400)))")")"
|
||||
MERGEABLE=MERGEABLE CHECKS=SUCCESS
|
||||
PR_JSON="$(jq -n --arg at "$(iso_at $((RNOW - 10 * 86400)))" '{created_at: $at}')"
|
||||
PR_JSON="$(jq -n --arg at "$(iso_at $((RNOW - 10 * 86400)))" \
|
||||
--argjson assignees "$assignee_json" '{created_at: $at, assignees: $assignees}')"
|
||||
run() { "$@"; } # mutations reach the stub and are recorded, not swallowed
|
||||
gh() {
|
||||
if [ "$1" = api ]; then
|
||||
|
|
@ -728,6 +733,8 @@ ruling_sweep_probe() { # $1 = the PR's labels → reconcile_pr's log lines
|
|||
shift
|
||||
done
|
||||
file="$RTMP/$(printf '%s' "$endpoint" | tr '/' '_').json"
|
||||
printf '%s\n' "$endpoint" >>"$RTMP/api-calls"
|
||||
[ ! -f "$file.error" ] || return 1
|
||||
# A missing fixture is an empty collection — projected through the
|
||||
# caller's --jq exactly like real gh, so '.[].foo' yields no lines.
|
||||
[ -f "$file" ] || { printf '[]\n' | jq -r "${jqexpr:-.}"; return 0; }
|
||||
|
|
@ -749,7 +756,7 @@ ruling_sweep_probe() { # $1 = the PR's labels → reconcile_pr's log lines
|
|||
printf '%s\n' "$*" >>"$RTMP/edits"
|
||||
fi
|
||||
}
|
||||
reconcile_pr 77 2>&1
|
||||
reconcile_pr "$n" 2>&1
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -788,6 +795,60 @@ expect "exactly one nudge across both sweeps" \
|
|||
expect "no label edit across both sweeps names the ruling flag" \
|
||||
no "$(grep -q 'needs-ruling' "$RTMP/edits" 2>/dev/null && echo yes || echo no)"
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# The attention pass on the PR surface (#232): every PR target is malformed,
|
||||
# assigned or not. The episode marker makes the comment once-per-labeling;
|
||||
# every other board mutation remains the ordinary state machine's concern.
|
||||
# ---------------------------------------------------------------------------
|
||||
attention_pr_fixture() { # $1 PR, $2 labeled timestamp
|
||||
jq -n --arg at "$2" \
|
||||
'[{"event":"labeled","label":{"name":"attention"},"actor":{"login":"setter"},"created_at":$at}]' \
|
||||
>"$RTMP/repos_owner_repo_issues_${1}_timeline.json"
|
||||
printf '[]\n' >"$RTMP/repos_owner_repo_issues_${1}_comments.json"
|
||||
}
|
||||
|
||||
attention_pr_fixture 78 "$(iso_at $((RNOW - 120)))"
|
||||
attention_mutations_before="$(wc -l <"$RTMP/edits")"
|
||||
attention_pr="$(ruling_sweep_probe $'attention\nstate:needs-human' 78 0 danmt 1)"
|
||||
expect "attention on an unassigned PR is diagnosed" yes \
|
||||
"$(grep -q 'malformed attention (pr)' <<<"$attention_pr" && echo yes || echo no)"
|
||||
expect "the PR comment points to the assigned claim issue" yes \
|
||||
"$(grep -qF 'assigned issue that owns the claim' "$RTMP/posted-78" && echo yes || echo no)"
|
||||
expect "the PR comment does not guess a target issue number" no \
|
||||
"$(grep -Eq '#[0-9]+' "$RTMP/posted-78" && echo yes || echo no)"
|
||||
ruling_sweep_probe $'attention\nstate:needs-human' 78 0 danmt 1 >/dev/null
|
||||
expect "two PR sweeps in one attention episode post once" 1 \
|
||||
"$(grep -cF '<!-- ceremony:attention-malformed:' "$RTMP/posted-78")"
|
||||
jq --arg at "$(iso_at $((RNOW - 30)))" \
|
||||
'. + [{"event":"labeled","label":{"name":"attention"},"actor":{"login":"setter"},"created_at":$at}]' \
|
||||
"$RTMP/repos_owner_repo_issues_78_timeline.json" \
|
||||
>"$RTMP/repos_owner_repo_issues_78_timeline.json.tmp" \
|
||||
&& mv "$RTMP/repos_owner_repo_issues_78_timeline.json.tmp" \
|
||||
"$RTMP/repos_owner_repo_issues_78_timeline.json"
|
||||
ruling_sweep_probe $'attention\nstate:needs-human' 78 0 danmt 1 >/dev/null
|
||||
expect "a re-set PR flag receives a second episode comment" 2 \
|
||||
"$(grep -cF '<!-- ceremony:attention-malformed:' "$RTMP/posted-78")"
|
||||
|
||||
attention_pr_fixture 79 "$(iso_at $((RNOW - 60)))"
|
||||
ruling_sweep_probe $'attention\nstate:needs-human' 79 1 danmt 1 >/dev/null
|
||||
expect "attention on an assigned PR is still diagnosed" 1 \
|
||||
"$(grep -cF '<!-- ceremony:attention-malformed:' "$RTMP/posted-79")"
|
||||
|
||||
attention_pr_fixture 80 "$(iso_at $((RNOW - 60)))"
|
||||
: >"$RTMP/repos_owner_repo_issues_80_timeline.json.error"
|
||||
unreadable_attention="$(ruling_sweep_probe $'attention\nstate:needs-human' 80 0 danmt 1)"
|
||||
expect "an unreadable PR attention timeline posts nothing" no \
|
||||
"$([ -f "$RTMP/posted-80" ] && echo yes || echo no)"
|
||||
expect "the unreadable fact is logged without a verdict" yes \
|
||||
"$(grep -qF 'attention timeline unreadable' <<<"$unreadable_attention" && echo yes || echo no)"
|
||||
|
||||
: >"$RTMP/api-calls"
|
||||
ruling_sweep_probe state:needs-human 81 0 danmt 1 >/dev/null
|
||||
expect "a flag-free PR performs no attention timeline read" no \
|
||||
"$(grep -qF 'repos/owner/repo/issues/81/timeline' "$RTMP/api-calls" && echo yes || echo no)"
|
||||
expect "attention diagnosis caused no PR mutation" "$attention_mutations_before" \
|
||||
"$(wc -l <"$RTMP/edits")"
|
||||
|
||||
# -- the sweep wiring observes the existing per-PR skip without writing -------
|
||||
blind_main_probe() {
|
||||
(
|
||||
|
|
|
|||
Loading…
Reference in a new issue