forked from heavy-duty/ceremony
test: cover attention target diagnostics
This commit is contained in:
parent
e7750c0c8f
commit
a9b3f4d766
6 changed files with 214 additions and 12 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
|
||||
|
|
|
|||
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).
|
||||
|
|
@ -85,12 +85,12 @@ reconcile_attention() { # $1 item, $2 pr|issue, $3 assignees, $4 suppression
|
|||
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 (heavy-duty/ceremony#232)." ;;
|
||||
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 (heavy-duty/ceremony#232)." ;;
|
||||
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,93 @@ 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"
|
||||
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"
|
||||
|
||||
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