Skip to content

test(brainbar): prove the watcher probe left the main actor with an event, not a 0.3s clock (XS) - #786

Merged
EtanHey merged 2 commits into
mainfrom
wt/swift-probe-assert
Sep 5, 2026
Merged

test(brainbar): prove the watcher probe left the main actor with an event, not a 0.3s clock (XS)#786
EtanHey merged 2 commits into
mainfrom
wt/swift-probe-assert

Conversation

@EtanHey

@EtanHey EtanHey commented Sep 5, 2026

Copy link
Copy Markdown
Owner

The defect

StatsCollectorTests.testWatcherProbeDoesNotBlockMainActorRefresh asserted XCTAssertLessThan(elapsed, 0.3) while its stub probe slept 0.6 s. Found by seat ebe88b04 on #784: a cold macos-15 runner measured 0.336 s. The property under test — refresh returns without waiting for the probeHELD; the proxy threshold did not. swift (macos-15) is a required check, so that flake blocks releases.

Per the plan, this does not nudge 0.3 → 0.5. It removes the clock from the property.

The fix (brain-bar/Tests only)

GatedWatcherProbe parks its first sample() on a semaphore that only the test releases. Reaching the line after collector.refresh(force: true) is the property: a probe sampled on the main actor could not have handed control back while it is still parked. XCTAssertFalse(probe.hasCompletedASample) states that directly. No wall-clock margin is involved anywhere in the assertion.

Two timeouts remain, both liveness rather than thresholds — a slow runner makes the test slower, never red:

  • the park is bounded (15 s) so a main-actor regression fails the assertion instead of hanging the suite;
  • the tail waits up to 30 s for the released probe result to publish.

Verification (local, both directions)

Check Result
swift test --filter StatsCollectorTests 16/16 green, 8 consecutive runs
Red proof: move watcherProcessProbe.sample() out of the detached task onto the main actor fails at StatsCollectorTests.swift:756 on the property assertion — reverted, git status clean of it
Same suite under 24× CPU load this test passes twice; the earlier 5 s tail had failed there

Closure: 5 consecutive local runs at 797e587 (M4, nice -n 15)

Run swift test --filter StatsCollectorTests this test alone
1 16 tests, 0 failures, 6.103 s passed (0.694 s)
2 16 tests, 0 failures, 6.324 s passed (0.845 s)
3 16 tests, 0 failures, 6.015 s passed (0.587 s)
4 16 tests, 0 failures, 5.795 s passed (0.565 s)
5 16 tests, 0 failures, 5.807 s passed (0.567 s)

The old assert's 0.3 s budget sat inside a test that legitimately takes ~0.6 s of stub delay; the spread above (0.565-0.845 s) is stub delay plus scheduling, and none of it is load-bearing any more.

CI on the first pushed SHA (041962f): swift (macos-15) pass, 5m53s -- the required check this PR exists to protect.

Review round 1 (Cursor low, addressed in 797e587)

The comment at ~749 overclaimed: reaching the line after refresh(force: true) proves nothing on its own (the probe may not have entered sample() yet at that instant), and the park is bounded by maxBlock, so "only this test can release it" was too strong. Reworded to name the actual proof -- the PAIR of assertions. Comment-only; no assertion or helper changed.

Out of scope — pre-existing, filed separately

Under that same synthetic 24× load (far harsher than a cold CI runner), two other tests in the file still fail on their own 2 s/3 s deadlines: testOlderFullRefreshPreservesPublishedTruthWhileNewerStandaloneIsPending and testSuccessfulDashboardRefreshRefetchesSelectedWiderWindow. Not touched here — canon rule 9, one defect one PR. I have not seen either fail in CI; the evidence is synthetic-load only.

Size: XS — one test file, no production change (74 insertions(+), 16 deletions(-)).

Agent: brainlayerClaude-9fcc41a6 (Claude Opus 5, 1M context)


Note

Low Risk
Test-only changes in StatsCollectorTests.swift; no production or runtime behavior is modified.

Overview
Fixes a flaky CI failure in testWatcherProbeDoesNotBlockMainActorRefresh where a 0.3s elapsed-time cap could fail on slow macOS runners even when refresh correctly did not wait on the watcher probe.

The test drops the in-class BlockingWatcherProbe (fixed 0.6s sleep) and uses a new GatedWatcherProbe that blocks the first sample() on a semaphore until the test releases it. After collector.refresh(force: true), it asserts the probe has started sampling but not finished—proving refresh returned without awaiting the probe, with no timing margin on that property. defer always releases the parked sample; the tail uses a 30s liveness wait (up from 3s) for the probe result to publish.

Reviewed by Cursor Bugbot for commit 797e587. Bugbot is set up for automated code reviews on this repo. Configure here.

Note

Replace delay-based watcher probe test with event-based GatedWatcherProbe

The test for non-blocking main-actor refresh previously inferred non-blocking behavior from a 0.3s wall-clock threshold. It now uses a gated probe that blocks the first sample until explicitly released, so the test can directly assert that refresh returned while the sample is still in progress.

  • Adds GatedWatcherProbe, a thread-safe test double that records sample calls and blocks the first sample until released or a bounded timeout expires
  • Removes the fixed-delay BlockingWatcherProbe and its elapsed-time assertion in StatsCollectorTests.swift
  • testWatcherProbeDoesNotBlockMainActorRefresh now verifies the collector is refreshing after refresh returns, releases the blocked sample, and waits for the running-process result with a longer liveness deadline

Macroscope summarized 797e587.

…vent, not a 0.3s clock (XS)

`testWatcherProbeDoesNotBlockMainActorRefresh` asserted `elapsed < 0.3` while its
stub probe slept 0.6s. On #784 a cold macos-15 runner measured 0.336s: the property
(refresh returns without waiting for the probe) HELD, the proxy threshold did not.
swift (macos-15) is a required check, so that flake blocks releases.

The probe now parks its first `sample()` on a semaphore only the test releases, so
"the refresh did not wait" is proven by an event -- reaching the next line while the
probe is still blocked is the property itself -- with no wall-clock margin anywhere.
The park is bounded (15s) so a main-actor regression fails the assertion instead of
hanging the suite, and the tail wait is a generous 30s liveness poll for the released
result: a slow runner makes this test slower, never red.

Verified both directions locally, brain-bar/Tests only:
- green: `swift test --filter StatsCollectorTests` -> 16/16, 8 consecutive runs
- red: moving `watcherProcessProbe.sample()` out of the detached task onto the main
  actor fails it on the property assertion (StatsCollectorTests.swift:756); reverted
- green under 24x CPU load, where the old 0.3s assert's replacement tail (5s) had
  failed; this test passes twice under that load

Agent: brainlayerClaude-9fcc41a6 (Claude Opus 5, 1M)

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@EtanHey EtanHey added the XS Extra-small change (400 lines or fewer) label Sep 5, 2026
@cursor

cursor Bot commented Sep 5, 2026

Copy link
Copy Markdown

Bugbot couldn't run - usage limit reached

Bugbot is counted against Cursor usage for this user or team, and this run hit a usage or spend limit.

A user or team admin can review and increase usage limits in the Cursor dashboard.

(requestId: serverGenReqId_75745832-8387-4aec-aa23-e23fa33c9da4)

@EtanHey

EtanHey commented Sep 5, 2026

Copy link
Copy Markdown
Owner Author

@coderabbitai review

Focus: this is a test-only change to a flaky wall-clock assert on a required check (swift (macos-15)). Worth checking that GatedWatcherProbe's bounded park cannot make the test pass for the wrong reason, and that the remaining timeouts are liveness waits rather than thresholds.

— brainlayerClaude-9fcc41a6 (Claude Opus 5, 1M context)

@coderabbitai

coderabbitai Bot commented Sep 5, 2026

Copy link
Copy Markdown

Warning

Review limit reached

Next included review available in 34 minutes.

Check out review usage here.

View limit details

Limit details: You’ve used the included review currently available.

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

Learn how review limits work.

Review configuration:

⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Team

Run ID: d982219d-7737-4618-8bcf-c6fd75e00936

📥 Commits

Reviewing files that changed from the base of the PR and between 2264b19 and 797e587.

📒 Files selected for processing (1)
  • brain-bar/Tests/BrainBarTests/StatsCollectorTests.swift

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@deepsource-io

deepsource-io Bot commented Sep 5, 2026

Copy link
Copy Markdown

DeepSource Code Review

We reviewed changes in 2264b19...797e587 on this pull request. Below is the summary for the review, and you can see the individual issues we found as inline review comments.

See full review on DeepSource ↗

PR Report Card

Overall Grade   Security  

Reliability  

Complexity  

Hygiene  

Code Review Summary

Analyzer Status Updated (UTC) Details
Python Sep 5, 2026 5:15p.m. Review ↗
Swift Sep 5, 2026 5:15p.m. Review ↗
JavaScript Sep 5, 2026 5:15p.m. Review ↗
Shell Sep 5, 2026 5:15p.m. Review ↗
Secrets Sep 5, 2026 5:15p.m. Review ↗

Important

AI Review is run only on demand for your team. We're only showing results of static analysis review right now. To trigger AI Review, comment @deepsourcebot review on this thread.

@github-actions

github-actions Bot commented Sep 5, 2026

Copy link
Copy Markdown

BrainLayer ratchet

Every Value below was measured by this run. A row this machine cannot measure says n/a — <reason> instead of a number; baselines in Notes name their own machine, method and date and were not measured here.

Row Status Value (measured by this run) Method Notes
commit provenance 🟢 GREEN measured 797e587b22e8 == PR head · checkout a2796b253f2b commit graph + live PR head · in-process · runner Which commit this whole table is about. On a pull_request event the checkout is GitHub's synthetic merge ref, whose sha is not on the PR — #759's table printed 13fa724278bf while that PR's head was 4632f979 — so this row names the PR-head parent instead, the sha a reviewer can actually see. The comparison sha is read live from repos/{owner}/{repo}/pulls/{n} when the table is collected, not taken from the event payload, because the payload cannot know the run has been overtaken. Residual window, stated rather than papered over: a push landing between that read and the comment being posted is not caught here — the run for that push refreshes the table.
baseline attestation 🟢 GREEN baseline f421d1a7c5e6 matches the main attestation (run 33980263749 · main 24482318d82d · 2026-09-05T17:13:16Z) main attestation artifact via Actions API · in-process · runner What every comparison is measured AGAINST, and who says so. The baseline fields of tests/fixtures/sprint_gate/corpus.json (queries, latency_baseline_ms, thresholds) are compared to the ratchet-attestation artifact of the latest successful push or (no-input) workflow_dispatch run of ratchet-attest.yml on main, fetched through the Actions API — a PR run cannot write to another run's artifacts. A field that differs is RED unless that main run measured the new value; today no runner-side collector measures any baseline field, so today the baseline cannot move by PR at all, and this row says so instead of a hand edit passing. Boundary: the comparator is this PR's checkout of ci_ratchet_table.py, diff-reviewable, not tamper-proof.
provenance 🟢 GREEN stamped a2796b253f2b == HEAD, tree clean wheel stamp · in-process · runner Sha half of #749 keg-mode provenance: a keg built from this wheel can answer __build_sha__. The helper-age and served-process predicates need a running BrainBar and are measured only by scripts/sprint_gate.py on an installed Mac. The sha here is the checkout's — the merge ref on a PR — because that is what publish.yml stamps at release time; the PR-head sha this table describes is the one in commit provenance above.
fallback replay debt ⚪ n/a n/a — no fallback queue on this machine: the pending memories live in ~/Gits/*/docs.local/decisions, and docs.local/ is gitignored, so a runner checkout has no copy of them to count docs.local walk · machine with the fallback queue intended_brain_store: true with no chunk_id means a memory reached disk and never reached the DB, so it answers no brain_search. Budget: 0. Any pending or unparseable file is a finding, never a band -- 122 of these sat from 2026-06-28 to 2026-09-05 because nothing counted them where a reader would look. Measured by walking the tree, so it is only ever measured on a machine that HAS the tree.
mapped bytes ⚪ n/a n/a — no BrainBar daemon at /tmp/brainbar.sock: this row needs the daemon, its hybrid helper and the indexed corpus running together, and no GitHub-hosted runner has them (macOS included) — only a self-hosted Darwin/arm64 runner on an installed Mac would socket · installed Mac Baseline 26.2 GB — installed Mac, socket, 2026-09-03, after R2 drained 15,070 → 0. Up from 16.8 GB because the drain left more vectors mapped under the same cap: the change is the drain, not a leak. Not measured by this run.
search p50/p95 ⚪ n/a n/a — no BrainBar daemon at /tmp/brainbar.sock: this row needs the daemon, its hybrid helper and the indexed corpus running together, and no GitHub-hosted runner has them (macOS included) — only a self-hosted Darwin/arm64 runner on an installed Mac would socket · installed Mac Margin p50: margin unmeasured — 0 of the 5 attested green main runs it needs; no verdict is rendered from fewer. Margin p95: margin unmeasured — 0 of the 5 attested green main runs it needs; no verdict is rendered from fewer. Calibrated on MacBook-Pro.local at 2026-09-01T08:42:22Z under active_sprint_load (tests/fixtures/sprint_gate/corpus.json). Not measured by this run.
idle CPU ⚪ n/a n/a — no BrainBar daemon at /tmp/brainbar.sock: this row needs the daemon, its hybrid helper and the indexed corpus running together, and no GitHub-hosted runner has them (macOS included) — only a self-hosted Darwin/arm64 runner on an installed Mac would ps sampling · installed Mac Ceiling: average CPU < 30% over a 60 s window (resource_budget in scripts/sprint_gate.py), ratified and kept as a hard budget. Margin daemon: margin unmeasured — 0 of the 5 attested green main runs it needs; no verdict is rendered from fewer. Margin helper: margin unmeasured — 0 of the 5 attested green main runs it needs; no verdict is rendered from fewer. Margin watcher: margin unmeasured — 0 of the 5 attested green main runs it needs; no verdict is rendered from fewer. Needs the BrainBar daemon, helper and watcher actually running. Not measured by this run.
signature_valid 🟢 GREEN 442 valid / 0 invalid · brainlayer 1.5.14 · macos-15/ARM64 · brew install exited non-zero (outcome: failure); the keg installed and the sweep ran after it codesign · brew keg · GitHub macOS runner scripts/release-verify-signatures.sh <keg> codesign-verifies every *.so/*.dylib under libexec/venv. The macOS parity job installs the published tap formula (etanhey/layers/brainlayer), so this row measures the release path — formula, published sdist and Homebrew's relocation — and not this PR's tree. Release-time baseline for the same keg on a different machine: 442 valid / 0 invalid — installed Mac (M4 Max), brew --prefix brainlayer 1.5.11, 2026-09-03.

🟢 GREEN measured, within budget · 🔴 RED measured, out of budget — a finding to clear before merge · ⚪ n/a not measurable on this machine, never guessed.

No RED rows.

Measured on Linux/x86_64 · measured 797e587b22e8 · PR head 797e587b22e8 · checkout a2796b253f2b · run · updated 2026-09-05 17:25:25 UTC

@coderabbitai

coderabbitai Bot commented Sep 5, 2026

Copy link
Copy Markdown
🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '--- PR diff summary ---'
git diff --stat HEAD^ HEAD
echo '--- changed files ---'
git diff --name-only HEAD^ HEAD

echo '--- candidate test files ---'
fd -a 'StatsCollectorTests\.swift$' .

echo '--- Swift declaration outline ---'
while IFS= read -r file; do
  ast-grep outline "$file" --lang swift --items all --match 'GatedWatcherProbe|testWatcherProbeDoesNotBlockMainActorRefresh|WatcherProbe|StatsCollectorTests' --view expanded || true
done < <(fd -a 'StatsCollectorTests\.swift$' .)

echo '--- PR diff ---'
git diff --find-renames --find-copies HEAD^ HEAD -- ':*.swift'

Length of output: 1343


🏁 Script executed:

#!/bin/bash
set -euo pipefail

file="brain-bar/Tests/BrainBarTests/StatsCollectorTests.swift"

echo '--- diff ---'
git diff --find-renames HEAD^ HEAD -- "$file"

echo '--- relevant symbol locations ---'
rg -n -C 8 'GatedWatcherProbe|testWatcherProbeDoesNotBlockMainActorRefresh|hasCompletedASample|wait\(|timeout|semaphore|continuation|sample\s*\(' "$file"

echo '--- file outline ---'
ast-grep outline "$file" --lang swift --items all --view expanded

Length of output: 23815


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '--- StatsCollector candidates ---'
fd -a 'StatsCollector\.swift$' .

echo '--- refresh and watcher-probe call sites ---'
rg -n -C 12 -g '*.swift' '\bfunc\s+refresh\s*\(|watcherProcessProbe\.sample\s*\(|watcherProcessProbe|isRefreshing' .

echo '--- target test complete ---'
sed -n '733,787p' brain-bar/Tests/BrainBarTests/StatsCollectorTests.swift

Length of output: 171210


@EtanHey Reviewed the change. I found no blocking issue.

GatedWatcherProbe cannot pass if sample() runs on the main actor. In that case, refresh(force:) blocks until the probe’s 15-second safety bound expires. The probe then records completion. XCTAssertFalse(probe.hasCompletedASample) fails.

The 10-second start wait confirms liveness. The 15-second gate bound prevents a hung suite. The 30-second result wait confirms that the released detached task publishes its result. None of these values is used as a performance assertion.

Static review only. This environment cannot run the Swift test suite.

⚠️ Action not completed

Review rate limited.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

…ove (review round 1)

Cursor low on #786: reaching the line after `refresh(force: true)` does not by
itself prove anything -- at that instant the probe may not have entered `sample()`
yet, and the park is bounded by `maxBlock`, so "only this test can release it" was
also too strong. The proof is the PAIR of assertions: the probe entered `sample()`,
AND it has not completed while the test runs on. Comment now says exactly that.

Comment-only; no assertion, helper, or production behaviour changed.

Agent: brainlayerClaude-9fcc41a6 (Claude Opus 5, 1M)

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@cursor

cursor Bot commented Sep 5, 2026

Copy link
Copy Markdown

Bugbot couldn't run - usage limit reached

Bugbot is counted against Cursor usage for this user or team, and this run hit a usage or spend limit.

A user or team admin can review and increase usage limits in the Cursor dashboard.

(requestId: serverGenReqId_f7563fd2-af42-4485-b63d-3c47181611eb)

@EtanHey
EtanHey merged commit aa7eb57 into main Sep 5, 2026
18 checks passed
@EtanHey
EtanHey deleted the wt/swift-probe-assert branch September 5, 2026 17:34
EtanHey added a commit that referenced this pull request Sep 5, 2026
All six sites: pyproject.toml, src/brainlayer/__init__.py, server.json
(root + packages[0]), brain-bar/bundle/Info.plist (short, bundle, release).
Casks/brainbar.rb stays 1.5.9: no Swift change this release, declared to
scripts/brainlayer-version-check.sh via
BRAINLAYER_VERSION_CHECK_CASK_LAG_REASON="no BrainBar release for 1.5.16"
(kept in the PR body, never exported into the push env — two tests assert
the check fails without a reason).

Why a release, 22 minutes after the last one. v1.5.15 was tagged at
51a72a0 (20:12:29). #778 — the index runtime watchdog — merged at
78d92bc (20:34:55), so it is NOT in 1.5.15. Verified:
`git merge-base --is-ancestor 78d92bc v1.5.15^{}` -> false, and the
installed keg's python raises
`ModuleNotFoundError: No module named 'brainlayer.index_watchdog'`.
That watchdog is the fix for the M1's nightly `brainlayer index` job,
which ran 14h03m at ~100% CPU on 09-05, 10h past its own 4h cap. Both
Macs' 03:15 index jobs are `launchctl disable`d as a stopgap and come
back ON only after 1.5.16 is installed and the watchdog is proven
present.

Nine commits ride along: #774 #777 #780 #778 #783 #785 #786 #787 #788.

Co-authored-by: brainlayerClaude-c1601b03 running claude-opus-5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

XS Extra-small change (400 lines or fewer)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant