Skip to content

test: keep composer fixtures Bash 3.2-clean and align relaunch startup waits - #3556

Open
kuan0808 wants to merge 4 commits into
kunchenguid:mainfrom
kuan0808:fm/up-02-test-fixtures-bash32
Open

test: keep composer fixtures Bash 3.2-clean and align relaunch startup waits#3556
kuan0808 wants to merge 4 commits into
kunchenguid:mainfrom
kuan0808:fm/up-02-test-fixtures-bash32

Conversation

@kuan0808

@kuan0808 kuan0808 commented Sep 2, 2026

Copy link
Copy Markdown

Intent

Keep the shared test fixtures runnable on stock macOS Bash 3.2, and give the relaunch startup waits the same 10s budget the rest of that file already uses.

What Changed

  • Rewrote the matrix_herdr_halfblock_rule_bounds_bare_wrap fixture in tests/fm-composer-lib.test.sh to use literal UTF-8 half-block, arrow, and middot glyphs instead of \u escapes that stock macOS Bash 3.2 cannot decode, and pointed the non-vacuousness footer check at the styled screen actually handed to fm_composer_classify_screen.
  • Raised the three startup poll loops in tests/fm-control-relaunch.test.sh from 200 to 1000 iterations of sleep 0.01, matching the 10s budget the rest of that file already uses.
  • Added a stock /bin/bash step to .github/workflows/ci.yml that runs the composer classifier suite and fails unless all 33 ok - lines appear, and recorded the resolved Bash 3.2 failure in docs/fm-test-isolation-proof.md.

Risk Assessment

✅ Low: Test-and-CI-only change that is well-bounded and matches its intent: the composer fixture now carries literal UTF-8 (verified by hexdump, no \u left outside a comment, and no bash-4-only constructs in tests/fm-composer-lib.test.sh, tests/lib.sh, or bin/fm-composer-lib.sh, whose ${!name} indirections are 3.2-safe), the new macos-latest lane executes the real suite under stock /bin/bash 3.2 with a count pin matching the 33 defined-and-invoked tests, the non-vacuity guard now inspects the $screen fixture that is actually classified and is backed by the two explicit fm_composer_row_has_edge assertions so the case cannot silently retire, printf '%b' removal is byte-for-byte neutral on a fixture with no backslashes, and the relaunch waits move from 200x0.01s to 1000x0.01s = the same nominal 10s the file's existing 100x0.1s waits already use.

Testing

Ran the composer classifier suite and the fm-control relaunch suite under this machine's stock /bin/bash 3.2.57: the branch fixture passes all 33 composer cases while the base-commit fixture fails on 3.2 with not ok - a half-block rule row must count as a structural edge, so the Bash 3.2 defect is reproduced before the fix and gone after it; the relaunch suite passes end to end, and a slow-start simulation (trace delivery marker arriving 8s late) shows the old 200-iteration wait aborting with relaunch did not reach trace delivery while the new 1000-iteration/10s wait completes, which is the file's existing 10s convention. The new CI lane was exercised by extracting the composer block from the macos-stock-bash job in ci.yml and running it for real: it passes on this branch and correctly errors when the suite drifts to 32 tests. No failures, flakes or setup problems; temp copies used for the before/after runs were deleted and the worktree is clean.

Evidence: Composer fixture on stock Bash 3.2: fails before the fix, passes after

Source: Composer fixture on stock Bash 3.2: fails before the fix, passes after

$ /bin/bash --version | head -1 GNU bash, version 3.2.57(1)-release (arm64-apple-darwin25) $ /bin/bash tests/fm-composer-lib.test.sh # BEFORE fix (base fixture, stock macOS Bash 3.2) not ok - a half-block rule row must count as a structural edge exit=1 # AFTER (this branch, same interpreter): 33 ok lines, exit 0

$ /bin/bash --version | head -1
GNU bash, version 3.2.57(1)-release (arm64-apple-darwin25)

$ /bin/bash tests/fm-composer-lib.test.sh   # BEFORE fix (base fixture, stock macOS Bash 3.2)
not ok - a half-block rule row must count as a structural edge
exit=1
Evidence: Full composer classifier run under stock Bash 3.2 (33 ok, exit 0)

Source: Full composer classifier run under stock Bash 3.2 (33 ok, exit 0)

ok - fm_composer_classify_content: a bare shell prompt glyph (>/$/%/#) reads unknown, never empty
ok - fm_composer_classify_content: stripped unbordered content is unknown except verified agent glyphs
ok - fm_composer_classify_content: a bare shell prompt carrying a command is not empty
ok - fm_composer_classify_content: a bare prompt glyph inside a bordered composer box reads empty (claude's own idle composer)
ok - fm_composer_classify_content: agent prompt glyphs (❯ claude, › codex, ⟩ muse) read empty bordered or bare
ok - fm_composer_classify_content: an empty composer reads empty
ok - fm_composer_classify_content: idle matching is limited to proven placeholder positions
ok - fm_composer_classify_content: idle matching preserves the caller's case mode
ok - fm_composer_classify_content: real unsubmitted text reads pending (including a popup argument-hint fill)
ok - matrix: claude's ❯+NBSP row reads empty on every profile in both locales (#1988)
ok - matrix: codex's dim hint is empty when styling proves it, unknown (never pending) when it cannot
ok - matrix: muse's ⟩ reads empty everywhere and survives losing the styled-glyph signal
ok - matrix: cursor's reverse-video placeholder remnant reads empty; real typed text stays pending
ok - matrix: herdr half-block rules bound a bare composer's wrap region
ok - matrix: pi's separated composer needs identity + structure; the blank row alone never proves it
ok - matrix: opencode's left-bar composer reads empty everywhere and scans the full active run
ok - matrix: grok's titled bottom border is tolerated as a title, not read as ambiguity
ok - matrix: kimi's bordered shell-glyph box reads empty through the shared owner (spawn's fourth copy retired)
ok - matrix: the real claude-in-zellij --ansi dump reads empty in both locales
ok - strict posture: blank and unidentified rows are unknown, never injectable empty
ok - fm_composer_classify_screen: the bare composer's wrap region stays identified; structure breaks it
ok - fm_composer_classify_screen: a row-leading agent glyph reanchors the live composer
ok - fm_composer_classify_screen: a lower dead shell invalidates only cursorless stale composers
ok - fm_composer_classify_screen: cursorless bare wrap regions participate in verdicts
ok - fm_composer_classify_screen: cursorless containers reject only contiguous unclaimed activity
ok - fm_composer_classify_screen: the bottom-most candidate wins; stale banners cannot
ok - fm_composer_classify_screen: incomplete lower structure invalidates stale boxes
ok - fm_composer_classify_screen: titled bottoms retain full box geometry
ok - fm_composer_classify_screen: a proven box tolerates a bottom-border cursor
ok - fm_composer_extract_selected_content: scopes user content and excludes furniture
ok - fm_composer_queued_enter_verdict: pending + busy returns empty (queued Enter)
ok - fm_composer_queued_enter_verdict: pending + idle/unknown stays pending
ok - fm_composer_queued_enter_verdict: only proven pending is converted
Evidence: Relaunch startup wait: 8s-late start aborts on the old budget, completes on the new 10s budget

Source: Relaunch startup wait: 8s-late start aborts on the old budget, completes on the new 10s budget

$ /bin/bash <relaunch metadata-race test, BASE startup wait: 200 x 0.01s> not ok - relaunch did not reach trace delivery exit=1 $ /bin/bash <same test, THIS BRANCH startup wait: 1000 x 0.01s = the 10s budget this file already uses> ok - fm-control relaunch: delivery and concurrent task metadata publication serialize exit=0

# Slow-start simulation: the fake transport stub needs 8s to reach trace delivery,
# so the relaunch startup marker lands after the old 200-iteration poll budget.
# Only that delay is injected; everything else is the real fm-control relaunch test
# (test_relaunch_serializes_concurrent_durable_metadata_publication).

$ /bin/bash <relaunch metadata-race test, BASE startup wait: 200 x 0.01s>
not ok - relaunch did not reach trace delivery
exit=1

$ /bin/bash <same test, THIS BRANCH startup wait: 1000 x 0.01s = the 10s budget this file already uses>
ok - fm-control relaunch: delivery and concurrent task metadata publication serialize
exit=0
Evidence: New ci.yml macos-stock-bash composer gate executed for real (passes on branch, catches drift)

Source: New ci.yml macos-stock-bash composer gate executed for real (passes on branch, catches drift)

--- run against this branch under /bin/bash 3.2 --- ok - fm_composer_queued_enter_verdict: only proven pending is converted gate exit=0 --- same gate against a drifted suite that reports only 32 tests --- ::error::expected 33 composer classifier tests, got 32 gate exit=1

# Composer block of the real CI step, extracted from .github/workflows/ci.yml
# job: macos-stock-bash   shell: /bin/bash {0}   (stock macOS Bash 3.2)
--- extracted script ---
composer_output=$(/bin/bash tests/fm-composer-lib.test.sh)
printf '%s\n' "$composer_output"
composer_count=$(printf '%s\n' "$composer_output" | grep -c '^ok - ')
[ "$composer_count" -eq 33 ] || {
  echo "::error::expected 33 composer classifier tests, got $composer_count"
  exit 1
}
--- run against this branch under /bin/bash 3.2 ---
ok - fm_composer_queued_enter_verdict: pending + busy returns empty (queued Enter)
ok - fm_composer_queued_enter_verdict: pending + idle/unknown stays pending
ok - fm_composer_queued_enter_verdict: only proven pending is converted
gate exit=0

--- same gate against a drifted suite that reports only 32 tests ---
ok - stub 32
::error::expected 33 composer classifier tests, got 32
gate exit=1
Evidence: Non-vacuity guard now refuses a footer-stripped classified fixture

Source: Non-vacuity guard now refuses a footer-stripped classified fixture

$ /bin/bash tests/fm-composer-lib.test.sh # fixture with its footer removed not ok - fixture lost its footer content exit=1

# Guard check: strip the footer rows below the closing half-block rule from the
# classified fixture. The non-vacuousness guard must refuse the gutted fixture
# instead of passing trivially (the silent-retirement path from review round 1).

$ /bin/bash tests/fm-composer-lib.test.sh   # fixture with its footer removed
not ok - fixture lost its footer content
exit=1
Evidence: fm-control relaunch suite, 51 ok, exit 0

Source: fm-control relaunch suite, 51 ok, exit 0

ok - fm-control relaunch: a same-harness relaunch replaces the agent in the same endpoint and worktree
ok - fm-control relaunch: durable task metadata survives replacement launch publication
ok - fm-control relaunch: delivery and concurrent task metadata publication serialize
ok - fm-control relaunch: disabling tracing clears metadata and pane context
ok - fm-control relaunch: the progress note lands in the instructions the replacement reads
ok - fm-control relaunch: a ship task refuses without the progress note its replacement needs
ok - fm-control relaunch: switching harness is one ordinary relaunch, and the old wiring goes with the old agent
ok - fm-control relaunch: a harness switch resets model and effort unless they are named too
ok - fm-control relaunch: a prefixed recorded harness can switch adapters transactionally
ok - fm-control relaunch: a prefixed command requires an explicit replacement harness
ok - fm-control relaunch: a same-harness relaunch keeps the profile axes it was running with
ok - fm-control relaunch: explicit model and effort win over the recorded ones
ok - fm-control relaunch: refuses to relaunch onto an adapter with no verified mechanics
ok - fm-control relaunch: the retired incarnation's global turn-end token is revoked
ok - fm-control relaunch: wiring cleanup failure refuses replacement arming
ok - fm-control-lib: one owner resolves each harness's turn-end registry entry, and refuses a malformed token
ok - fm-control relaunch: a secondmate relaunch re-resolves its durable configured harness pin
ok - fm-control relaunch: invalid configured effort is ignored before stop
ok - fm-control relaunch: an adapter unverified for this task kind refuses before the agent is stopped
ok - fm-control relaunch: explicit secondmate harness resets unnamed profile axes
ok - fm-control relaunch: a ship task keeps its recorded harness instead of re-reading crew config
ok - fm-spawn --relaunch: with no explicit harness it reuses the task's recorded one, never the crew default
ok - fm-spawn --relaunch: wiring armed under a prefixed harness name is still retired
ok - fm-spawn --relaunch: switching away from muse retires its session binding
ok - fm-spawn --relaunch: switching away from cursor retires its session binding
ok - fm-control relaunch: an unaccountable local copy refuses before the agent is touched
ok - fm-control relaunch: a worker with nothing to work from is never launched
ok - fm-control relaunch: a refusal before the agent is stopped leaves the durable record untouched
ok - fm-control relaunch: checkpoint inspection failures refuse before stopping
ok - fm-control relaunch: a launch failure after the stop keeps the prior record and reports the real state
ok - fm-control relaunch: unpublished rollback keeps concurrent durable metadata
ok - fm-control relaunch: post-publication failure keeps the new durable record
ok - fm-control relaunch: partial stop reconciles actual agent state
ok - fm-control relaunch: failed journal replacement preserves durable phase
ok - fm-spawn relaunch: prepublication abort removes replacement state
ok - fm-control relaunch: the checkpoint records the exact unlanded work it preserved
ok - fm-control relaunch: a secondmate's child work is accounted for and its charter is left alone
ok - fm-control relaunch: a secondmate home that is not this secondmate's is refused
ok - fm-control relaunch: unreadable and untraversable child state fails checkpoint
ok - fm-control relaunch: two control actions on one task serialize instead of interleaving
ok - fm-spawn relaunch: direct entry participates in lifecycle serialization
ok - fm-promote: promotion participates in lifecycle serialization
ok - fm-spawn --relaunch: refuses to launch a second agent into a live endpoint
ok - fm-spawn --relaunch: symlinked records refuse before inspection
ok - fm-spawn --relaunch: keeps its early meta lock continuous
ok - fm-spawn --relaunch: pending closes refuse before replacement begins
ok - fm-spawn --relaunch: every identity axis comes from the record, and a contradicting flag refuses
ok - fm-spawn --relaunch: an unrecorded task is refused
ok - fm-spawn --relaunch: refuses to start a replacement outside the copy holding the work
ok - relaunch re-reads the backlog item instead of blindly re-running the transition
ok - relaunch heals an item that drifted out of In flight while the task stayed live

Pipeline

Updates from git push no-mistakes

✅ **intent** - passed

✅ No issues found.

✅ **Rebase** - passed

✅ No issues found.

⚠️ **Review** - 1 info
  • ℹ️ tests/fm-composer-lib.test.sh:281 - esc=$(printf &#39;\033&#39;) re-derives a value the file already owns: ESC=$(printf &#39;\033&#39;) at line 155, used by 11 other fixtures in this same file (lines 196, 213, 231, 232, 256, 339, 398, 443, ...). The fix round renamed the base commit's leaked global ESC_LOCAL to a proper local, which was the right direction, but the simplest form is to drop esc from the local list on line 275, delete line 281, and use ${ESC} in the fixture on line 282. Byte-identical output, two fewer lines, one fewer name for the same escape byte.
✅ **Test** - passed

✅ No issues found.

  • /bin/bash tests/fm-composer-lib.test.sh on stock macOS Bash 3.2.57 — 33 ok - lines, exit 0
  • Pre-fix reproduction: git show 5fb0ce7:tests/fm-composer-lib.test.sh run under the same /bin/bash 3.2 → not ok - a half-block rule row must count as a structural edge, exit 1
  • /bin/bash tests/fm-control-relaunch.test.sh — 51 ok - lines, exit 0 (~2m35s)
  • Slow-start behavior check: real test_relaunch_serializes_concurrent_durable_metadata_publication with an 8s delay injected into the fake transport stub, run at the base 200-iteration wait (fails relaunch did not reach trace delivery) vs this branch's 1000-iteration/10s wait (passes)
  • CI consumer check: parsed .github/workflows/ci.yml with a YAML loader, extracted the composer block from job macos-stock-bash (shell /bin/bash {0}), ran it against this branch (exit 0) and against a stubbed 32-test suite (::error::expected 33 composer classifier tests, got 32, exit 1)
  • Guard non-vacuity check: stripped the footer rows below the closing half-block rule from the classified $screen fixture → not ok - fixture lost its footer content, exit 1
  • /bin/bash -n syntax check of tests/fixtures.sh, tests/lib.sh and both changed test files under Bash 3.2
✅ **Document** - passed

✅ No issues found.

✅ **Lint** - passed

✅ No issues found.

✅ **Push** - passed

✅ No issues found.

@greptile-apps

greptile-apps Bot commented Sep 2, 2026

Copy link
Copy Markdown

Confidence Score: 5/5

The PR appears safe to merge.

No blocking failure remains.

Reviews (3): Last reviewed commit: "no-mistakes(document): Correct stale sto..." | Re-trigger Greptile

@kunchenguid

Copy link
Copy Markdown
Owner

Speaking as Kun's firstmate:

HEAD 5db05f1e575185a75687cd64bcea06bc5e831985 — MERGEABLE/BLOCKED (required checks not yet green; first-time fork CI was pending approval). Fork PR (kuan0808, maintainerCanModify). Author not blocked. Created today; not 14-day stale.

Attestation: MISSING — no head-bound no-mistakes ## Pipeline section in body or comments. NM 33687534441 FAILURE ("This PR was not raised through no-mistakes"); duplicate edited-event NM 33687568150 also FAILURE. CI 33687534470 approved this pass and is in progress.

Diff vs body: the body describes a tests/fm-gotmp.test.sh sibling-list repair. The fetched files and the actual diff do not include that file. This PR touches only .github/workflows/ci.yml (+10), tests/fm-composer-lib.test.sh, and tests/fm-control-relaunch.test.sh. The gotmp fixture work lives in #3568 (first commit). Title leftover; not a reason to close. Keep both PRs open until one actually lands and the other is subsumed.

Security (ci.yml, first-class): the workflow delta only extends the existing macos-stock-bash job ("Stock macOS Bash snapshot compatibility") with the same /bin/bash tests/….test.sh + ok - count pattern already used for snapshot/bearings. Job name unchanged. No pull_request_target, no workflow_run, no new actions, no pin changes (actions/checkout@v6 already on main), no secrets, permissions: contents: read unchanged, no weakened gates, no proposed-head script exec beyond the job's existing checkout-and-run-tests. Not flagged-malicious. Fork workflows approved after that review.

Contract-class: restorebin/ untouched. On main (d22318ea) the half-block fixture still uses Bash 4.2+ \u escapes, so stock macOS /bin/bash 3.2 feeds the classifier the literal ASCII \u2580 and the already-correct edge rule fails; Linux CI is Bash 5, which is why #2238 merged green. Literal UTF-8 glyphs match every other fixture in the file (33 invocations; the new job step requires 33 ok - lines). The four relaunch rendezvous waits move from 2s (200×10ms) to 10s (1000×10ms) so the cases pin behavior rather than laptop-load timing. Not a product default change.

VISION (per rule, evidence = test + ci.yml diffs vs main d22318ea):

  1. One captain, one interface — aligns (no captain-facing surface change).
  2. Authority is explicit and never inferred — aligns (no autonomy/consent widening).
  3. Scripts own the mechanics, agents own the judgment — aligns (fixture encoding + wait budget; classifier unchanged).
  4. A restart is a non-event — aligns (no durable-state change).
  5. Delegation with a spine — aligns (tests actually pin the promised behavior).
  6. The fleet outlives any vendor — aligns (Bash 3.2-clean fixtures; macOS stock-bash CI coverage).
  7. Scope — aligns (CI belongs to the forge; field/CI incident → regression coverage). Closing: aligns.

This is waiting on you (the author), not a captain decision. Please git push no-mistakes so the PR body gets a head-bound attestation for the current HEAD (or the tip that push creates). Fork CI/NM workflows for this HEAD were approved after diff review (run ids 33687534470, 33687534441, 33687568150). Once attestation MATCH + NM green + CI green, this restore can auto-merge.

workflow-approvals this pass: 33687534470, 33687534441, 33687568150

tests/fm-composer-lib.test.sh fails on stock macOS /bin/bash 3.2 with
"a half-block rule row must count as a structural edge". The classifier is
fine: bin/fm-composer-lib.sh already treats the upper and lower half-block
rows as edges. The fixture wrote those glyphs as \u escapes inside printf
and $'...', and Bash 3.2 has no \u escape (added in 4.2), so the classifier
received the literal escape text and no edge matched. Linux CI runs Bash 5,
which is why this merged green in kunchenguid#2238.

- Write the half-block fixture with literal UTF-8 glyphs, matching every
  other fixture in the file, and say why in a comment.
- Run tests/fm-composer-lib.test.sh in the existing macos-stock-bash CI job
  under real /bin/bash 3.2, so a Bash 4+-only fixture cannot land again.

No classifier behavior changes.
Four relaunch cases waited at most 2s (200 x 10ms) for a fixture to signal
that a background control run had reached its rendezvous point. Under suite
load on a laptop that is not enough, and the case then failed on timing
rather than on the behavior it pins. The rest of this file already budgets
10s for the same kind of wait; use it here too.
@kuan0808
kuan0808 force-pushed the fm/up-02-test-fixtures-bash32 branch from 5db05f1 to c0bdf43 Compare September 2, 2026 23:45
@kuan0808 kuan0808 changed the title test: keep fixtures Bash 3.2-clean and give the teardown fixture its real siblings test: keep composer fixtures Bash 3.2-clean and align relaunch startup waits Sep 2, 2026
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.

2 participants