Skip to content

test(watcher): pin the shutdown sentinel's position in cleanup() - #3051

Merged
yixuan-ag2 merged 5 commits into
mainfrom
test/watcher-shutdown-sentinel-order
Aug 18, 2026
Merged

yixuan-ag2 merged 5 commits into
mainfrom
test/watcher-shutdown-sentinel-order

Conversation

@yixuan-ag2

Copy link
Copy Markdown
Collaborator

What

#3025 moved the $DISPATCH_DIR/shutting-down write to the top of cleanup(), so the drain and queue guards observe it before any release, kill or sweep can still promote a worker (the #2934 shape). It shipped as a pure shell-ordering change with no test, and the sentinel name appears nowhere under tests/ — so the property can regress silently, and the failure mode is a lost task rather than a red build.

This adds the ordering assertions to tests/watch-tasks-stream-shutdown-emit.test.sh, which already owns this script's shutdown behaviour and already carries the #2934 lineage, rather than starting a new file.

Before / after

At the parent commit the sentinel is untested — the suite passes whether or not the write is first:

$ grep -rl "shutting-down" tests/ src/ scripts/
src/watch-tasks-stream.sh

At this head, with the write moved to just after sentinel_release_if_owner:

  FAIL the shutting-down sentinel precedes sentinel_release_if_owner: expected 'yes', got 'no'
Results: FAILED

Restored:

  ok   cleanup() body is extractable (else every ordering check below is vacuous)
  ok   cleanup() still contains the sentinel anchor
  ok   cleanup() still contains the release anchor
  ok   cleanup() still contains the kill anchor
  ok   cleanup() still contains the fallback anchor
  ok   the shutting-down sentinel precedes sentinel_release_if_owner
  ok   ...precedes the first kill
  ok   ...precedes the handler-fallback sweep
Results: watch-tasks-stream emit diagnostics — all checks passed

Guarded against passing for the wrong reason

Both failure modes this file already worries about apply here, so both are closed explicitly:

  • an empty cleanup() extraction would make every ordering comparison vacuously true, so the extraction is asserted non-empty first;
  • a missing anchor yields an empty variable, and [ "" -lt 5 ] is an error, not a pass — so each anchor's presence is asserted before it is compared.

Scope

Test-only; src/ untouched. review-checks PASS, shellcheck -S error clean, bash -n ok.

This is a follow-up closing a gap in my own merged PR, not new behaviour — reviewers should feel free to treat it as low priority against the rest of the queue.

#3025 moved the `$DISPATCH_DIR/shutting-down` write to the top of
`cleanup()` so the drain and queue guards observe it before any release,
kill or sweep can still promote a worker (#2934). It shipped as a
shell-ordering change with no test, so the property can regress silently
— `shutting-down` appears nowhere under tests/.

Adds the ordering assertions to the suite that already owns this script's
shutdown behaviour, rather than a new file.

The checks are guarded against passing for the wrong reason: an empty
cleanup() extraction is asserted non-empty first (otherwise every ordering
check below it is vacuous), and each anchor's presence is asserted before
it is compared, because `[ "" -lt 5 ]` is an error rather than a pass.

Falsified: moving the sentinel write to just after
`sentinel_release_if_owner` fails the suite with

    FAIL the shutting-down sentinel precedes sentinel_release_if_owner:
         expected 'yes', got 'no'

Restored, all checks pass. review-checks PASS, shellcheck clean, bash -n ok.

@john-the-dev john-the-dev left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed the diff (test-only, +29). The ordering pins are exactly the #3025 invariant — sentinel-first in cleanup() — and the anchor-existence guards make the ordering checks non-vacuous: a renamed anchor fails its own check instead of silently passing the comparison. awk extraction is comment-stripped so the match can't land on prose. Safe; no live path touched.

@yixuan-ag2

Copy link
Copy Markdown
Collaborator Author

The red tsc + tests (clean install) here is the gh pr diff 503 flake, not this branch. Re-ran the failed job.

From the job log (run 32055212191, job 95464473586) — the step never reached tsc or the tests:

Run set -euo pipefail
  gh pr diff 3051 > /tmp/review-pr.diff
  bash scripts/review-checks.sh --diff /tmp/review-pr.diff
HTTP 503: No server is currently available to service your request.
##[error]Process completed with exit code 1.

Review checks (REVIEW.md) failed and every later step (Setup Node, Install, Typecheck, Run tests) shows skipped. So the check name is misleading — nothing was typechecked and no test ran.

Verified it is not a content failure, using main's scanner rather than this branch's copy:

$ git diff $(git merge-base origin/main HEAD) HEAD | wc -l
40                                    # one file: tests/watch-tasks-stream-shutdown-emit.test.sh
$ bash <main>/scripts/review-checks.sh --diff /tmp/pr3051.diff
prose-cap: PASS (comment blocks within cap 2, exts .py)
review-checks: PASS (hardcoded-paths + root-artifacts + prose-cap clean)

And the discriminator that it is not repo-wide: the same check is success on #3041 at its current head.

This is the failure mode #3050 fixes — a fetch failure that lands as a content-gate failure. Worth noting it now costs a full CI cycle per occurrence.

One incidental finding, not from this PR: run main's scanner over git diff origin/main <branch> (the against-main form, which this branch being BEHIND makes large) and it reports packages/ag2-sparrow/ag2_sparrow/remote_gateway_bridge.py:1869 comment block is 3 lines (cap 2). That line is in main, not in this diff — CI's merge-base diff never sees it, so nothing is failing because of it. Flagging only in case the cap is meant to hold for the tree and not just for PR diffs.

@sonichi sonichi left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approving — this adds real coverage where there was none, and the vacuity guards are the part I want to praise before I criticise the approach.

Verified, not assumed: the ordering the test asserts is genuinely true at main's cleanup() — sentinel write at relative line 7, sentinel_release_if_owner 16, first kill -TERM 18, fallback_outstanding_handlers 20. So it passes today for the right reason.

I did not run the file. Its earlier sections spawn watchers and send SIGTERM, and this host has a live task watcher doing real work; a shutdown test against that is not worth the outage. I exercised the new block's logic against the real source by hand instead. Flagging so you know which claim is measured and which is not: the ordering is measured, "the suite still passes end to end" is not — CI is mid-run on a2c71e82 as I write.

The nit, and it is a real one: this pins source ORDER, not behaviour. line_of greps the extracted cleanup() text and compares line numbers, so it passes if the sentinel merely appears first, and fails on a refactor that preserves semantics — extract the kill into a helper and the anchor vanishes even though the guarantee is intact. CLAUDE.md is direct about this shape ("Concurrency tests must call the production writer, not a copied recipe or source-regex surrogate"), and unlike a docs test this one is about a concurrency guarantee (#2934's worker promotion).

Two concrete ways it can go quietly wrong:

  • grep -vE '^[[:space:]]*#' strips only whole-line comments. A trailing comment mentioning shutting-down above the real write would become the anchor and the ordering check would compare the wrong line. Not present today; nothing prevents it.
  • line_of 'kill -TERM' takes the first match. cleanup() also has kill -TERM 0 later; today the order makes that harmless, but the anchor is positional, not semantic.

I am not blocking on it. The behavioural form — run the watcher, SIGTERM it, assert the sentinel exists before any promotion can observe it — is a genuinely hard race to stage, and a guarded source-order check beats no check. Worth a follow-up rather than a rewrite here, and the existence guards you added are exactly what keeps this from being the usual vacuous version of this pattern.

@yixuan-ag2
yixuan-ag2 enabled auto-merge (squash) August 18, 2026 04:42
@github-actions

Copy link
Copy Markdown
Contributor

Coverage Gate

✅ No Python changes — nothing to gate (bar: 95% on changed lines).

@yixuan-ag2
yixuan-ag2 disabled auto-merge August 18, 2026 06:13
@github-actions

Copy link
Copy Markdown
Contributor

@cla-assistant check

@yixuan-ag2
yixuan-ag2 merged commit c127d93 into main Aug 18, 2026
16 checks passed
@yixuan-ag2
yixuan-ag2 deleted the test/watcher-shutdown-sentinel-order branch August 18, 2026 09:10
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants