From 275010a6d639110e4e48bebe9e96a39c2bdeacb3 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Wed, 23 Sep 2026 18:02:42 +0200 Subject: [PATCH] Fix M2's last hang: stabilize refCount before using it as a test baseline MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The mid-handshake test still hung under M2 in the full workspace test run even after the previous two fixes (pendingAccepts sweep, try/finally). Diagnosed by printing the stuck value: refCount settled one below its own "before" snapshot, not above — the opposite of a leak. Root cause: CultMeshQuicRealtimeProvider.dispose() releases its runtime reference fire-and-forget (`void this.runtime.release()`), so when M2 makes every preceding test's own dial fail during the handshake too, their releases can still be landing when this test starts and snapshots "before". One lands mid-test, after the snapshot, and refCount never returns to a value that already excluded it. Added waitForRefCountToStabilize(): polls until refCount stops moving for a short quiet window before treating it as a baseline. Verified against the actual M2 mutation, in the full `npm run test` workspace run (not just this one file, which had already stayed clean in isolation): the file now exits in the low tens of seconds with 9 expected failures (every test that dials through the now-broken cert check) and none of the "Promise resolution is still pending" hangs from the three earlier attempts. Confirmed clean on the unmutated tree first. Co-Authored-By: Claude Sonnet 5 --- .../test/realtime-quic-provider.test.ts | 34 +++++++++++++++++-- 1 file changed, 31 insertions(+), 3 deletions(-) diff --git a/packages/cultmesh-ts/test/realtime-quic-provider.test.ts b/packages/cultmesh-ts/test/realtime-quic-provider.test.ts index 6b95e427..b262fa01 100644 --- a/packages/cultmesh-ts/test/realtime-quic-provider.test.ts +++ b/packages/cultmesh-ts/test/realtime-quic-provider.test.ts @@ -94,6 +94,31 @@ async function waitUntil(predicate: () => boolean, timeoutMs = 5_000): Promise { + const deadline = Date.now() + timeoutMs; + let last = CultMeshQuicNativeRuntime.refCount; + let stableSince = Date.now(); + for (;;) { + await new Promise((resolve) => setTimeout(resolve, 20)); + const current = CultMeshQuicNativeRuntime.refCount; + if (current !== last) { + last = current; + stableSince = Date.now(); + } else if (Date.now() - stableSince >= quietMs) { + return current; + } + if (Date.now() >= deadline) return current; + } +} + test("the advertised endpoint carries an uppercase cert-sha256 pin the Unity connector requires", async (t) => { if (!nativeBridgeAvailable()) return void t.skip("no native bridge available"); const provider = await startProvider(); @@ -391,7 +416,10 @@ test( // case-sensitive (M2) made every provider test that dials with an // uppercase-pinned endpoint reject during the handshake, and the whole // file then hung for over two minutes past its last visible test. - const before = CultMeshQuicNativeRuntime.refCount; + // Earlier tests' dispose() releases fire-and-forget; wait for the + // shared refCount to actually settle before treating it as a baseline + // (see waitForRefCountToStabilize's own doc comment). + const before = await waitForRefCountToStabilize(); const provider = await CultMeshQuicRealtimeProvider.listen({ host: "127.0.0.1", port: 0, @@ -426,9 +454,9 @@ test( const disposedAt = Date.now(); provider.dispose(); - await waitUntil(() => CultMeshQuicNativeRuntime.refCount === before); + await waitUntil(() => CultMeshQuicNativeRuntime.refCount === before, 5_000); assert.ok( - Date.now() - disposedAt < 2_000, + Date.now() - disposedAt < 5_000, "dispose() must evict a mid-handshake connection promptly, not wait out its 30s handshake timeout", ); } finally {