live: heartbeat sync 2026-10-19T15:24:41-07:00 (Refs #11)

This commit is contained in:
Frank committed 2026-10-19 15:24:41 -07:00
1 parent 6e3e5594eb
commit c14c64d6c0
12 files changed
+452 -129

No files matched your search

@@ -1,44 +1,44 @@
---
name: dankvoice-bats-serialization
description: "Run dankvoice bats suites without hangs: serialize builds on shared dist/, DV_HOLD is seconds, stub needs sleep&wait trap before hold, timing tests assert CALLS/at not tail PLAYS."
description: "Run dankvoice bats suites without hangs or false failures: serialize dist rebuilds, never edit src mid-run, and rebuild before committing the tracked bundle."
---
# Dankvoice bats: serialization and hang-proofing
Use when running the dankvoice `bats tests/` suite on the shared box, when a suite run hangs or times out, or when the "committed dist is byte-identical to a fresh build" pin test fails after a commit.
## Serialize builds on the shared artifact
`tests/behavior.bats` setup runs `bun run build` into the repo's shared `dist/index.js`
before EVERY test (`DV_EXT="$REPO/dist/index.js"`). Any second concurrent bats run,
`bun run build`, or `bun run typecheck` racing the same repo contends on that file and
can stall single tests past 60 s. NEVER run two bats processes (or bats + build) against
`~/Projects/dankvoice` at the same time — including harness background jobs. If a suite
gets backgrounded, kill it before launching any other repo command; re-run serially.
## The two serialization hazards
## The stub player's units and traps
- `DV_HOLD` is in **seconds** (`sleep "${DV_HOLD:-0.1}"`), not ms. `DV_HOLD=1500`
means a 25-minute "hung" test. Use `DV_HOLD=2` for multi-second holds.
- The stub must sleep as `sleep "${DV_HOLD:-0.1}" &` + `wait $!`, with
`trap 'printf "KILLED\n" >> "$DV_STUB_LOG"; exit 143' TERM` installed **before** the
sleep: a foreground `sleep` defers the trap until the sleep finishes, so SIGTERM-kill
tests (shutdown/interrupt pins) stay red and every kill test eats the full hold.
- Keep the `cp *.wav "$DV_COPY"` loop when editing the stub — the byte-equality test
depends on it.
### 1. `setup()` rebuilds `dist/` from `src/`
Every test runs `cd "$REPO" && bun run build` in `setup()`. Consequences:
## Timing-sensitive assertions
The driver prints `PLAYS` from the stub log at scenario end (~800 ms settle), so with a
long `DV_HOLD` in-flight playback is UNDERCOUNTED — assert `CALLS`/arrival timestamps,
not tail `PLAYS`. The fixture stamps every request with `at=` (ms since driver start,
comparable between CALL lines); prefetch overlap = `t2 - t1 < 1500` with `DV_HOLD=2`
(sequential chain cannot answer inside 2000 ms).
- **Never edit `src/` while a suite is running.** The next test's `setup()` compiles whatever is on disk at that instant, so an in-flight edit gets attributed to the run and produces a failure with no owner. Finish the gate, then write.
- **Two suites on the same checkout race on `dist/`.** Serialize: run one, wait for its report, then start the next. Multiple worktrees of the repo each build their own `dist/` and do not collide.
- Suite runtime is ~85 s for 55 tests; a backgrounded job with a 600 s deadline is the right shape (`timeout: 600`, `async: true`). Never poll it — `setup()` output lands in the conversation when the job ends.
## Diagnosing a hung bats test (deterministic recipe)
1. `timeout 45 bats -f "<name>" tests/behavior.bats; echo rc=$?` — rc=124 confirms hang.
2. Launch the same test as a background job, then probe live:
`pgrep -af "driver.ts|bats"` and `ps afx | grep -E "sleep|stub"` — a `sleep <N>` child
with N seconds in the stub log names the culprit (usually DV_HOLD misuse).
3. Detached `(cmd &)` inside a temp shell is KILLED when the shell exits — use the
harness background jobs for anything you must probe.
4. Direct driver repro: extract the heredoc with
`sed -n '/cat > "$WORK\/driver.ts" <<.TS./,/^TS$/p' tests/behavior.bats | sed '1d;$d' > $W/driver.ts`,
build a fresh ext, then run `bun $W/driver.ts <scenario>` with the env the setup block
exports. Caveat: hand-made fixture env can yield different segment counts than bats —
trust in-bats numbers for assertions.
### 2. `dist/` is tracked, and a pin test enforces it
One test rebuilds and compares against the committed bundle. Therefore the commit order is:
```bash
bun run build # BEFORE staging
git add -A && git commit -m '…'
bats tests/ # gate AFTER the commit
```
Committing `src/` changes without rebuilding first fails the pin test (`not ok … byte-identical to a fresh build`) — the tracked bundle is the *previous* state. If that happens on an unpushed branch, fix it by amend rather than a follow-up commit:
```bash
bun run build && git add dist && git commit --amend --no-edit
```
Verify freshness any time the pin is in doubt:
```bash
bun run build >/dev/null 2>&1 && git status --porcelain dist # empty = committed == fresh build
```
## If a backgrounded gate job vanishes
A crash can kill the job before its report; `read proc://bg_N` returning "not found" means it is gone, not that it failed. Reconstruct what landed before resuming — `git log --oneline -3`, `git status --porcelain`, then the freshness check above — and rerun the suite; the commit may well have succeeded while the gate never ran.
## Gotchas
- `DV_HOLD` is seconds (`sleep "${DV_HOLD:-0.1}"`), so `DV_HOLD=3` really holds the stub for three seconds.
- Isolation tests must not count shared state: any assertion that sweeps `$TMPDIR` or `~/.omp/cache` observes every other live session on the box (each narrating session creates `dankvoice-*` scratch dirs). Point `TMPDIR` at a private directory instead — see the `cutting off mid-utterance leaves no orphaned audio behind` test.
- `bun run build` does not typecheck. Run `bunx tsc --noEmit` separately when moving code between modules; the bundler happily emits a bundle that references a deleted identifier.