Skip to content

Flaky daemon smoke test: findFreePort→bind race causes intermittent test-deno /health Connection refused #137

Description

@taras

Summary

The embedded smoke test packages/testing/tests/smoke.test.ts (golden EMBEDDED_TESTS) intermittently fails on the test-deno CI job (ubuntu-latest) at the "A daemon serves requests until its scope closes" chapter, defined in smoke-test/Guide/Daemons.md.

Failure signature:

TestFailureError: 1 of 47 tests failed
A daemon serves requests until its scope closes: error sending request for url
(http://127.0.0.1:<port>/health): client error (Connect):
tcp connect error: Connection refused (os error 111)

Evidence that it is a pre-existing flake, not code-caused

The daemon chapter is byte-identical across three consecutive PR #136 commits, yet CI results differ:

committest-deno
d57f9ebpass
47c467bfail (this signature)
e7bce64pass

Same test, same daemon chapter, different outcomes → flaky, unrelated to the PR's code. Locally deno task test passes 115 tests / 922 steps, and the daemon command runs fine (Node is present on the runner via the pre-installed ubuntu-latest toolchain; test-deno uses only setup-deno).

The chapter

smoke-test/Guide/Daemons.md does:

  1. js evalconst daemonPort = yield* findFreePort()
  2. bash daemon execnode -e "require('http').createServer(...).listen({daemonPort},'127.0.0.1')"
  3. js evalwhen(() => fetch(daemonUrl + "/health").expect(), { timeout: 5000, interval: 50 })
  4. curl + <AssertEquals … expected={"\ndaemon-ok"} />

Likely root cause

The classic findFreePort() → daemon-bind race: findFreePort probes a port, closes its socket, and returns the number; the node daemon then binds it. On a contended runner another process can grab the port in between → EADDRINUSE → the daemon exits → /health gets Connection refused for the full 5 s window → the chapter fails.

The daemon runs via daemonFactory (packages/core/src/modifiers/daemon.ts) using @effectionx/processdaemon(cmd, { shell: true }), which backgrounds the process, so the child's stderr and exit status are not surfaced — hence the true cause is currently invisible in CI.

Suggested approach (do not just bump the timeout)

  1. First capture the daemon child's exit status + stderr on the runner (confirm EADDRINUSE vs. a spawn/other failure) — e.g. redirect the node -e stderr to a file and surface it on when() failure, or add a one-off diagnostic step.
  2. Fix the root cause (bind-with-retry on EADDRINUSE, or bind to port 0 and report the actual port instead of findFreePort + fixed-port), in a separate minimal PR against main.
  3. Note: changing this chapter requires updating the EMBEDDED_TESTS golden list in packages/testing/tests/smoke.test.ts.

Surfaced while landing PR #136 (agent/acp-stack-07-acp-client); that PR is otherwise green and its failure was solely this flake.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    flakeIntermittent or timing-sensitive failure that can pass without a code change

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions