Repository navigation
test: wait for events instead of fixed times; ci: run the whole suite once, on Linux at Node 22.12.0 - #884
Open
nissa-seru wants to merge 28 commits into
Open
test: wait for events instead of fixed times; ci: run the whole suite once, on Linux at Node 22.12.0#884nissa-seru wants to merge 28 commits into
nissa-seru wants to merge 28 commits into
Conversation
Five workspace tests waited out real time that what they check doesn't
need.
"no restored timestamp hides a change" slept 2.1 s twice and listed the
mount, so that the file's stamps were old by the time a hash recorded
its fingerprint. What that bought is an old mtime for utimes and cp -p
to restore, as they do from a backup. The test now sets the seeded
file's times 60 s back with utimes instead; only ctime can't be set, and
the kernel moves it to now on every change. With the sleeps and the
listings gone, the test fails and passes as before under each of these
mutants of the fingerprint check (3 to 5 runs of each, with and without
the sleeps):
mutant this test fingerprintVouches'
assertions
ctime left out of the passes fail: "any field
field comparison differing"
the racy window left out passes fail: "hashed inside
the racy window"
both fails: "cp -p: not run
rehashed and
adopted"
ctime left out of the field fails: "cp -p: fail: "any field
comparison and of the rehashed and differing"
window's newest stamp adopted"
The assertions vary only ctime among the fields, and sample the window
at 1,000 and 5,000 ms.
"a recheck of an unchanged file records nothing" slept 2.1 s so that
every hash after it came past the racy window of the stamps utimes set.
It now moves Date.now() past that window for the rest of the test, which
makes no change after that point, and asserts what the wait was for:
after the first recheck, the recorded fingerprint vouches for the file.
A recheck that re-recorded whenever a fresh fingerprint vouched still
fails it, and would pass if the clock were moved for the first sync
alone.
The two catch-up tests held each of chokidar's directory reads for a
second, so that a pass started beside the scan would finish before
chokidar watched anything. The reads now wait until the start (or the
re-attach) has returned, a timer turn has passed, and every pass started
by then has ended. A hold gives up after 15 s, so that a regression in
which start(), reattach() or a pass waited for the watcher's ready can't
hang its test, and a hold that gave up fails the test by name. A pass
started at start() still fails the first, a pass started at the
re-attach the second, a catch-up bound of 0 ms both, and a start() that
waits for the watcher's ready the first, on the hold that gave up. The
re-attach test now re-attaches only once the clock has moved past the
first ready, which the second ready's time is told apart from.
"a batch that comes while a late scan's successor still runs isn't held
either" set a 300 ms scan deadline and waited it out; its siblings use
50 ms, and so does it now. A successor scan not marked late still fails
it, at the third batch.
StdioTransport gives a stdio child STDIO_EXIT_GRACE_MS (2 s) to exit after SIGTERM, and again after SIGKILL, before it says the child could not be reaped. In test/mcp-server-ownership.test.ts, 17 tests use a child whose kill() is a no-op, so each close of one waits out both periods, four seconds; the startup test closes two, one per engine. Those waits were most of the file's 90 s. Nothing ends such a child while it is closed (a close never ends its stdin, and no test that shortens a grace gives its child any of the fixtures' other ways to exit), so the wait only delays the verdict, and the tests assert what the failed reap leaves (the error's words, the server still registered, a connect refused, the launch count, the retry completing), not how long it took. - src/mcpl/transport.ts: setStdioExitGraceForTests(child, ms) records a grace for one child in a WeakMap. Its doc says to set it only on a child that can exit by no means, since a short grace there races no exit. escalate(), the one attempt terminate() makes for a child, reads that child's grace once at its start, or STDIO_EXIT_GRACE_MS when there is none, and uses the value for both waits, its log line and its error. The setter isn't reachable from the package: package.json's exports name only ".", and src/index.ts doesn't re-export it. - test/mcp-server-ownership.test.ts: unreapable(graceMs?) gives each child it makes unkillable that grace. 12 of its 13 call sites pass SHORT_GRACE_MS (500 ms), two of them in loops that make two tests each, so 14 of the 15 tests that use it, the startup test among them. The loop's two "lost on its own whose child can't be reaped" tests, which make their child's kill() a no-op by hand, give that child SHORT_GRACE_MS too. The stop() test's two SIGKILL error checks match "did not exit within 500ms of SIGKILL", where they took any number, so a setter that records nothing, or an escalate() that doesn't read the child's grace, fails them. "a listing of a server whose disconnect failed to reap isn't published" calls unreapable() with no grace and keeps 2 s: it releases its held listing after polling for the close, and needs the failing disconnect still in flight then. Why per child: a grace set for the whole process also shortens the waits of children that do exit, such as the ignore-sigterm child after SIGKILL and ordinary children closed at a test's end, and those race the timer. On Linux with Node 24.22.1, spawning the fixture beside the whole suite, a child's exit arrived at most 473.5 ms after SIGKILL and 268 ms after SIGTERM in 1,240 trials of each (the one-minute load reached 69). Every child that can exit keeps 2 s here, as in production. Runs of the file alone, before and after interleaved in one run: 94.1, 95.0 and 92.9 s before; 40.8, 44.4 and 40.1 s after; 40 of 40 passing in each, at a one-minute load of 26-59. With terminate() taking a child still alive after SIGKILL's wait as reaped, the same 17 tests fail as with that break before the change; with a disconnect in flight no longer refusing a connect of its id, the same 3. The whole suite at this head: 8,793 tests, 8,731 passing, 0 failing, 62 skipped. No changelog fragment: the change is test-only, with an internal setter the package doesn't export (CONTRIBUTING.md, "What needs an entry"). It touches src/, so the pull request needs the no-changelog label.
…nnect tests reconnect an ordinary child
- SHORT_GRACE_MS goes from 500 to 50. It is given only to children whose
kill() the test made a no-op and that nothing else ends while they're
closed, so it bounds no exit that can arrive: it only brings "could not
be reaped" sooner. No test that takes it asserts, after an await, that
the reap is still in flight: each awaits the reap's verdict, or an
alert only the failed reap raises, before it asserts.
- "a connect during a disconnect is refused…" and "two simultaneous
disconnects…" reconnect with an ordinary child, modern('m', '', log),
in place of the ignore-SIGTERM one. Their reconnected child only has to
be listed and routed; as the ignore-SIGTERM fixture it cost a further
2 s at stop(). An ordinary child exits on SIGTERM within production's
2 s, so nothing is shortened. The first child of each still ignores
SIGTERM, as before.
Runs of the file alone (Linux, Node 24.22.1): 23.4, 24.9 and 23.9 s at a
one-minute load of 35-61, against 40.1-44.4 s with the previous commit
alone and 92.9-95.0 s before it. With terminate() taking a child still
alive after SIGKILL's wait as reaped, the same 17 tests fail; with a
disconnect in flight no longer refusing a connect of its id, the same 3.
… unkillable The per-child seam the ownership tests take, setStdioExitGraceForTests(child, ms), now in the three files that make a stdio child's kill() a no-op and then wait out StdioTransport's 2 s after SIGTERM and 2 s after SIGKILL to reach "could not be reaped", 4 s a close. 17 tests take a short grace. Each such child is given 50 ms, beside the no-op kill() that makes it unable to exit. Nothing else ends it while it's closed, so the wait races no exit and only brings the verdict sooner. - test/mcp-modern-outcomes.test.ts: firstLaunch(true)'s child, the five children liveLine() reaches and makes unkillable by hand, the stuck transport of "a child that cannot be reaped fails close() explicitly", and the second-launch-on children of "a failed launch that cannot be reaped halts reconnecting". The three checks of the error's words that named 2000ms name the short grace, so a grace that isn't applied fails them. liveLine()'s child is typed ChildProcess, as the setter takes. - test/mcpl-legacy-lifetime.test.ts: unreapableFrom(from, graceMs?) gives its stuck children the grace; eight of its nine callers pass it. - test/mcpl-stderr-tail.test.ts: the one startup test whose launch can't be reaped. Two tests keep 2 s, since each asserts, after an await, that the reap is still in flight: "concurrent close() calls share one teardown and its verdict" (mcp-modern-outcomes) and "a close that cannot reap fails, and so do concurrent ones" (mcpl-legacy-lifetime) each check at 1 s that neither close has settled. Every other shortened test asserts only after the reap's verdict: a rejected close() or connect, an 'unreaped' event, a permanent failure, holdsUnreapedChild, or lossReaped(). mcpl-stderr-tail's "a handshake that times out carries the last of the server's stderr" keeps its 30 s on purpose: it pins the handshake-timeout path itself, a wait for an answer that never comes, beside the exited-server path its neighbour covers.
…turn past it abandonActiveTurns() waits up to 10 s, polling every 250 ms, for the turns it reached to end, then lists those still running as unabandonable. Three tests hold a turn past that grace on purpose and check that it's listed, so each waited out the full 10 s: - stop-without-stream: "a quiesce's abandon of a turn whose hooks outlast the abandon's grace lists it…" (its hooks are held until after the quiesce returns) and "an abandon of the wait while it still holds the finished stream lists the turn if it outlasts the grace…" (its round's post is held until then); - host-quiesce: "abandon reports turns it cannot cancel (token held, no stream)" (the token is held until the test's finally). - src/framework.ts: the grace is ABANDON_GRACE_MS (10 s), as before. setAbandonGraceForTests(framework, ms), for tests only, records a grace for one framework in a WeakMap, which abandonActiveTurns() takes as its deadline in place of the constant. Its doc says to set it only where the test holds every turn the abandon reaches past the grace, so no turn can end within it. src/index.ts doesn't re-export it, and package.json's exports name only ".". - The three tests set 50 ms before their quiesce. Two comments that named the ten-second grace go with it. The loop re-tests its deadline after each 250 ms sleep, so with 50 ms its first sleep is the whole wait, and each abandon reports about 0.25 s after it starts.
…Request, so the push it buffers is delivered
"MCPL: pushes buffer while quiesced; host/command resume reopens and
flushes" used a stdio fixture that advertised MCPL 0.4 and left
featureSets/update, the initial policy Request, unanswered, as a pre-0.5
server does. The host waits 15 s for it (sendFeatureSetsUpdateRequest's
default). Until then no grant is established, and the push the test
buffers while quiesced was refused once the resume flushed it: its
response was -32002, 'push-event rejected: capability "pushEvents"
initial policy not yet established'. The test took about 16 s, and two of
its checks couldn't fail for the reason they give: "no wake while
quiesced", since a refused push wakes no one, and "flushed", since a
refusal counted as the answer.
- The fixture advertises 0.5 with a "chat" feature set that uses
pushEvents, as the push names, and answers the Request with
{ accepted: true }, as the suite's other MCPL fixtures do. It logs the
push response's result beside its error.
- The test asserts that the flushed push is answered with no error and
with accepted: true, since a push the host refuses can also be
answered as a result with accepted: false.
…for fixed sleeps settle() released the held turn, then three times ran runUntilIdle() and slept 300 ms, 17 calls. Two tests in the explicit and hybrid describe did the same inline. What each then checks is which server a reply was published to, or that none was. In the default routing a turn awaits its queued posts before it ends, but in explicit mode, and when a tool ends the turn, its posts go out after it (chainSpeech, framework.ts). Every post a stream queues while it is the agent's current stream is chained behind the agent's entry in proseSpeechChains; a stream another has replaced chains privately, and no test here replaces one. - postsSettled(framework, agent) runs runUntilIdle(), awaits that entry, runs runUntilIdle() again, and returns once the entry hasn't moved, so a post queued by a turn the last post started is waited for too. settle() and the two inline loops call it. - Four fixed sleeps after an awaited publish go: 500 ms after three tests' background channel_publish calls, and runUntilIdle() and 300 ms after the code_execution test's script finished. Each call returns once the server answered channels/publish, or once the request timed out (handleToolPublish awaits sendChannelsPublish; the script awaits tools.channel_publish). The fixture logs a publish before it answers it and, in these tests, answers every publish at once, so what the sleep waited for had already happened. The test reaches proseSpeechChains through a cast, as host-quiesce reaches activeTurnTokens.
… up at 60 s by name
"a create() refused after it started the watchdog leaves none to kill the
process later" ended its child with SIGTERM after 20 s, as a safety, and
then checked the child's exit. The watchdog's worker ends its process with
SIGKILL (action 'exit') or SIGABRT ('abort'), never SIGTERM, so under load
a child still starting failed the test as if the release had left a
watchdog behind. With its child on one CPU shared with 16 busy loops, the
test at the parent commit failed 2 of 2 after 21.3 and 21.5 s: the safety
had ended the child before it printed anything.
What the test requires now:
- A refused create() that had started the liveness watchdog leaves no
watchdog worker running: the child sees that worker exit, then ends on its
own with code 0.
- A child that hasn't ended by LONGER_THAN_ANY_START_MS (60 s) fails the
test by saying so, as "the watchdog worker started and never exited" when
the child printed a start and the refusal and no exit, and as "the child
hadn't finished" otherwise. A slow start can't read as the exit check's
verdict, and a create() still running can't read as the release's.
- The child must have seen exactly one watchdog worker start, so the wait
can't pass on nothing.
- The watchdog can't fire before the give-up: thresholdMs is
NEVER_FIRES_IN_A_TEST_MS (2 x LONGER_THAN_ANY_START_MS), so a stalled main
thread can't get a correct child killed.
The child wraps node:worker_threads' Worker (createRequire and
syncBuiltinESMExports) before create(), and prints a line when each worker
whose file names liveness-watchdog-worker starts and when it exits. If
create() doesn't refuse, the child exits at once, so the refusal check names
that. Otherwise it holds the workers referenced, waits on their exits, and
exits 0. The 200 ms wait and the 5 s busy loop, which gave a surviving
watchdog time to fire, are gone. The parent checks the give-up first, then
the exit (with what the child printed), the armed and refusal lines, and the
start count. A renamed worker file fails that count, and the count's message
names the file the child watches for.
The give-up ends the child with SIGKILL: it must end the child whatever the
child handles, and its own assertion comes first, so the signal never reads
as the watchdog's.
On Linux with Node 24.22.1:
- as is, 3 of 3 passed, the test taking 1.7-2.0 s; with its child on one
CPU shared with 16 busy loops, 2 of 2 passed in 21.9 and 25.9 s.
- with the release no longer stopping the watchdog, and with stop() no
longer terminating the worker: each fails at the give-up after 60.3 s,
"the watchdog worker started and never exited".
- with create() held on a timer after the watchdog starts: fails at the
give-up after 60.1 s, "the child hadn't finished".
- with the child's matcher naming another file, and without
syncBuiltinESMExports(): each fails at the start count, 0.
- with create() no longer refusing: fails at the refusal check.
- this file and test/liveness-watchdog.test.ts together: 14 of 14; the
whole suite passed.
…t 8 s as a SIGTERM runWedgedChild killed its child with SIGTERM after 8 s, and the two tests that use it then read that signal as the watchdog's: "expected SIGKILL, got code/signal SIGTERM". The watchdog never sends SIGTERM, so under load a child still starting at 8 s would have failed as if the watchdog had misbehaved, the same way "a create() refused after it started the watchdog leaves none to kill the process later" failed at 20 s. What the tests require now: - A wedged process whose watchdog's action is 'exit' is ended by SIGKILL, and one whose action is 'abort' by SIGABRT, with its report written, as before. - A child the watchdog hasn't ended by LONGER_THAN_ANY_START_MS (60 s) fails the test by saying so: "the wedged child hadn't ended by 60000 ms, so the test ended it". That check comes before the signal check, so a slow start, or a watchdog that never fires, can't read as a wrong signal. The child wedges its main thread for NEVER_FIRES_IN_A_TEST_MS (2 x LONGER_THAN_ANY_START_MS) rather than 30 s, so a watchdog that never fires leaves it wedged until the give-up, while a child the test somehow left behind still ends by itself. The give-up ends the child with SIGKILL. The two comments that described the old helper and its endless wedge go, since neither held any longer. On Linux with Node 24.22.1: - as is, the file passed 3 of 3 in each of 3 runs, the two wedged tests taking 1.0-1.3 s. With the child on one CPU shared with 16 busy loops, 2 of 2 passed, the wedged tests taking 7.5-9.2 s. At the parent commit, on that load, they passed 2 of 2 in 7.1-8.6 s, against the old 8 s safety. - with the watchdog's worker no longer killing its process, and with the worker's file missing: both wedged tests fail at the give-up after 60.0-60.1 s, by name; the healthy test passes.
….5 s after the worker's first "watchdog does NOT kill a healthy (heartbeating) process" kept its child alive for a fixed 1.5 s and passed if nothing killed it. Under load the watchdog's worker could start late enough to make no check in that time, so a main thread that stops beating passed too: with its child on one CPU shared with 16 busy loops, a version whose main thread beats only once passed 4 of 5 runs. What the test requires now: - The child stops waiting at the first check the real watchdog worker makes whose time, read just before the check ran, is at least 1.5 s after the worker's first check, and exits 0; nothing kills it before then. That check measures the heartbeat's age from the last beat it reads and kills when it is over 400 ms. So a child whose beats stopped for good more than 400 ms before it is killed before it can pass, and so is a worker whose view of the beats froze more than 400 ms before it, however late the worker starts and however bunched its checks. Beats that stop within the last 400 ms of the window, or a stall that ends between two checks, aren't guaranteed to be caught, as with the fixed 1.5 s before. - The child also stops waiting if the worker exits first; the test then fails, saying how many checks the worker made. A child whose wrapper never saw the worker start, and a child that ends before recording what it saw, each fail by saying so. A child that hasn't ended by LONGER_THAN_ANY_START_MS (60 s) fails by the give-up's own message. The child sees the real worker's checks with no change to src: its Worker wrapper (createRequire and syncBuiltinESMExports, as in failed-create-releases.test.ts) starts the watchdog's worker file under a short eval shim. The shim wraps the worker's setTimeout so that each callback reads the time before it runs and posts it once it has run, then imports the real worker file, which schedules every check with setTimeout. A check that kills the process posts nothing. The child counts the window from the worker's first check, so a worker that starts late, or reads the beat only once, still has the whole window to age it, and writes what it saw to a file once, when it stops waiting. On Linux with Node 24.22.1: - without added load, as is, 3 of 3 passed, the test taking 2.2-2.4 s. - with the child on one CPU shared with 16 busy loops, as is passed 5 of 5 (11.4-16.5 s). The main thread's interval stamping no beat after the first, the beats stopping 600 ms after start(), and the worker reading only the first beat were each killed by SIGKILL in every run (5, 5 and 3 runs), as a rule written before the runs required. Counted from start() instead, the window let the last of these pass 2 of 3 runs. - without added load, the same three are each killed, and a worker that never schedules a check fails: "the watchdog's worker exited after 0 checks". The whole file passed, 3 of 3.
…at 20 s "a SIGUSR2 after every framework has stopped leaves the process running" gave its child 20 s, then ended it with SIGTERM and failed with "the child did not finish within 20 s". The child imports the whole framework and creates and stops one before its signal, so on a loaded machine it can still be starting at 20 s: the test failed so in two whole suites, at one-minute loads of 101-126 sampled at their starts and ends, and with its process on one CPU shared with 16 busy loops. The give-up is now LONGER_THAN_ANY_START_MS (60 s), as in the isolate tests, and fails by its own name: "the child hadn't finished by 60000 ms, so the test ended it". It ends the child with SIGKILL, so a handler in the child can't hold it. The verdict is still the child's own exit. On Linux with Node 24.22.1, at one-minute loads 74-107 sampled when the runs began and ended, "under load" meaning the test's process on one CPU shared with 16 busy loops: - before this change, under load in an earlier run (one-minute loads 124-176 sampled during it), the test failed 2 of 2 by its 20 s give-up. - with it, the file passed 2 of 2 without added load (4.9-5.4 s) and 2 of 2 under load (45.5-57.5 s). - with the framework's handler listening for SIGUSR1 instead of SIGUSR2, the test fails: the child ends by SIGUSR2.
…st warms up first Two tests in this file failed on loaded machines without anything being wrong with the connection: - "a lost child reconnects with backoff…" waited for its reconnect with the file's until(), which gave up after 5 s and said only "condition reached in time". The reconnect relaunches a node child and handshakes again, which took longer than that under load. Every until() in the file waits on a child process, so until() now gives up at LONGER_THAN_ANY_START_MS (60 s), naming what it waited for. - "a deadline requests cancellation and reports no-response…" set the connection's requestTimeoutMs to 300 ms before its warm-up call, so the warm-up "echo" ran under the deadline meant for the slow call, and under load the warm-up itself was refused with the no-response error. The connection now starts with the default deadline, and the test sets 300 ms on its configuration after the warm-up; callTool reads the deadline from that configuration at each call. The slow call's verdict, rejected at the deadline while the tool hadn't finished, is unchanged, but "hadn't finished" no longer compares the host's deadline with the tool's own 20 s sleep: the tool now runs in the fixture's release mode, released only by the test's own give-up at LONGER_THAN_ANY_START_MS, which the test clears once the call has rejected. A call that hadn't ended when the give-up released the tool fails by the give-up's name, "the call hadn't ended by 60000 ms, so the test released the tool", not as "Missing expected rejection", and so does a call that rejects only after the release. The fixture notes the cancellation when the call is already cancelled as its handler starts, as it is when the request and its cancellation are read together. On Linux with Node 24.22.1, "under load" meaning the test's process on one CPU shared with 16 busy loops: - before this change, under load (one-minute loads 124-176 sampled), the reconnect test failed 2 of 2. - with it, before the deadline test's give-up was named, the file passed 2 of 2 without added load (8.6-9.0 s) and 2 of 2 under load (101.3-107.9 s), at one-minute loads 61-114 sampled. - as committed, the deadline test passed 2 of 2 without added load (at one-minute loads 30-34); with the per-call deadline ignored, it fails after 60 s: "the call hadn't ended by 60000 ms, so the test released the tool". - with no reconnect ever scheduled, the reconnect test fails after 60 s: "the connection reconnects, within 60000 ms" (run before the deadline test's release mode and the fixture's change, which it doesn't use).
The file's within() gave node children 10 s to connect or answer and 30 s to finish. "importing the package, or running its server module under the command's name, starts nothing…" runs three children in a row, two importing the whole package and one running a copy of the server module, and on a loaded machine the second was still running at 30 s: the test failed with "the package root imported under `node -e`: the run finished within 30000 ms". within() now always gives up at LONGER_THAN_ANY_START_MS (60 s), failing by the name it's given, so a slow start can't read as a verdict. The verdicts are unchanged: exit codes, stderr, connections and answers. On Linux with Node 24.22.1, at one-minute loads 74-107 sampled when the runs began and ended, "under load" meaning the test's process on one CPU shared with 16 busy loops: - the file passed 2 of 2 without added load (7.3-9.2 s) and 2 of 2 under load (52.8 s each). - with the server module starting itself whenever its entry point is named mcp-server.js, the test fails, naming the bundled case.
…00 ms "a judge that takes too long is timed out, its request aborted, and the agent woken" slept 200 ms, then asserted the judge's request was aborted. The rule's 100 ms debounce and the 50 ms judge timeout put the abort at least 150 ms after evaluate(), leaving the sleep 50 ms, and a loaded run found the request not yet aborted. The test now waits, with the file's until(), for the request to be aborted and the agent woken, giving up at LONGER_THAN_ANY_START_MS (60 s) rather than until's 5 s default. What it asserts is unchanged. On Linux with Node 24.22.1, at one-minute loads 61-114 sampled when the runs began and ended, "under load" meaning the test's process on one CPU shared with 16 busy loops: - the file passed 2 of 2 without added load (12.8-20.4 s) and 2 of 2 under load (45.5-77.3 s). - with the judge timeout no longer aborting the request, the test fails after 60 s: "timed out waiting for the judge to be timed out and the agent woken".
…ll time "an error answer's data with many keys that redact alike is numbered in time that grows with the keys, not with their square" timed redactedData over 16,384 keys with performance.now() and asserted under 2 s. Wall time measures the machine as well as the algorithm: a whole suite, at one-minute loads of 122 at its start and 108 at its end, measured 2,781 ms. The test now counts the numbering's probes. Each key that redacts to a taken name is numbered by probing a Set of the names taken, so the test counts calls to Set.prototype.has during redactedData, at 1,024 keys and at 4,096, and asserts the larger makes fewer than 8 times the probes: the loop as written makes 2n-1 (2,047 and 8,191, about 4 times), and a search starting from 2 again for each key n(n+1)/2 (about 16 times). The count reaches the probes through the global Set.prototype.has, restored after each call, with no seam added to src. The test's name now says what it counts: "…is numbered with name probes that grow with the keys, not with their square". So it guards this numbering against the quadratic search it once had, not every way redaction's time could grow: work done outside Set.prototype.has isn't counted. A numbering rebuilt without a Set makes fewer probes than keys, and the test fails saying so, to be pinned again. On Linux with Node 24.22.1, at one-minute loads 61-114 sampled when the runs began and ended, "under load" meaning the test's process on one CPU shared with 16 busy loops: - the file passed 2 of 2 without added load (6.6-9.7 s) and 2 of 2 under load (63.4-104.6 s). - with each name's numbers searched from 2 again, the test fails: "524800 name probes for 1,024 keys and 8390656 for 4,096", sixteen times the probes for four times the keys.
…lock "organization acceleration 429 defers one fresh compile, includes arrivals, and avoids failure breaker" set the real cooldown to 1,000 ms and had to push and settle a second message inside it before checking that the message was held. On a loaded machine the cooldown ran out first, the held request was inferred, and the test failed with "new arrival is held, not separately inferred". It then slept 1,100 ms to let the cooldown pass. The cooldown records when it ends as Date.now() plus its length, and its release re-arms while that time is still ahead. So the test now mocks Date alone (node:test's mock.timers) after create(): while the mocked clock stands still, the cooldown's timer can fire but can't end it, however slow the machine. The clock moves 1 ms before the second arrival, since a hold keeps two requests as one when they match, timestamp included, and 999 ms after it, which ends the cooldown at its 1,000 ms; the test then waits for the deferred call with the file's waitFor. setTimeout stays real, since the framework's event loop yields with setTimeout and a mocked one would stall runUntilIdle. The mock is reset before stop(). The file's provider-wait test already mocks Date the same way. With the clock exact, the recovery receipt's cooldownMs is now asserted to be 1,000. waitFor checked a 5 s give-up between drains, shorter than a turn on a loaded machine; it now checks LONGER_THAN_ANY_START_MS (60 s), naming what it waited for. What the tests assert is unchanged. On Linux with Node 24.22.1, at one-minute loads 61-114 sampled when the runs began and ended, "under load" meaning the test's process on one CPU shared with 16 busy loops: - the file passed 2 of 2 without added load (10.0-11.1 s) and 2 of 2 under load (48.7-51.9 s). - with the receipt's cooldownMs written as 0, the test fails at the receipt. - with the cooldown's release doing nothing, the test fails after 60 s: "the deferred attempt once the cooldown had passed did not happen within 60000 ms"; with setTimeout mocked as well as Date, it times out before its first assertion (both run before the receipt's assertion was added).
…the kill by an event, not the clock "npm pack or npm install that outlives its limit fails at the limit even when it handles SIGTERM" sets the script's limit to 10 s for its install case, and the script runs npm pack under that same limit before the install the case hangs. The case handed pack to the real npm through a second node, so on a loaded machine the real pack used up the 10 s: a whole suite failed with "npm pack didn't finish within 10 s" in place of the install's message. In the install case the npm shim now answers pack itself, printing the one record the script reads (the tarball's file name and its files), so the real pack no longer runs under the install's limit; the shim's own start still does, and the install never reads the tarball. The real pack stays covered by the file's other cases. Both cases asserted that they took under 40 s by the wall clock. That was what showed the limit's kill ending an npm that handles SIGTERM: the "didn't finish within N s" message comes from ETIMEDOUT, which execFileSync also reports when a child that outlived its kill exits by itself later. Each hanging npm's own timer now writes an "-outlived" note, and the test asserts it is absent, so the kill is pinned by an event rather than by the clock. The timer carries that verdict, so it sits past the give-up, at NEVER_FIRES_IN_A_TEST_MS (120 s, main's was 60 s): a correct 5 or 10 s kill can't lose a race with it however slow the machine. On Linux with Node 24.22.1, "under load" meaning the test's process on one CPU shared with 16 busy loops: - before this change, under load, the test failed 3 of 3 with "npm pack didn't finish within 10 s" (one-minute loads 55-95 sampled). - with the shim's answer, before the "-outlived" notes, it passed 2 of 2 without added load and 3 of 3 under load, each time with the hanging install's note, so the script reached the install with the shim's record; the whole file passed, 27 of 27. - as committed, it passed 2 of 2 without added load (16.1 s each, at one-minute loads 148-161 from other work). With the install's kill sent as SIGTERM, it fails after 121.6 s: "npm install, ignoring SIGTERM, outlived the limit"; with npm pack's kill sent as SIGTERM, after 130.6 s: "npm pack, ignoring SIGTERM, outlived the limit".
The file's until() gave up after 10 s and said only "in time: <what>".
The file's waits are for child processes and sockets, and for registry
state around a launch: a reconnect launch starting, a permanent
reconnect failure, a dial reaching its listener. On a loaded machine
three tests failed there without anything being wrong: "a reconnect
launch that cannot be reaped halts reconnecting…" ("in time: a
permanent reconnect failure") and the two tests that wait for "the
reconnect launch has started".
until() now gives up at LONGER_THAN_ANY_START_MS (60 s), naming what it
waited for and the bound. What the tests assert is unchanged.
On Linux with Node 24.22.1, without added load (one-minute loads 8-21
sampled when the runs began and ended):
- the file passed 2 of 2, 19 of 19 tests (48.2-49.0 s).
- with no reconnect ever scheduled, "a reconnect launch that cannot be
reaped…" fails after 60 s: "a permanent reconnect failure, within
60000 ms".
The file's 27 cases ran one after another, about 69 runs of
scripts/pack-installs-alone.mjs between them, each an npm pack, a linked
npm install and a child process per entry point. Alone it used about a
third of a core over its wall, waiting on its children, and in a whole
suite it ended last: started late in name order, it ran alone for its
last 75-119 s on a 12-core Linux machine.
The cases now run inside one describe with { concurrency: 4 }. Top-level
tests in a file run one at a time whatever --test-concurrency says (that
option spreads files), so a describe is what lets them overlap. Each
case already works in its own mkdtemp directory and gives the script its
own environment; the two shims made at the top (NO_BUN, BUN_BY_NODE) are
only read. The one case that changes process.env (PATH, in the
npmCommand case) changes and restores it without an await between, so no
other case can run meanwhile.
Measured alone on Linux, Node 24.22.1, at a one-minute load of 11-30:
the file as it was took 125.5 and 139.1 s; the same cases four at a time
took 53.5 and 38.6 s, with every case passing, about what four separate
processes of a quarter of the cases each took (50.7 and 38.5 s). One
file at concurrency 4 holds one of CI's test-file slots, where four
files would hold up to four.
Four at a time, a case's wall-clock bound also counts its siblings'
work, so the load-limit case's five checks now time each kill from the
hang's own start, the mtime of the note it writes once its SIGTERM
handler is in, rather than from check()'s start. That leaves out the
script's real npm pack and npm install, which run under their own 120 s
limit before the load and are what grows under load (as an npm pack did
past a 10 s limit in a loaded whole suite). The bound stays 40 s: a
script that only sends SIGTERM leaves each hang to its own 60 s timer,
so it fails the bound however loaded the machine is. A sibling can still
hold the thread with a synchronous call, packInto()'s real npm pack or
the tests' own npm pack at its 5 s limit (5.0-5.1 s): that delays when a
check reads its bound, by as long as the call takes, but not when its
hang was killed.
The cases are re-indented into the describe, except the lines inside the
bin load hook case's template literal, which is the spy preload's text;
git diff -w shows the node:fs and node:test imports, the describe's two
lines, and the five checks' bounds with their helper.
…he package installs and imports everywhere else
Build & Test ran the whole suite on up to five legs: ubuntu at Node
22.12.0, 24 and the newest Node, and macOS at 22.12.0 and the newest
Node on a push to main, or at 22.12.0 alone on a pull request (macOS at
24 was always left out). It now runs once, on ubuntu at 22.12.0, the
floor that engines.node declares, with the steps it had: the typecheck,
the build, npm test with AF_TEST_CONCURRENCY, the unforced files and the
check that no Node diagnostic report was written.
The other legs become the install-and-start job: macOS at 22.12.0, 24
and the newest Node, and ubuntu at 24 and the newest Node. Each installs
the repository with npm install (the repository keeps no lockfile),
builds, and runs scripts/pack-installs-alone.mjs, the check pack-check
and the publish workflow already run, rather than a second check inline.
It packs the package as npm publish packs it, installs the tarball alone
into an empty directory with its dependencies from the registry, as a
consumer's install resolves them, and loads the package's entry point.
For the two bins, agent-framework-mcp and agent-framework-recover, it
checks each #! line and that each one's static imports load; it runs
neither, since running agent-framework-mcp starts the MCP server. These
legs set up no Bun; where the runner has none, the check skips the bun
target's load and says so in its summary ("bun targets present, not
loaded (bun doesn't run here: ...)"). The job's 40-minute limit is
pack-check's, above the check's own limits (10 minutes each for npm pack
and npm install, a minute for each load), so the check names what hung.
pack-check (the packed release on its own, under Node 22.12.0 and 24)
and code-execution-bun (code_execution's files under Bun 1.4.0 and
1.4.3, on ubuntu, and on macOS on a push to main or when a pull request
touches what they run) are as they were.
What no longer runs:
- the suite on macOS. It ran there because "process/stdio behaviour
differs (pipes are asynchronous on macOS), and ubuntu-only runs have
missed that", as the Test step's comment said: c2fa58a made the macOS
legs run the suite, by removing the Test step's ubuntu-only condition,
when a refusal's stderr write was lost to process.exit on macOS's
asynchronous pipes. code_execution's files still run on macOS under
Bun, on a push to main and on a pull request that touches them.
- the suite at Node 24 and at the newest Node, on any platform. What
differs by Node version, such as whether --test-timeout bounds a whole
file (22.12) or each test (24), and stdio flushing around process
exit, is now tested at 22.12.0 alone. pack-check still runs its check
under 24 as well as 22.12.0, for the load hook's two branches.
macOS runners: a pull request takes Install & start's macOS leg at
22.12.0 alone, as Build & Test's was before, in place of the whole suite
there, which took 5 to 10 minutes in three of main's recent runs; one
that touches what code_execution's tests run also takes
code-execution-bun's two macOS legs, as before. A push to main takes
Install & start on macOS at all three Node versions. The changes job
decides both, as it decided Build & Test's macOS legs: its build-exclude
output became start-exclude.
Check contexts change with the jobs: Build & Test keeps its
(ubuntu-latest, node 22.12.0) context and loses the others, and Install
& start's come. As CONTRIBUTING says, a maintainer checks that the
repository's required status checks match. CONTRIBUTING's paragraph on
push-time CI says what runs where, and ci-workflow-pins.test.ts's doc no
longer speaks of a macOS runner running the suite.
…ial policy Request
"connectMcplServer hot-connects a network-only (WebSocket) MCPL server"
ran an in-process fixture that advertised MCPL 0.4 and answered only
initialize and tools/list. connectMcplServer awaits
settlePolicyExchange, so the unanswered featureSets/update waited out
its 15 s timeout: the test took 15.1 s on CI's ubuntu 22.12.0 leg (run
40751371209).
The fixture now advertises 0.5 and answers featureSets/update with
{ accepted: true }, as the suite's other MCPL fixtures do. What the test
checks, the connection, its url, the echo tool listed and gone after the
disconnect, doesn't depend on the grant.
…race "a quiesce abandon whose cancel throws lists the turn as unabandonable, takes its mark back, and resolves" holds its turn in its first tool until after the quiesce returns, so the abandon waited out its 10 s grace: 11.1 s of the describe's 12.8 s on CI's ubuntu 22.12.0 leg (run 40751371209). It sets setAbandonGraceForTests(framework, 50) before the quiesce, as the three tests in stop-without-stream and host-quiesce do; the turn it holds is what the setter's doc asks for.
…leeping 1.15 s Eight tests armed a self-wake, which armSelfWake clamps to at least 1 s, and slept 1150 or 1200 ms to see it fire or not: 9.3 s of the file on CI's ubuntu 22.12.0 leg (run 40751371209), and a verdict that a late timer could flip. They run an EventGate alone, with no framework, and the self-wake is a plain setTimeout whose callback is synchronous (event-gate.ts, armSelfWake). Each now enables t.mock.timers for setTimeout only and ticks 1150 or 1200 ms: the timer fires, or doesn't, inside the tick, and Date stays real, so the notice's timestamp is the clock's. The unused sleep helper goes.
Seven tests make the first stream end in a transient error and check the routing across the retry. Each waited the default first retry delay, membrane 0.8.0's retryDelayMs of 1000 under DefaultErrorPolicy: about 1 s each, of the file's 11.5 s on CI's ubuntu 22.12.0 leg (run 40751371209). None uses the delay as a window: the ' !' and late-delivery tests hold a post until they release it, and wait on the retry's start or its finished response; the three across-a-retry cases check what was routed once runUntilIdle() has returned. They now pass AgentFramework.create an errorPolicy, the public option, that retries once with no delay: "an error retry keeps a ' !' whose envelope is delivered after the retry starts" and "a retried turn's text waits for the replaced stream's late deliveries", each in both modes, and the three "across an error retry" cases (continueTurn's retry path; its capacity path keeps the default). createFramework takes the policy as a third argument. "hybrid: a capacity-held turn's later compile receipts the drafts its first stream held" is unchanged: its 1 s is the provider acceleration cooldown, which the test waits out by polling, not an error retry.
fingerprintVouches decides whether the workspace may trust a file's recorded hash without reading the file again. At e85c57d, the whole suite passed with any one of these taken out of it: size, mtime, inode or device left out of the comparison; ctime left out of the window's newest stamp; the 2,000 ms window moved to 1,000 ms or to 4,000 ms; or its edge made inclusive. ctime left out of the comparison alone is caught by the existing "any field differing" assertion. The code and tests this describes are unchanged between e85c57d and the commit this one is based on. Reading the rule since found three more terms that, by reading, no existing test pins: mtime in the window's newest stamp, mtime's comparison to the nanosecond, and the window's rounding of nanoseconds down to whole milliseconds. A real change to size, mtime, inode or device moves ctime too, so tests through the file system can't tell those terms apart. The new file calls fingerprintVouches directly and pins each term as a requirement on what it answers, on a file system whose ctime it trusts: - it vouches when none of size, mtime, ctime, inode and device has changed and the stamps are more than 2,000 ms older than the hash; - it doesn't once any one of those five alone has changed, by as little as a nanosecond for a stamp; - it doesn't for stamps exactly 2,000 ms before the hash, and does for stamps 2,000 ms and 1 ns before it; - it doesn't for a ctime inside the window when mtime is older, or for an mtime inside the window when ctime is older.
Three-way's "a recorded fingerprint vouches only for an unchanged, non-racy file on a ctime-keeping filesystem" held five assertions, and workspace-fingerprint.test.ts now pins three of them: it vouches for an unchanged fingerprint past the window, not for one inside it, and not once a field differs. Its other two move into the new file as tests of their own: it never vouches on a file system whose ctime the workspace doesn't trust, and not when no fingerprint was recorded. The old test goes, so a later change to the fingerprint meets one test of each requirement, in the file a reader would look in.
added 2 commits
October 29, 2026 01:21
main 0f11975 brings anima-research#864, anima-research#876, anima-research#881 and the other merges since 62a126c. One file conflicted, test/workspace-three-way.test.ts: - its observe.js import takes both sides: this branch's RACY_WINDOW_MS and anima-research#864's ROOT_ON_ANOTHER_DEVICE, ROOT_REPLACED, filesystemKeepsBirthTime and fsTypeKeepsBirthTime; - anima-research#864's new test, "a filesystem keeps a birth time fixed only where it is listed, and only on Linux", goes in before "no restored timestamp hides a change", without the comment above that test, which this branch removed.
…s its SIGTERM and SIGKILL waits do main's endOnPipeError (from anima-research#876) gives a child whose pipe failed STDIO_EXIT_GRACE_MS to exit on its own before ending it, reading the constant directly. This branch's escalate() reads the child's test grace (setStdioExitGraceForTests) for the waits after SIGTERM and SIGKILL. So a child given a test's grace still waited 2 s after a pipe error. StdioTransport.exitGrace() is now the one lookup, the child's test grace or STDIO_EXIT_GRACE_MS, and the pipe-failure wait and escalate() both call it. Production's grace is unchanged: no child has a test grace outside the tests. test/mcpl-transport-close.test.ts: "a child given a test's exit grace waits only that grace after a pipe failure, as after SIGTERM and SIGKILL" gives a child that can exit by no means (its kill() a no-op) a 50 ms grace, mocks setTimeout, and fails its stdin. It ticks the clock: nothing is sent at 49 ms, SIGTERM at 50, SIGKILL 50 ms later, and 'unreaped' names 50ms after 50 more. With the pipe-failure wait reading STDIO_EXIT_GRACE_MS again, no SIGTERM comes at 50 ms and it fails. "a failed write to a child that ignores SIGTERM ends it with SIGKILL 2 s later" keeps the real 2 s, with no test grace.
nissa-seru
force-pushed
the
alfred/suite-speed
branch
from
October 29, 2026 05:59
da5ef38 to
a3f568c
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
This pull request changes tests and CI so the suite spends less time waiting, and CI runs the whole suite once, on Linux at Node 22.12.0. It replaces many fixed-time waits in the tests it touches with waits for the event they stand for, a mocked clock, retries with no delay, or a 50 ms grace where the test itself keeps what the grace waits for from happening. Changes says which waits stay as they were. Eight test files gain
LONGER_THAN_ANY_START_MS, a 60 s give-up, for waits in them. On macOS, and on Ubuntu at Node 24 and the newest release, new Install & start legs check that the packed package installs on its own and loads. Run alone, the fourteen files we timed both ways took 582.4 s onmainand 260.8 s with an earlier version of this change (under Tests). The change is tests and CI, plus two setters insrc/for tests, whichsrc/index.tsdoesn't re-export.Problem
At
main's62a126c7, run alone, the fourteen test files this change touches and we timed took 582.4 s, against 260.8 s with an earlier version of this change (the table under Tests). Their tests wait out fixed times: stop graces for children the tests made unkillable, abandon graces for turns the tests hold past them, the timeout of the initial policy request (featureSets/update) where the fixture never answers it, retry delays and fixed sleeps. CI's Build & Test runs the whole suite on Ubuntu and macOS at several Node versions.Changes
Waits for an event, not for time:
src/mcpl/transport.tsaddssetStdioExitGraceForTests(child, ms), which keepsmsin aWeakMapthat:521reads in place ofSTDIO_EXIT_GRACE_MS(2 s) for a child that has an entry.mcp-server-ownership,mcp-modern-outcomes,mcpl-legacy-lifetimeandmcpl-stderr-tailgive 50 ms to children whosekill()the test made a no-op, and to no other child. Two tests keep 2 s on such a child.src/framework.tsaddssetAbandonGraceForTests(framework, ms), whose doc says it's for tests only, and thatframework's abandon then takesmsas its grace in place ofABANDON_GRACE_MS(10 s). Four tests that hold a turn past the grace take 50 ms, inhost-quiesce,stop-without-streamandturn-continuation.host-quiesce's MCPL push fixture andmcpl-runtime-lifecycle's hot-connect fixture now advertise MCPL 0.5 and answer the initial policy request (featureSets/update). The host-quiesce test also asserts that the flushed push is answered with no error andaccepted: true.reply-server-routeswaits for the agent's queued posts (postsSettled) in place of three loops of three 300 ms sleeps, and drops three 500 ms sleeps and one more 300 ms sleep.event-gate-selfwake's eight tests mocksetTimeoutand tick it.provider-acceleration-cooldownmocksDateto step the cooldown.event-gate-judge's judge-timeout test waits for the abort.mcp-recorded-textcounts name probes at two sizes in place of a wall-clock bound.explicit-prose-routing's error-retry tests giveAgentFramework.createtheerrorPolicyretryAtOnce:maxRetries: 1, and anonInferenceErrorthat answersattempt < 1 ? { retry: true, delayMs: 0 } : { retry: false }.workspace-three-way. Five tests drop or shorten fixed waits. One sets the seeded file's times 60 s back withutimes, one movesDate.now()past the racy window, two hold chokidar's directory reads until the passes started by then have ended, and one takes its siblings' 50 ms scan deadline.Give-ups.
diagnostics-signal,event-gate-judge,failed-create-releases,liveness-watchdog,mcp-modern-connection,mcp-server-bin,mcpl-legacy-lifetimeandprovider-acceleration-cooldowngainLONGER_THAN_ANY_START_MS(60 s) as the give-up of waits in them.pack-installs-alone's install-limit case answersnpm packitself, and its load-limit case times each of its five checks from the start of that check's own hang.pack-installs-alone's 27 cases run four at a time (describe("pack-installs-alone", { concurrency: 4 }, …)).The workspace fingerprint gets a test file of its own,
test/workspace-fingerprint.test.ts, with one test for each term of what a fingerprint vouches for and each edge of its window.workspace-three-way's two outside cases move there, and itsfingerprintstest goes. This file adds coverage, not speed. It comes with this change because it takes over those cases fromworkspace-three-way, whose other tests this change also edits.CI (
.github/workflows/ci.yml, withCONTRIBUTING.mdto match):Build & Test (ubuntu-latest, node 22.12.0)keeps its steps: typecheck, build,npm test, the unforced files and the diagnostic-report check. It's now one leg.Install & start (<os>, node <version>)is new. It runsnpm install, the build andnode scripts/pack-installs-alone.mjs, the checkpack-checkand the publish workflow already run.main: macOS at Node 22.12.0, 24 and the newest release, and Ubuntu at 24 and the newest release.pack-checkandcode-execution-bunare unchanged.Build & Test (ubuntu-latest, node 22.12.0)stays, and the otherBuild & Testnames go. GitHub's API gives"protected": falseformain, and no rules for it.So the suite no longer runs on macOS, or at Node 24 and the newest release. Two comments this change removes said why those legs ran it:
Open pull requests. By
git merge-tree --write-treeat 23:27 UTC, two of the 55 open pull requests merge cleanly ontomain's62a126c7and conflict with this head: #864, intest/workspace-three-way.test.ts, and #850, intest/event-gate-judge.test.ts. Four that already conflict withmainconflict differently with this head.#194also conflicts intest/event-gate-selfwake.test.ts. In#155and#116the conflict regions insrc/framework.ts, and in#370those in.github/workflows/ci.yml, differ from the ones againstmain.Left as they are:
mcpl-stderr-tail's "a handshake that times out carries the last of the server's stderr" still waits out the handshake's 30 s timeout, which its comment names as what it waits for (:108).setAbandonGraceForTeststakes effect.main.Tests
Each changed test file alone, on
mainand with this change. On the files of an earlier version of this change, from a clean checkout of a commit that isn't in this pull request's history, and onmain's62a126c7, each built on its own: Linux, Node 24.22.1, npm 11.19.1 and TypeScript 7.0.2. Each compiled test file ran by itself, one at a time, withnpm test'snodeflags and a TAP reporter. That commit's files differ from this head's,c556485e, only inCONTRIBUTING.md, in comments of.github/workflows/ci.yml,test/ci-workflow-pins.test.tsandtest/workspace-three-way.test.ts, and intest/explicit-prose-routing.test.ts'sretryAtOnce. The repository keeps no lockfile, andpackage.jsongives its direct dependencies as^ranges apart from@animalabs/mcpl-core's exact0.4.0. Bynpm ls --depth=0at 23:00 UTC in the run's own copy, whosenode_modulesdates from the run's install at 21:56:41 UTC and whichmain's side used through a link, they were@animalabs/chronicle0.4.0,@animalabs/context-manager0.23.1,@animalabs/mcpl-core0.4.0,@animalabs/membrane0.8.0,@babel/parser7.29.11,@modelcontextprotocol/clientand@modelcontextprotocol/server2.4.0,@types/node22.20.5,@types/ws8.18.2,chokidar4.0.3,discord.js14.27.0,tsx4.23.15,typescript7.0.2 andws8.22.0.mainmain/ this changemcp-server-ownershipmcp-modern-outcomesmcpl-legacy-lifetimemcpl-stderr-tailstop-without-streamhost-quiescereply-server-routespack-installs-aloneworkspace-three-waymcpl-runtime-lifecycleturn-continuationevent-gate-selfwakeexplicit-prose-routingci-workflow-pinsmainand 260.8 s with this change.workspace-three-wayhas one test fewer with this change because itsfingerprintstest is gone.explicit-prose-routingpassed 120 of 120 in 4.8 s andci-workflow-pins5 of 5 in 0.3 s, at a commit whose copies of both are this head's, andworkspace-three-way386 of 386 in 16.5 s at this head.mcp-modern-outcomes,mcpl-legacy-lifetimeandmcpl-stderr-tail, andmain's run ofstop-without-stream.diagnostics-signalpassed 4 of 4 in 2.3 s,event-gate-judge58 of 58 in 8.7 s,failed-create-releases11 of 11 in 2.7 s,liveness-watchdog3 of 3 in 4.0 s,mcp-modern-connection10 of 10 in 3.5 s,mcp-recorded-text45 of 45 in 2.6 s,mcp-server-bin5 of 5 in 2.5 s,provider-acceleration-cooldown10 of 10 in 7.9 s, andworkspace-fingerprint12 of 12 in 0.2 s.A child that's never reaped. To check that the shortened stop graces still let their tests catch a child that's never reaped, we changed each side's compiled
src/mcpl/transport.jsso a child still alive after SIGKILL's wait counts as reaped, and ran the four files with a 30 s test timeout. Onmainand with this change the same tests failed or timed out, by name: 17 of 40 inmcp-server-ownership, 9 of 59 inmcp-modern-outcomes, 9 of 19 inmcpl-legacy-lifetimeand 1 of 23 inmcpl-stderr-tail. Inmcpl-legacy-lifetime, three of the nine fail onmainat its 10 s give-up and time out with this change at the 30 s test timeout, since its give-up is 60 s.Each change taken back, in a copy of that run's build:
mcpl-runtime-lifecycle's hot-connect fixture without itsfeatureSets/updateanswer: the test passes, in 16.2 s, against 1.1 s with the answer.armSelfWake's delay doubled:event-gate-selfwakefails 5 of its 9 tests.accepted: false:host-quiesce's push test fails at "the flushed push is accepted".scripts/pack-installs-alone.mjsending a timed-out child with SIGTERM: the case whose title opens "a load, a CommonJS bin's parse or the bun probe that outlives its limit fails at the limit even when it handles SIGTERM" fails with "ended 60106.9 ms after the hang started".workspace-fingerprint, in a separate run from a clean checkout of another commit with an earlier version of this change, whosesrc/andtest/workspace-fingerprint.test.tsare this head's, on Linux with Node 24.22.1: its 12 tests passed in each of 3 runs, and each of 15 single changes to the fingerprint's checks failed exactly one test.Not verified
ci.ymlruns Build & Test, with the whole suite, onpull_request.src/, so there's no changelog fragment, and the pull request needs theno-changeloglabel.Alfred-2451, William-2616 and Hilda-2580 made the commits, and Alfred-2451 ran the timings. Cordelia-2434 read the CI change and weighed what each part adds, and Amos-2081 read the per-child stop grace. Harriet-2914 read eight of William-2616's commits, the load fixes, and Alfred-2451 read the other three, the watchdog commits. Duncan-2410 read the fingerprint tests. Daniel-2612 wrote this description.