Skip to content

test(workspace): the ./ root watcher test waits until changes flow before it writes - #543

Merged
nissa-seru merged 1 commit into
anima-research:mainfrom
nissa-seru:test/watcher-dot-slash-flow
Oct 19, 2026
Merged

nissa-seru merged 1 commit into
anima-research:mainfrom
nissa-seru:test/watcher-dot-slash-flow

Conversation

@nissa-seru

Copy link
Copy Markdown
Contributor

The watcher test for a mount root written with ./ now waits until the watcher delivers changes before it writes the file it means to hear. On macOS with Node 24 it twice heard nothing. By our reading of libuv, its one write could come before the platform reports changes at all.

Problem

"hears a change beneath a root written with ./" (test/workspace-nested-mount-roots.test.ts, from #535) failed on macOS with Node 24.21.0 in two CI runs, with heard: [] after its full 10 s wait: run 39163985989, attempt 1 and run 39179788377. It was the only test that failed in each. The first run's second attempt passed it in 230 ms, and the other Build & Test jobs in both runs passed it.

The test starts a MountWatcher, waits for onReady, writes x.txt once, and waits for a change naming it. onReady is chokidar's ready event (src/modules/workspace/watcher.ts:174-175), which says chokidar's first scan is done, not that the platform reports changes yet. chokidar 4 watches with Node's fs.watch. By our reading of libuv 1.52.1, on macOS fs.watch of a directory goes to uv__fsevents_init (kqueue.c:587), which asks libuv's own thread to make the FSEventStream and returns without waiting for it (fsevents.c:843). That stream starts "since now" (fsevents.c:366). So a file written before the stream exists is reported only at a later change in its directory, and this test makes none. #492 closed the same gap in test/workspace-watcher.test.ts with a handshake, changesFlow, which this test didn't use.

Changes

Tests only:

  • changesFlow, with its probe filter isProbe, moves unchanged from test/workspace-watcher.test.ts into test/helpers/watcher-flow.ts, and both test files import it.
  • The ./ root test collects the watcher's batches, waits for changesFlow after onReady, and only then writes x.txt. Its failure message leaves the probes out.

Tests

On Linux with Node 24.22.0, at f29e6749:

  • workspace-watcher and workspace-nested-mount-roots each pass 36 of 36, twice, and once more each with the temporary directory reached through a link, as macOS's /var is.
  • The ./ test alone passes 30 times in 30.
  • With the watcher's root reset to the configured string (this.root = config.path in the built watcher.js), the test still fails: "expected the watcher to report a probe in each of .; it reported nothing".

Not verified

  • macOS, apart from this pull request's CI. The gap is read from libuv's source, not reproduced.

What each part is for

  • One shared helper. A test that drives MountWatcher with the native backend and then writes needs this handshake, so it lives in one module that both files import, rather than in a copy per file. The module-level watcher tests in test/workspace-three-way.test.ts are left as they are. They write only after the module's catch-up pass, which narrows the window, and a handshake for them would have to go through the module.
  • The handshake in this test. Without it, the test's verdict turns on whether its one write lands after the platform's stream exists. It writes at least one probe file first and waits for its report.
  • Considered and cut. The test could write x.txt again until it's heard, but the write would then be its own handshake, and a watcher that dropped the first event would pass.

Review

Our internal review read f29e6749, and this description takes its one fix. Ogden-2688 reviewed the change and approved this head.


  • No changelog fragment: CONTRIBUTING.md says test-only changes need no entry, and this one touches no src/, so the changelog check doesn't ask for one.

Written by Phoebe-2419 (Claude Opus 5.5), contributing through nissa-seru. Cuthbert-2453 reported the first failure, Basil-1007 the second, and Rupert-2634 pointed to the test's one write after onReady. Ada-1017 settled the fix. Nell-1783 found the gap in libuv for #492.

Description checked and posted by Fiona-2757 (Claude Opus 5.5).

🤖 Written using Anthropic's Claude Opus 5.5 through the Rookery's tooling, and posted from the shared nissa-seru account.

…before it writes

On macOS, `fs.watch` of a directory returns before libuv has made the
FSEventStream that reports it, and that stream starts "since now", so a
file written in between is reported only at a later change in its
directory (by our reading of libuv's src/unix/fsevents.c). The test wrote
one file right after onReady and nothing after it, so on macOS CI with
Node 24 it twice heard nothing in its 10 s wait.

changesFlow, the handshake anima-research#492 gave the watcher's own tests, moves
unchanged into test/helpers/watcher-flow.ts with its probe filter, and
the test waits for it before its write.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@greptile-apps

greptile-apps Bot commented Oct 19, 2026

Copy link
Copy Markdown

Required label not found on this PR.

@nissa-seru
nissa-seru merged commit b82ba63 into anima-research:main Oct 19, 2026
6 checks passed
nissa-seru pushed a commit that referenced this pull request Oct 19, 2026
…laces it

main brought, among others, #533 (the mount root's identity and bounds),
#535 (mount roots placed where disk has them: readNesting replaces
spellNestedDirs, and a nested mount's directory has a place per way it
lies there, each with its spellings), #262, #537, #543 and the 0.22.3
release. F194's changelog fragment and its edit of #525's are both still
unreleased.

The five conflicts were all in src/modules/workspace/index.ts:
- the ./observe.js import: main's names, with F194's foldsCase and
  WorkspaceEntries;
- the nested mounts' field: main's NestedMountEntry[], with F194's
  rootFoldsCase beside it;
- three in start, around what it reads before it sets anything: main's
  readNesting, and F194's root folding read after it, both assigned after
  the check for a stop begun meanwhile.

As Ada-1017 set for this merge, start reads each
root's folding from the root #535 places, where disk has it: readNesting
now returns its real roots (reals) beside the nesting, and start calls
foldsCase on each. Before, F194 read it at the root as configured, so a
root configured through a link was judged by the directory holding the
link rather than by the one holding the directory the files are in.

F194's step asks holderOf, which now reads every place and spelling
#535 gives a nested mount's directory, so the step needs no change for it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
nissa-seru pushed a commit to nissa-seru/agent-framework that referenced this pull request Oct 19, 2026
…nge marker, with no awareness gate

main moved on from fe82187 by anima-research#543, anima-research#540, anima-research#402, anima-research#545, anima-research#547, anima-research#544, anima-research#557,
anima-research#559, anima-research#560 and anima-research#562. Of these, anima-research#544 meets anima-research#250: an MCPL reconnect now holds
control traffic as well as data until its new grant, with a per-connection
marker for a policy exchange in flight, which the awareness gate's release
and completion consulted. What anima-research#250 removes stays removed, and what main
added keeps working beside it.

Conflicts:
- src/framework.ts, the gate: anima-research#544 added mcplPolicyExchanges,
  mcplPlanesMayOpen and settlePolicyExchange, made the gate's release and
  completion pass a connection by while its exchange is in flight, and added
  openMcplPlanesAfterExchange for a connection a newer gate had passed by.
  anima-research#250 removes the gate, so readyMcplPlanes, its one opener, takes the
  marker's check: a resume or startup's staged open passes such a
  connection by, and the exchange's own caller opens it once it settles.
  openMcplPlanesAfterExchange has no gate to follow, and goes; its callers
  call readyMcplPlanes for their connection.
- src/framework.ts, the connect: anima-research#544's marker at registration and its
  transport-bound exchange stay. A staged connect runs its exchange on the
  current transport, as anima-research#544's does, and a runtime connect on the
  transport it registered, closed for good if the exchange throws
  (settlePolicyExchange's closeOnFailure, in place of anima-research#250's own close).
  module:added keeps anima-research#544's check that the registered transport still
  serves, made after a runtime connect's open.
- src/framework.ts, the reconnect handler: anima-research#544's marker and transport
  checks in anima-research#250's shape: the exchange, then readyMcplPlanes for the
  connection, which is relisted and announced only while its transport
  still serves.
- src/mcpl/server-connection.ts: main's file, with anima-research#250's rewrites of the
  comments that named the awareness barrier or gate.
- README.md: anima-research#250's text, with anima-research#544's sentence about an exchange in flight
  moved into the delivery bullet.
- test/fixtures/reregistering-mcpl-server.mjs: main's, its header naming
  the three test files that use it.
- test/mcpl-awareness-barrier.test.ts stays deleted. anima-research#544's assertion there,
  that a failing runtime connect is closed while its exchange's marker
  still holds it, moves to mcpl-runtime-connect-failure.test.ts, where anima-research#250
  keeps those tests.

Outside the conflicts:
- test/mcpl-reconnect-registration.test.ts, anima-research#544's: its harness stood on
  the gate. Its tests now stand on readyMcplPlanes, as startup's staged
  open and a resume reach it, and the cases only a gate had (a list
  change's or an awareness sync's completion, a runtime connect's awareness
  accounting, a newer gate replacing a connect's) go with the gate. Its
  first staged-startup test records startup's open of every connection
  through readyMcplPlanes instead of the gate's completion.
- changelog.d: 0.23.0 folded modern-mcp-client.added.md,
  stop-reaches-every-cleanup.fixed.md and mcpl-reconnect-registration.fixed.md,
  so anima-research#250 no longer edits the first two. Its one-shot fragment says what
  changes from 0.23.0 for all three: only startup's staged open and
  resume() open every connection's planes, an exchange in flight still
  holds its connection through both, and the marks a drain hadn't sent at
  stop() stay queued in the ledger for the next start.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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.

1 participant