test: cover attention target diagnostics

This commit is contained in:
Andriujose 2026-08-03 19:53:25 +00:00
parent e7750c0c8f
commit a9b3f4d766
6 changed files with 214 additions and 12 deletions

View file

@ -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
View 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).

View file

@ -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
View 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

View file

@ -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},

View file

@ -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() {
(