Skip to content

fix: keep-alive Durable Object test harness and Studio teardown cleanup (#116) - #124

Merged
Madraka merged 2 commits into
mainfrom
fix/issue-116-miniflare-sockets
Oct 25, 2026
Merged

Madraka merged 2 commits into
mainfrom
fix/issue-116-miniflare-sockets

Conversation

@Madraka

@Madraka Madraka commented Oct 25, 2026

Copy link
Copy Markdown
Contributor

What

Fixes #116.

Durable Object suites ran out of local ports (EADDRNOTAVAIL).

  • Miniflare 4's dispatchFetch sets undici's reset on every request: it sends connection: close, so each call costs one new TCP connection and one local port in TIME_WAIT for 30 s.
  • RemoteDoSqliteDriver (@cloudhal/storage/testing, createMiniflareDoSqlite) sends every SqlDriver call that way, and SqlBlobStore adds more.
  • On macOS, core-ts's store.test.ts peaked at 16,344 TIME_WAIT sockets against 16,384 ephemeral ports, and 37 tests in its Durable Object tail failed with EADDRNOTAVAIL.
  • Fix: the harness now sends calls straight to the entry socket that await mf.ready resolves to, over a node:http keep-alive Agent (4 sockets, 1 s idle close).
  • A non-200 response or a connection closed mid-response rejects with a plain Error, so a broken harness can never satisfy toThrow(StorageError).
  • No Miniflare patch, so the ADR-0012 pin stays. No change to the test Worker or to the public { driver(db?), dispose() } shape.
  • Patch changeset for @cloudhal/storage.

Studio teardown flake: "window is not defined" (CI run 39959905949).

  • Studio's Vitest has no globals, so Testing Library never registered its own afterEach(cleanup).
  • Trees stayed mounted after a file ended. Base UI / Floating UI work then committed outside act, and React's passive flush ran after happy-dom had removed window.
  • apps/studio/test/setup.ts now runs cleanup() and vi.useRealTimers() after every test, and drains three setImmediate rounds after each file.

Verification (local, sequential):

Before After
pnpm --filter @cloudhal/core-ts test 37 failed (EADDRNOTAVAIL), 337 s 988/988 twice in a row, 0 EADDRNOTAVAIL, ~350 s
Peak TIME_WAIT 16,344 ~4,000 (the D1 suites' startup burst; DO-only run: 11)
@cloudhal/storage do-sqlite.test.ts 17/17 17/17
components.test.tsx × 10 flaky in CI 10/10, no unhandled errors
Studio – 79 files, 1275 tests, 0 unhandled, 0 act() warnings

Also run: biome, typecheck for storage and Studio, size:check, design:lint.

Process: three independent reviewers checked the change, and two of their findings held up when a separate agent tried to refute them. Both are fixed in this PR: the mid-response close now settles the promise (reproduced with a small Node script), and harness HTTP failures are a plain Error.

Found along the way, not in this PR:

  • packages/mcp-server/src/render/checks/content.ts:72 writes /\s/ inside a template literal, so the in-page empty-section check tests for the letter s, not whitespace. pnpm lint:fix "fixes" it to /s/.
  • The published @cloudhal/storage/testing (and analytics, comments, schedules) ./testing exports point at files build-packages.ts skips.

Checklist

  • pnpm check and cargo test --workspace pass locally: the affected packages' typecheck and tests, size:check and design:lint were run one at a time (the owner's machine freezes when suites run in parallel). Full pnpm check and cargo were not run; no Rust or contract code changed.
  • Contract change? None.
  • User-visible change in a published package? .changeset/do-sqlite-keepalive.md (@cloudhal/storage: patch)
  • Commits are signed off (git commit -s, DCO)
  • Docs / AGENTS.md updated if behaviour or commands changed: storage README

🤖 Generated with Claude Code

Madraka and others added 2 commits October 25, 2026 08:51
#116)

Miniflare 4's dispatchFetch sets undici's reset on every request, so each
RemoteDoSqliteDriver call opened a TCP connection and left a local port in
TIME_WAIT for 30 s. core-ts's store.test.ts reached ~16.3k TIME_WAIT sockets
on macOS (16,384 ephemeral ports) and its Durable Object tail failed with
EADDRNOTAVAIL (37 tests).

createMiniflareDoSqlite() now sends calls straight to the entry socket that
`await mf.ready` resolves to, over a node:http keep-alive Agent (4 sockets,
1 s idle close). A non-200 or a connection closed mid-response rejects with a
plain Error, so a broken harness can never satisfy toThrow(StorageError). The
public shape { driver(db?), dispose() } is unchanged.

Two consecutive `pnpm --filter @cloudhal/core-ts test` runs: 988/988, no
EADDRNOTAVAIL, TIME_WAIT peak ~4k (the D1 suites' startup burst).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Signed-off-by: Madraka <[email protected]>
#116)

Studio's Vitest runs without globals, so Testing Library never registered
its own cleanup. Files that do not call cleanup() kept their trees mounted;
Base UI / Floating UI work committed outside act after the file ended, and
React's passive flush (setImmediate) read window after happy-dom's teardown:
"ReferenceError: window is not defined" (CI run 39959905949).

setup.ts now runs cleanup() and vi.useRealTimers() after every test, and
drains three setImmediate rounds after each file. components.test.tsx 10/10
clean; Studio 79 files, 1275 tests, no unhandled errors or act() warnings.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Signed-off-by: Madraka <[email protected]>
@Madraka
Madraka merged commit 8911a12 into main Oct 25, 2026
4 checks passed
@Madraka
Madraka deleted the fix/issue-116-miniflare-sockets branch October 25, 2026 11:47
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.

test: local Miniflare Durable Object suites fail with EADDRNOTAVAIL (socket exhaustion)

1 participant