E2E: give test servers ports nothing else can take (fixes "bind(): Address already in use")
Every e2e file picked ports by listening on port 0 and closing the socket, then passing the number to wrangler dev. Until wrangler bound it, anything binding port 0 (workerd in another wrangler dev or `wrangler d1 execute`, a docs server, an outgoing connection) could be handed the same port, and the Workers runtime died at start. Seen on claude/ci-status-in-claude (OAuth) and claude/indexnow-fallback (real histories). test/e2e/ports.mjs replaces the 18 copies (and the docs screenshot script's): ports come from below the kernel's ephemeral range, which port 0 binds and outgoing connections never use, each reserved with a lock file so files running side by side never share one. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01M5tHXfgBQaLEPM9iUPrp8n
- CI / Typecheck and bundlesuccess
- CI / Unit tests (1/2)success
- CI / Unit tests (2/2)success
- End-to-end / End-to-end (agent reviews, tasks, Today)success
- End-to-end / End-to-end (git, PRs, Actions)success
- End-to-end / End-to-end (large pushes)success
- End-to-end / End-to-end (passkeys, registry, email)success
- End-to-end / End-to-end (real histories)success
- End-to-end / End-to-end (runner Docker)success
- End-to-end / End-to-end (security)success
22 files changed+147−184
Files (22)
- apps/diffinery/scripts/docs-screenshots.mjs+2−8
- apps/diffinery/test/e2e/accounts.test.mjs+1−10
- apps/diffinery/test/e2e/agent-setup.test.mjs+1−9
- apps/diffinery/test/e2e/agentic.test.mjs+1−10
- apps/diffinery/test/e2e/agents-harness.mjs+1−10
- apps/diffinery/test/e2e/e2e.test.mjs+1−10
- apps/diffinery/test/e2e/early-access.test.mjs+1−9
- apps/diffinery/test/e2e/editor-smoke.mjs+1−10
- apps/diffinery/test/e2e/email.test.mjs+1−10
- apps/diffinery/test/e2e/graphql.test.mjs+1−10
- apps/diffinery/test/e2e/jetbrains-smoke.mjs+1−9
- apps/diffinery/test/e2e/moderation.test.mjs+1−9
- apps/diffinery/test/e2e/oauth.test.mjs+1−9
- apps/diffinery/test/e2e/passkeys.test.mjs+1−10
- apps/diffinery/test/e2e/ports.mjs+125−0
- apps/diffinery/test/e2e/registry.test.mjs+1−9
- apps/diffinery/test/e2e/review.test.mjs+1−10
- apps/diffinery/test/e2e/security-harness.mjs+1−10
- apps/diffinery/test/e2e/security-txt.test.mjs+1−10
- apps/diffinery/test/e2e/status.test.mjs+1−10
- apps/diffinery/test/e2e/tasks.test.mjs+1−1
- apps/diffinery/test/e2e/terminals.test.mjs+1−1
modifiedapps/diffinery/scripts/docs-screenshots.mjs+2−8View file
| @@ -13,19 +13,13 @@ | ||
| 13 | 13 | import { tmpdir } from "node:os"; |
| 14 | 14 | import { join, dirname, resolve } from "node:path"; |
| 15 | 15 | import { fileURLToPath } from "node:url"; |
| 16 | -import { createServer } from "node:net"; | |
| 17 | 16 | import { chromium } from "playwright"; |
| 17 | +import { freePort } from "../test/e2e/ports.mjs"; | |
| 18 | 18 | |
| 19 | 19 | const APP = resolve(dirname(fileURLToPath(import.meta.url)), ".."); |
| 20 | 20 | const OUT = join(APP, "docs/images"); |
| 21 | 21 | const PUBLIC = "https://diffinery.com"; |
| 22 | -const port = await new Promise((res) => { | |
| 23 | - const srv = createServer(); | |
| 24 | - srv.listen(0, "127.0.0.1", () => { | |
| 25 | - const { port } = srv.address(); | |
| 26 | - srv.close(() => res(port)); | |
| 27 | - }); | |
| 28 | -}); | |
| 22 | +const port = await freePort(); | |
| 29 | 23 | // The browser talks to the Worker here; the pages themselves say diffinery.com. |
| 30 | 24 | const ORIGIN = `http://localhost:${port}`; |
| 31 | 25 | const only = process.argv.slice(2); |
modifiedapps/diffinery/test/e2e/accounts.test.mjs+1−10View file
| @@ -13,10 +13,10 @@ | ||
| 13 | 13 | import { tmpdir } from "node:os"; |
| 14 | 14 | import { join, dirname, resolve } from "node:path"; |
| 15 | 15 | import { fileURLToPath } from "node:url"; |
| 16 | -import { createServer } from "node:net"; | |
| 17 | 16 | import { test, before, after } from "node:test"; |
| 18 | 17 | import assert from "node:assert/strict"; |
| 19 | 18 | import { migratedState } from "./d1-state.mjs"; |
| 19 | +import { freePort } from "./ports.mjs"; | |
| 20 | 20 | |
| 21 | 21 | const APP = resolve(dirname(fileURLToPath(import.meta.url)), "../.."); |
| 22 | 22 | const work = mkdtempSync(join(tmpdir(), "diffinery-accounts-")); |
| @@ -30,15 +30,6 @@ | ||
| 30 | 30 | const T = { sully: tok("sully"), alice: tok("alice"), alicebot: tok("alicebot"), bob: tok("bob"), carl: tok("carl"), erin: tok("erin"), dan: tok("dan") }; |
| 31 | 31 | const S = { sully: sess("sully"), sullyOld: sess("sullyold"), alice: sess("alice"), aliceOther: sess("aliceother"), bob: sess("bob"), carl: sess("carl"), erin: sess("erin"), dan: sess("dan") }; |
| 32 | 32 | |
| 33 | -const freePort = () => | |
| 34 | - new Promise((res) => { | |
| 35 | - const s = createServer(); | |
| 36 | - s.listen(0, "127.0.0.1", () => { | |
| 37 | - const { port } = s.address(); | |
| 38 | - s.close(() => res(port)); | |
| 39 | - }); | |
| 40 | - }); | |
| 41 | - | |
| 42 | 33 | const sleep = (ms) => new Promise((r) => setTimeout(r, ms)); |
| 43 | 34 | async function until(what, fn, timeoutMs = 120_000) { |
| 44 | 35 | const end = Date.now() + timeoutMs; |
modifiedapps/diffinery/test/e2e/agent-setup.test.mjs+1−9View file
| @@ -18,10 +18,10 @@ | ||
| 18 | 18 | import { tmpdir } from "node:os"; |
| 19 | 19 | import { join, dirname, resolve } from "node:path"; |
| 20 | 20 | import { fileURLToPath } from "node:url"; |
| 21 | -import { createServer } from "node:net"; | |
| 22 | 21 | import { test, before, after } from "node:test"; |
| 23 | 22 | import assert from "node:assert/strict"; |
| 24 | 23 | import { migratedState } from "./d1-state.mjs"; |
| 24 | +import { freePort } from "./ports.mjs"; | |
| 25 | 25 | |
| 26 | 26 | const APP = resolve(dirname(fileURLToPath(import.meta.url)), "../.."); |
| 27 | 27 | const work = mkdtempSync(join(tmpdir(), "diffinery-agent-setup-")); |
| @@ -35,14 +35,6 @@ | ||
| 35 | 35 | const T = { ada: tok("ada"), bob: tok("bob"), wes: tok("wes") }; |
| 36 | 36 | const S = { ada: `ada-session-${randomBytes(12).toString("hex")}`, bob: `bob-session-${randomBytes(12).toString("hex")}`, wes: `wes-session-${randomBytes(12).toString("hex")}` }; |
| 37 | 37 | |
| 38 | -const freePort = () => | |
| 39 | - new Promise((res) => { | |
| 40 | - const s = createServer(); | |
| 41 | - s.listen(0, "127.0.0.1", () => { | |
| 42 | - const { port } = s.address(); | |
| 43 | - s.close(() => res(port)); | |
| 44 | - }); | |
| 45 | - }); | |
| 46 | 38 | const sleep = (ms) => new Promise((r) => setTimeout(r, ms)); |
| 47 | 39 | async function until(what, fn, timeoutMs = 120_000) { |
| 48 | 40 | const end = Date.now() + timeoutMs; |
modifiedapps/diffinery/test/e2e/agentic.test.mjs+1−10View file
| @@ -12,10 +12,10 @@ | ||
| 12 | 12 | import { randomBytes } from "node:crypto"; |
| 13 | 13 | import { join, dirname, resolve } from "node:path"; |
| 14 | 14 | import { fileURLToPath } from "node:url"; |
| 15 | -import { createServer } from "node:net"; | |
| 16 | 15 | import { test, before, after } from "node:test"; |
| 17 | 16 | import assert from "node:assert/strict"; |
| 18 | 17 | import { migratedState } from "./d1-state.mjs"; |
| 18 | +import { freePort } from "./ports.mjs"; | |
| 19 | 19 | |
| 20 | 20 | const APP = resolve(dirname(fileURLToPath(import.meta.url)), "../.."); |
| 21 | 21 | const work = mkdtempSync(join(tmpdir(), "diffinery-agentic-e2e-")); |
| @@ -24,15 +24,6 @@ | ||
| 24 | 24 | let devLog = ""; |
| 25 | 25 | let token; |
| 26 | 26 | |
| 27 | -const freePort = () => | |
| 28 | - new Promise((res) => { | |
| 29 | - const s = createServer(); | |
| 30 | - s.listen(0, "127.0.0.1", () => { | |
| 31 | - const { port } = s.address(); | |
| 32 | - s.close(() => res(port)); | |
| 33 | - }); | |
| 34 | - }); | |
| 35 | - | |
| 36 | 27 | async function api(method, path, body, tok = token) { |
| 37 | 28 | const res = await fetch(`${base}/api/v3${path}`, { |
| 38 | 29 | method, |
modifiedapps/diffinery/test/e2e/agents-harness.mjs+1−10View file
| @@ -8,11 +8,11 @@ | ||
| 8 | 8 | import { createRequire } from "node:module"; |
| 9 | 9 | import { join, dirname, resolve } from "node:path"; |
| 10 | 10 | import { fileURLToPath } from "node:url"; |
| 11 | -import { createServer as netServer } from "node:net"; | |
| 12 | 11 | import { createServer as httpServer } from "node:http"; |
| 13 | 12 | import { before, after } from "node:test"; |
| 14 | 13 | import assert from "node:assert/strict"; |
| 15 | 14 | import { migratedState } from "./d1-state.mjs"; |
| 15 | +import { freePort } from "./ports.mjs"; | |
| 16 | 16 | |
| 17 | 17 | export const APP = resolve(dirname(fileURLToPath(import.meta.url)), "../.."); |
| 18 | 18 | export const AGENT = resolve(APP, "../../containers/runner/dist/agent.mjs"); |
| @@ -37,15 +37,6 @@ | ||
| 37 | 37 | export let token; |
| 38 | 38 | export const modelRequests = []; |
| 39 | 39 | |
| 40 | -export const freePort = () => | |
| 41 | - new Promise((res) => { | |
| 42 | - const s = netServer(); | |
| 43 | - s.listen(0, "127.0.0.1", () => { | |
| 44 | - const { port } = s.address(); | |
| 45 | - s.close(() => res(port)); | |
| 46 | - }); | |
| 47 | - }); | |
| 48 | - | |
| 49 | 40 | export async function api(method, path, body, tok = token) { |
| 50 | 41 | const res = await fetch(`${base}/api/v3${path}`, { |
| 51 | 42 | method, |
modifiedapps/diffinery/test/e2e/e2e.test.mjs+1−10View file
| @@ -14,12 +14,12 @@ | ||
| 14 | 14 | import { createHash, randomBytes } from "node:crypto"; |
| 15 | 15 | import { join, dirname, resolve } from "node:path"; |
| 16 | 16 | import { fileURLToPath } from "node:url"; |
| 17 | -import { createServer } from "node:net"; | |
| 18 | 17 | import { crc32, deflateSync } from "node:zlib"; |
| 19 | 18 | import { test, describe, before, after } from "node:test"; |
| 20 | 19 | import assert from "node:assert/strict"; |
| 21 | 20 | import { fakeGitHub, lfsObject } from "./fake-github.mjs"; |
| 22 | 21 | import { migratedState } from "./d1-state.mjs"; |
| 22 | +import { freePort } from "./ports.mjs"; | |
| 23 | 23 | |
| 24 | 24 | const APP = resolve(dirname(fileURLToPath(import.meta.url)), "../.."); |
| 25 | 25 | const AGENT = resolve(APP, "../../containers/runner/dist/agent.mjs"); |
| @@ -34,15 +34,6 @@ | ||
| 34 | 34 | let token; |
| 35 | 35 | let agentToken; |
| 36 | 36 | |
| 37 | -const freePort = () => | |
| 38 | - new Promise((res) => { | |
| 39 | - const s = createServer(); | |
| 40 | - s.listen(0, "127.0.0.1", () => { | |
| 41 | - const { port } = s.address(); | |
| 42 | - s.close(() => res(port)); | |
| 43 | - }); | |
| 44 | - }); | |
| 45 | - | |
| 46 | 37 | async function api(method, path, body, tok = token) { |
| 47 | 38 | const send = () => |
| 48 | 39 | fetch(`${base}/api/v3${path}`, { |
modifiedapps/diffinery/test/e2e/early-access.test.mjs+1−9View file
| @@ -16,24 +16,16 @@ | ||
| 16 | 16 | import { tmpdir } from "node:os"; |
| 17 | 17 | import { join, dirname, resolve } from "node:path"; |
| 18 | 18 | import { fileURLToPath } from "node:url"; |
| 19 | -import { createServer } from "node:net"; | |
| 20 | 19 | import { request as httpRequest } from "node:http"; |
| 21 | 20 | import { test, after } from "node:test"; |
| 22 | 21 | import assert from "node:assert/strict"; |
| 23 | 22 | import { migratedState } from "./d1-state.mjs"; |
| 23 | +import { freePort } from "./ports.mjs"; | |
| 24 | 24 | |
| 25 | 25 | const APP = resolve(dirname(fileURLToPath(import.meta.url)), "../.."); |
| 26 | 26 | const work = mkdtempSync(join(tmpdir(), "diffinery-early-access-e2e-")); |
| 27 | 27 | const running = []; |
| 28 | 28 | |
| 29 | -const freePort = () => | |
| 30 | - new Promise((res) => { | |
| 31 | - const s = createServer(); | |
| 32 | - s.listen(0, "127.0.0.1", () => { | |
| 33 | - const { port } = s.address(); | |
| 34 | - s.close(() => res(port)); | |
| 35 | - }); | |
| 36 | - }); | |
| 37 | 29 | const hash = (s) => createHash("sha256").update(s).digest("hex"); |
| 38 | 30 | const sleep = (ms) => new Promise((r) => setTimeout(r, ms)); |
| 39 | 31 |
modifiedapps/diffinery/test/e2e/editor-smoke.mjs+1−10View file
| @@ -14,12 +14,12 @@ | ||
| 14 | 14 | import { spawn, execFileSync } from "node:child_process"; |
| 15 | 15 | import { createHash, randomBytes } from "node:crypto"; |
| 16 | 16 | import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync, chmodSync } from "node:fs"; |
| 17 | -import { createServer } from "node:net"; | |
| 18 | 17 | import { tmpdir } from "node:os"; |
| 19 | 18 | import { join, dirname, resolve } from "node:path"; |
| 20 | 19 | import { fileURLToPath } from "node:url"; |
| 21 | 20 | import assert from "node:assert/strict"; |
| 22 | 21 | import { chromium, _electron } from "playwright"; |
| 22 | +import { freePort } from "./ports.mjs"; | |
| 23 | 23 | |
| 24 | 24 | const APP = resolve(dirname(fileURLToPath(import.meta.url)), "../.."); |
| 25 | 25 | const EDITOR = process.env.EDITOR_BIN; |
| @@ -35,14 +35,6 @@ | ||
| 35 | 35 | const CHARLES_TOKEN = `dfy_${randomBytes(24).toString("hex")}`; |
| 36 | 36 | let devLog = ""; |
| 37 | 37 | |
| 38 | -const freePort = () => | |
| 39 | - new Promise((res) => { | |
| 40 | - const s = createServer(); | |
| 41 | - s.listen(0, "127.0.0.1", () => { | |
| 42 | - const { port } = s.address(); | |
| 43 | - s.close(() => res(port)); | |
| 44 | - }); | |
| 45 | - }); | |
| 46 | 38 | async function until(what, fn, timeoutMs = 60_000) { |
| 47 | 39 | const end = Date.now() + timeoutMs; |
| 48 | 40 | while (Date.now() < end) { |
| @@ -322,7 +314,6 @@ | ||
| 322 | 314 | assert.ok(grants.length === 1 && grants[0].revoked_at, "the connection is revoked"); |
| 323 | 315 | console.log("signed out: credential removed, connection revoked"); |
| 324 | 316 | |
| 325 | - | |
| 326 | 317 | await app.close(); |
| 327 | 318 | cleanup(); |
| 328 | 319 | if (process.env.KEEP !== "1") rmSync(work, { recursive: true, force: true }); |
modifiedapps/diffinery/test/e2e/email.test.mjs+1−10View file
| @@ -17,11 +17,11 @@ | ||
| 17 | 17 | import { createHmac, randomBytes } from "node:crypto"; |
| 18 | 18 | import { join, dirname, resolve } from "node:path"; |
| 19 | 19 | import { fileURLToPath } from "node:url"; |
| 20 | -import { createServer } from "node:net"; | |
| 21 | 20 | import { test, before, after } from "node:test"; |
| 22 | 21 | import assert from "node:assert/strict"; |
| 23 | 22 | import { chromium } from "playwright"; |
| 24 | 23 | import { migratedState } from "./d1-state.mjs"; |
| 24 | +import { freePort } from "./ports.mjs"; | |
| 25 | 25 | |
| 26 | 26 | const APP = resolve(dirname(fileURLToPath(import.meta.url)), "../.."); |
| 27 | 27 | const AGENT = resolve(APP, "../../containers/runner/dist/agent.mjs"); |
| @@ -33,15 +33,6 @@ | ||
| 33 | 33 | let me; |
| 34 | 34 | const eventsSecret = randomBytes(24).toString("hex"); |
| 35 | 35 | |
| 36 | -const freePort = () => | |
| 37 | - new Promise((res) => { | |
| 38 | - const s = createServer(); | |
| 39 | - s.listen(0, "127.0.0.1", () => { | |
| 40 | - const { port } = s.address(); | |
| 41 | - s.close(() => res(port)); | |
| 42 | - }); | |
| 43 | - }); | |
| 44 | - | |
| 45 | 36 | async function api(method, path, body, tok = token) { |
| 46 | 37 | const res = await fetch(`${base}/api/v3${path}`, { |
| 47 | 38 | method, |
modifiedapps/diffinery/test/e2e/graphql.test.mjs+1−10View file
| @@ -12,10 +12,10 @@ | ||
| 12 | 12 | import { tmpdir } from "node:os"; |
| 13 | 13 | import { join, dirname, resolve } from "node:path"; |
| 14 | 14 | import { fileURLToPath } from "node:url"; |
| 15 | -import { createServer } from "node:net"; | |
| 16 | 15 | import { test, before, after } from "node:test"; |
| 17 | 16 | import assert from "node:assert/strict"; |
| 18 | 17 | import { migratedState } from "./d1-state.mjs"; |
| 18 | +import { freePort } from "./ports.mjs"; | |
| 19 | 19 | |
| 20 | 20 | const APP = resolve(dirname(fileURLToPath(import.meta.url)), "../.."); |
| 21 | 21 | const work = mkdtempSync(join(tmpdir(), "diffinery-graphql-e2e-")); |
| @@ -23,15 +23,6 @@ | ||
| 23 | 23 | let base; |
| 24 | 24 | let log = ""; |
| 25 | 25 | |
| 26 | -const freePort = () => | |
| 27 | - new Promise((res) => { | |
| 28 | - const s = createServer(); | |
| 29 | - s.listen(0, "127.0.0.1", () => { | |
| 30 | - const { port } = s.address(); | |
| 31 | - s.close(() => res(port)); | |
| 32 | - }); | |
| 33 | - }); | |
| 34 | - | |
| 35 | 26 | const hash = (s) => createHash("sha256").update(s).digest("hex"); |
| 36 | 27 | const T = { ann: `dfy_ann_${randomBytes(12).toString("hex")}`, annRead: `dfy_anr_${randomBytes(12).toString("hex")}`, bob: `dfy_bob_${randomBytes(12).toString("hex")}` }; |
| 37 | 28 |
modifiedapps/diffinery/test/e2e/jetbrains-smoke.mjs+1−9View file
| @@ -16,12 +16,12 @@ | ||
| 16 | 16 | import { spawn, execFileSync } from "node:child_process"; |
| 17 | 17 | import { createHash, randomBytes } from "node:crypto"; |
| 18 | 18 | import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync, chmodSync, copyFileSync } from "node:fs"; |
| 19 | -import { createServer } from "node:net"; | |
| 20 | 19 | import { tmpdir } from "node:os"; |
| 21 | 20 | import { join, dirname, resolve } from "node:path"; |
| 22 | 21 | import { fileURLToPath } from "node:url"; |
| 23 | 22 | import assert from "node:assert/strict"; |
| 24 | 23 | import { chromium } from "playwright"; |
| 24 | +import { freePort } from "./ports.mjs"; | |
| 25 | 25 | |
| 26 | 26 | const APP = resolve(dirname(fileURLToPath(import.meta.url)), "../.."); |
| 27 | 27 | const IDE = process.env.IDE_HOME; |
| @@ -38,14 +38,6 @@ | ||
| 38 | 38 | const CHARLES_TOKEN = `dfy_${randomBytes(24).toString("hex")}`; |
| 39 | 39 | let devLog = ""; |
| 40 | 40 | |
| 41 | -const freePort = () => | |
| 42 | - new Promise((res) => { | |
| 43 | - const s = createServer(); | |
| 44 | - s.listen(0, "127.0.0.1", () => { | |
| 45 | - const { port } = s.address(); | |
| 46 | - s.close(() => res(port)); | |
| 47 | - }); | |
| 48 | - }); | |
| 49 | 41 | async function until(what, fn, timeoutMs = 60_000) { |
| 50 | 42 | const end = Date.now() + timeoutMs; |
| 51 | 43 | let last; |
modifiedapps/diffinery/test/e2e/moderation.test.mjs+1−9View file
| @@ -14,10 +14,10 @@ | ||
| 14 | 14 | import { tmpdir } from "node:os"; |
| 15 | 15 | import { join, dirname, resolve } from "node:path"; |
| 16 | 16 | import { fileURLToPath } from "node:url"; |
| 17 | -import { createServer } from "node:net"; | |
| 18 | 17 | import { test, before, after } from "node:test"; |
| 19 | 18 | import assert from "node:assert/strict"; |
| 20 | 19 | import { migratedState } from "./d1-state.mjs"; |
| 20 | +import { freePort } from "./ports.mjs"; | |
| 21 | 21 | |
| 22 | 22 | const APP = resolve(dirname(fileURLToPath(import.meta.url)), "../.."); |
| 23 | 23 | const work = mkdtempSync(join(tmpdir(), "diffinery-moderation-")); |
| @@ -30,14 +30,6 @@ | ||
| 30 | 30 | const T = { olive: tok("olive"), sully: tok("sully"), rita: tok("rita"), mal: tok("mal"), sid: tok("sid"), sam: tok("sam") }; |
| 31 | 31 | const S = { olive: `olive-${randomBytes(12).toString("hex")}`, sully: `sully-${randomBytes(12).toString("hex")}`, rita: `rita-${randomBytes(12).toString("hex")}`, sid: `sid-${randomBytes(12).toString("hex")}` }; |
| 32 | 32 | |
| 33 | -const freePort = () => | |
| 34 | - new Promise((res) => { | |
| 35 | - const s = createServer(); | |
| 36 | - s.listen(0, "127.0.0.1", () => { | |
| 37 | - const { port } = s.address(); | |
| 38 | - s.close(() => res(port)); | |
| 39 | - }); | |
| 40 | - }); | |
| 41 | 33 | const sleep = (ms) => new Promise((r) => setTimeout(r, ms)); |
| 42 | 34 | |
| 43 | 35 | /** Call the REST API as a token, a session cookie (`{ cookie }`) or anonymously (null). */ |
modifiedapps/diffinery/test/e2e/oauth.test.mjs+1−9View file
| @@ -11,7 +11,6 @@ | ||
| 11 | 11 | import { createHash, randomBytes } from "node:crypto"; |
| 12 | 12 | import { mkdtempSync, rmSync } from "node:fs"; |
| 13 | 13 | import { createServer as createHttpServer } from "node:http"; |
| 14 | -import { createServer } from "node:net"; | |
| 15 | 14 | import { tmpdir } from "node:os"; |
| 16 | 15 | import { join, dirname, resolve } from "node:path"; |
| 17 | 16 | import { fileURLToPath } from "node:url"; |
| @@ -19,6 +18,7 @@ | ||
| 19 | 18 | import assert from "node:assert/strict"; |
| 20 | 19 | import { chromium } from "playwright"; |
| 21 | 20 | import { migratedState } from "./d1-state.mjs"; |
| 21 | +import { freePort } from "./ports.mjs"; | |
| 22 | 22 | |
| 23 | 23 | const APP = resolve(dirname(fileURLToPath(import.meta.url)), "../.."); |
| 24 | 24 | const work = mkdtempSync(join(tmpdir(), "diffinery-oauth-")); |
| @@ -34,14 +34,6 @@ | ||
| 34 | 34 | const hash = (s) => createHash("sha256").update(s).digest("hex"); |
| 35 | 35 | const b64url = (b) => Buffer.from(b).toString("base64url"); |
| 36 | 36 | |
| 37 | -const freePort = () => | |
| 38 | - new Promise((res) => { | |
| 39 | - const s = createServer(); | |
| 40 | - s.listen(0, "127.0.0.1", () => { | |
| 41 | - const { port } = s.address(); | |
| 42 | - s.close(() => res(port)); | |
| 43 | - }); | |
| 44 | - }); | |
| 45 | 37 | const sleep = (ms) => new Promise((r) => setTimeout(r, ms)); |
| 46 | 38 | async function until(what, fn, timeoutMs = 60_000) { |
| 47 | 39 | const end = Date.now() + timeoutMs; |
modifiedapps/diffinery/test/e2e/passkeys.test.mjs+1−10View file
| @@ -17,11 +17,11 @@ | ||
| 17 | 17 | import { tmpdir } from "node:os"; |
| 18 | 18 | import { join, dirname, resolve } from "node:path"; |
| 19 | 19 | import { fileURLToPath } from "node:url"; |
| 20 | -import { createServer } from "node:net"; | |
| 21 | 20 | import { test, before, after } from "node:test"; |
| 22 | 21 | import assert from "node:assert/strict"; |
| 23 | 22 | import { chromium } from "playwright"; |
| 24 | 23 | import { migratedState } from "./d1-state.mjs"; |
| 24 | +import { freePort } from "./ports.mjs"; | |
| 25 | 25 | |
| 26 | 26 | const APP = resolve(dirname(fileURLToPath(import.meta.url)), "../.."); |
| 27 | 27 | const work = mkdtempSync(join(tmpdir(), "diffinery-passkeys-")); |
| @@ -42,15 +42,6 @@ | ||
| 42 | 42 | /** The proof only the invitation email carries (src/auth/invite-proof.ts). */ |
| 43 | 43 | const proofFor = (token) => createHmac("sha256", `diffinery-invite-proof-v1:${MASTER_KEY}`).update(hash(token)).digest("base64url"); |
| 44 | 44 | |
| 45 | -const freePort = () => | |
| 46 | - new Promise((res) => { | |
| 47 | - const s = createServer(); | |
| 48 | - s.listen(0, "127.0.0.1", () => { | |
| 49 | - const { port } = s.address(); | |
| 50 | - s.close(() => res(port)); | |
| 51 | - }); | |
| 52 | - }); | |
| 53 | - | |
| 54 | 45 | const sleep = (ms) => new Promise((r) => setTimeout(r, ms)); |
| 55 | 46 | async function until(what, fn, timeoutMs = 60_000) { |
| 56 | 47 | const end = Date.now() + timeoutMs; |
addedapps/diffinery/test/e2e/ports.mjs+125−0View file
| @@ -1,0 +1,125 @@ | ||
| 1 | +// Ports for the servers end-to-end tests start (`wrangler dev` and its inspector, the sandbox pool, ...). | |
| 2 | +// | |
| 3 | +// Asking the kernel for a free port (listen on 0, close, hand the number on) races: until the server | |
| 4 | +// binds it, the port is free again, and anything that binds port 0 meanwhile can be given the same | |
| 5 | +// number: workerd's own sockets in another `wrangler dev` or `wrangler d1 execute`, a docs server, an | |
| 6 | +// outgoing connection. Then wrangler dies with "bind(): Address already in use". So ports here come | |
| 7 | +// from below the kernel's ephemeral range, which port 0 binds and outgoing connections never use, and | |
| 8 | +// each is reserved with a lock file naming this process, so test files running side by side | |
| 9 | +// (--test-concurrency) never pick the same one. A process keeps its ports until it exits; a lock left | |
| 10 | +// by a process that's gone is taken over. | |
| 11 | +import { linkSync, mkdirSync, readFileSync, renameSync, unlinkSync, writeFileSync } from "node:fs"; | |
| 12 | +import { createServer } from "node:net"; | |
| 13 | +import { tmpdir } from "node:os"; | |
| 14 | +import { join } from "node:path"; | |
| 15 | + | |
| 16 | +const LOCKS = join(tmpdir(), "diffinery-e2e-ports"); | |
| 17 | +const [FIRST, LAST] = range(); | |
| 18 | +const held = new Set(); | |
| 19 | +let next = FIRST + Math.floor(Math.random() * (LAST - FIRST + 1)); | |
| 20 | + | |
| 21 | +process.on("exit", () => { | |
| 22 | + for (const port of held) { | |
| 23 | + try { | |
| 24 | + unlinkSync(join(LOCKS, String(port))); | |
| 25 | + } catch {} | |
| 26 | + } | |
| 27 | +}); | |
| 28 | + | |
| 29 | +/** A port on 127.0.0.1 that's free now and that no other caller, in this process or another, will be given. */ | |
| 30 | +export async function freePort() { | |
| 31 | + mkdirSync(LOCKS, { recursive: true }); | |
| 32 | + for (let tried = 0; tried <= LAST - FIRST; tried++) { | |
| 33 | + const port = next; | |
| 34 | + next = next === LAST ? FIRST : next + 1; | |
| 35 | + if (held.has(port) || !reserve(port)) continue; | |
| 36 | + if (await bindable(port)) { | |
| 37 | + held.add(port); | |
| 38 | + return port; | |
| 39 | + } | |
| 40 | + // Something outside the tests is listening there; leave the port to it. | |
| 41 | + unlinkSync(join(LOCKS, String(port))); | |
| 42 | + } | |
| 43 | + throw new Error(`no free port left in ${FIRST}-${LAST}`); | |
| 44 | +} | |
| 45 | + | |
| 46 | +/** Ports from 20000 up to the kernel's ephemeral range (32768 to 60999 on Linux by default; macOS's starts at 49152). */ | |
| 47 | +function range() { | |
| 48 | + let [low, high] = [32768, 60999]; | |
| 49 | + try { | |
| 50 | + [low, high] = readFileSync("/proc/sys/net/ipv4/ip_local_port_range", "utf8").trim().split(/\s+/).map(Number); | |
| 51 | + } catch {} | |
| 52 | + if (low >= 21000) return [20000, low - 1]; | |
| 53 | + // An ephemeral range starting this low leaves too few ports under it; use the ports above it. | |
| 54 | + if (high <= 64000) return [high + 1, 65535]; | |
| 55 | + throw new Error(`the ephemeral port range (${low}-${high}) leaves no room for test servers`); | |
| 56 | +} | |
| 57 | + | |
| 58 | +/** Takes the lock file for `port`. It's written whole under a name of its own, then linked into place, so it never exists half written. */ | |
| 59 | +function reserve(port) { | |
| 60 | + const file = join(LOCKS, String(port)); | |
| 61 | + const mine = `${file}.${process.pid}`; | |
| 62 | + writeFileSync(mine, String(process.pid)); | |
| 63 | + try { | |
| 64 | + for (let attempt = 0; attempt < 2; attempt++) { | |
| 65 | + try { | |
| 66 | + linkSync(mine, file); | |
| 67 | + return true; | |
| 68 | + } catch (e) { | |
| 69 | + if (e.code !== "EEXIST") throw e; | |
| 70 | + } | |
| 71 | + if (!takeOver(file)) return false; | |
| 72 | + } | |
| 73 | + return false; | |
| 74 | + } finally { | |
| 75 | + unlinkSync(mine); | |
| 76 | + } | |
| 77 | +} | |
| 78 | + | |
| 79 | +/** | |
| 80 | + * Moves a lock whose process is gone (killed before it could clean up) out of the way. It's renamed | |
| 81 | + * before it's read again, so of two processes taking over the same lock only one moves it, and a live | |
| 82 | + * lock moved by mistake goes back. | |
| 83 | + */ | |
| 84 | +function takeOver(file) { | |
| 85 | + if (alive(pidIn(file))) return false; | |
| 86 | + const moved = `${file}.stale-${process.pid}`; | |
| 87 | + try { | |
| 88 | + renameSync(file, moved); | |
| 89 | + } catch { | |
| 90 | + return false; | |
| 91 | + } | |
| 92 | + const dead = !alive(pidIn(moved)); | |
| 93 | + if (!dead) { | |
| 94 | + try { | |
| 95 | + linkSync(moved, file); | |
| 96 | + } catch {} | |
| 97 | + } | |
| 98 | + unlinkSync(moved); | |
| 99 | + return dead; | |
| 100 | +} | |
| 101 | + | |
| 102 | +function pidIn(file) { | |
| 103 | + try { | |
| 104 | + return Number(readFileSync(file, "utf8")); | |
| 105 | + } catch { | |
| 106 | + return 0; | |
| 107 | + } | |
| 108 | +} | |
| 109 | + | |
| 110 | +function alive(pid) { | |
| 111 | + if (!pid) return false; | |
| 112 | + try { | |
| 113 | + process.kill(pid, 0); | |
| 114 | + return true; | |
| 115 | + } catch (e) { | |
| 116 | + return e.code === "EPERM"; | |
| 117 | + } | |
| 118 | +} | |
| 119 | + | |
| 120 | +const bindable = (port) => | |
| 121 | + new Promise((res) => { | |
| 122 | + const s = createServer(); | |
| 123 | + s.once("error", () => res(false)); | |
| 124 | + s.listen(port, "127.0.0.1", () => s.close(() => res(true))); | |
| 125 | + }); |
modifiedapps/diffinery/test/e2e/registry.test.mjs+1−9View file
| @@ -14,10 +14,10 @@ | ||
| 14 | 14 | import { tmpdir } from "node:os"; |
| 15 | 15 | import { join, dirname, resolve } from "node:path"; |
| 16 | 16 | import { fileURLToPath } from "node:url"; |
| 17 | -import { createServer } from "node:net"; | |
| 18 | 17 | import { test, before, after } from "node:test"; |
| 19 | 18 | import assert from "node:assert/strict"; |
| 20 | 19 | import { migratedState } from "./d1-state.mjs"; |
| 20 | +import { freePort } from "./ports.mjs"; | |
| 21 | 21 | |
| 22 | 22 | const APP = resolve(dirname(fileURLToPath(import.meta.url)), "../.."); |
| 23 | 23 | const AGENT = resolve(APP, "../../containers/runner/dist/agent.mjs"); |
| @@ -29,14 +29,6 @@ | ||
| 29 | 29 | let token; |
| 30 | 30 | let owner; |
| 31 | 31 | |
| 32 | -const freePort = () => | |
| 33 | - new Promise((res) => { | |
| 34 | - const s = createServer(); | |
| 35 | - s.listen(0, "127.0.0.1", () => { | |
| 36 | - const { port } = s.address(); | |
| 37 | - s.close(() => res(port)); | |
| 38 | - }); | |
| 39 | - }); | |
| 40 | 32 | const sleep = (ms) => new Promise((r) => setTimeout(r, ms)); |
| 41 | 33 | async function until(what, fn, timeoutMs = 60_000) { |
| 42 | 34 | const end = Date.now() + timeoutMs; |
modifiedapps/diffinery/test/e2e/review.test.mjs+1−10View file
| @@ -15,11 +15,11 @@ | ||
| 15 | 15 | import { randomBytes } from "node:crypto"; |
| 16 | 16 | import { join, dirname, resolve } from "node:path"; |
| 17 | 17 | import { fileURLToPath } from "node:url"; |
| 18 | -import { createServer as netServer } from "node:net"; | |
| 19 | 18 | import { createServer as httpServer } from "node:http"; |
| 20 | 19 | import { test, before, after } from "node:test"; |
| 21 | 20 | import assert from "node:assert/strict"; |
| 22 | 21 | import { migratedState } from "./d1-state.mjs"; |
| 22 | +import { freePort } from "./ports.mjs"; | |
| 23 | 23 | |
| 24 | 24 | const APP = resolve(dirname(fileURLToPath(import.meta.url)), "../.."); |
| 25 | 25 | const AGENT = resolve(APP, "../../containers/runner/dist/agent.mjs"); |
| @@ -38,15 +38,6 @@ | ||
| 38 | 38 | let token; |
| 39 | 39 | const modelRequests = []; |
| 40 | 40 | |
| 41 | -const freePort = () => | |
| 42 | - new Promise((res) => { | |
| 43 | - const s = netServer(); | |
| 44 | - s.listen(0, "127.0.0.1", () => { | |
| 45 | - const { port } = s.address(); | |
| 46 | - s.close(() => res(port)); | |
| 47 | - }); | |
| 48 | - }); | |
| 49 | - | |
| 50 | 41 | async function api(method, path, body, tok = token) { |
| 51 | 42 | const res = await fetch(`${base}/api/v3${path}`, { |
| 52 | 43 | method, |
modifiedapps/diffinery/test/e2e/security-harness.mjs+1−10View file
| @@ -6,10 +6,10 @@ | ||
| 6 | 6 | import { tmpdir } from "node:os"; |
| 7 | 7 | import { join, dirname, resolve } from "node:path"; |
| 8 | 8 | import { fileURLToPath } from "node:url"; |
| 9 | -import { createServer } from "node:net"; | |
| 10 | 9 | import { before, after } from "node:test"; |
| 11 | 10 | import assert from "node:assert/strict"; |
| 12 | 11 | import { migratedState } from "./d1-state.mjs"; |
| 12 | +import { freePort } from "./ports.mjs"; | |
| 13 | 13 | |
| 14 | 14 | export const APP = resolve(dirname(fileURLToPath(import.meta.url)), "../.."); |
| 15 | 15 | export const work = mkdtempSync(join(tmpdir(), "diffinery-security-")); |
| @@ -25,15 +25,6 @@ | ||
| 25 | 25 | /** One minute of an environment's wait timer, in this Worker (WAIT_TIMER_MINUTE_MS). */ |
| 26 | 26 | export const TIMER_MINUTE_MS = 10_000; |
| 27 | 27 | |
| 28 | -export const freePort = () => | |
| 29 | - new Promise((res) => { | |
| 30 | - const s = createServer(); | |
| 31 | - s.listen(0, "127.0.0.1", () => { | |
| 32 | - const { port } = s.address(); | |
| 33 | - s.close(() => res(port)); | |
| 34 | - }); | |
| 35 | - }); | |
| 36 | - | |
| 37 | 28 | export const sleep = (ms) => new Promise((r) => setTimeout(r, ms)); |
| 38 | 29 | export async function until(what, fn, timeoutMs = 60_000) { |
| 39 | 30 | const end = Date.now() + timeoutMs; |
modifiedapps/diffinery/test/e2e/security-txt.test.mjs+1−10View file
| @@ -11,24 +11,15 @@ | ||
| 11 | 11 | import { tmpdir } from "node:os"; |
| 12 | 12 | import { join, dirname, resolve } from "node:path"; |
| 13 | 13 | import { fileURLToPath } from "node:url"; |
| 14 | -import { createServer } from "node:net"; | |
| 15 | 14 | import { test, after } from "node:test"; |
| 16 | 15 | import assert from "node:assert/strict"; |
| 17 | 16 | import { migratedState } from "./d1-state.mjs"; |
| 17 | +import { freePort } from "./ports.mjs"; | |
| 18 | 18 | |
| 19 | 19 | const APP = resolve(dirname(fileURLToPath(import.meta.url)), "../.."); |
| 20 | 20 | const work = mkdtempSync(join(tmpdir(), "diffinery-securitytxt-e2e-")); |
| 21 | 21 | const running = []; |
| 22 | 22 | |
| 23 | -const freePort = () => | |
| 24 | - new Promise((res) => { | |
| 25 | - const s = createServer(); | |
| 26 | - s.listen(0, "127.0.0.1", () => { | |
| 27 | - const { port } = s.address(); | |
| 28 | - s.close(() => res(port)); | |
| 29 | - }); | |
| 30 | - }); | |
| 31 | - | |
| 32 | 23 | /** Start a Worker with extra `--var`s and seed SQL; resolves to its base URL once /healthz answers. */ |
| 33 | 24 | async function worker(name, vars, seed = []) { |
| 34 | 25 | const port = await freePort(); |
modifiedapps/diffinery/test/e2e/status.test.mjs+1−10View file
| @@ -10,24 +10,15 @@ | ||
| 10 | 10 | import { tmpdir } from "node:os"; |
| 11 | 11 | import { join, dirname, resolve } from "node:path"; |
| 12 | 12 | import { fileURLToPath } from "node:url"; |
| 13 | -import { createServer } from "node:net"; | |
| 14 | 13 | import { test, after } from "node:test"; |
| 15 | 14 | import assert from "node:assert/strict"; |
| 16 | 15 | import { migratedState } from "./d1-state.mjs"; |
| 16 | +import { freePort } from "./ports.mjs"; | |
| 17 | 17 | |
| 18 | 18 | const APP = resolve(dirname(fileURLToPath(import.meta.url)), "../.."); |
| 19 | 19 | const work = mkdtempSync(join(tmpdir(), "diffinery-status-e2e-")); |
| 20 | 20 | const running = []; |
| 21 | 21 | |
| 22 | -const freePort = () => | |
| 23 | - new Promise((res) => { | |
| 24 | - const s = createServer(); | |
| 25 | - s.listen(0, "127.0.0.1", () => { | |
| 26 | - const { port } = s.address(); | |
| 27 | - s.close(() => res(port)); | |
| 28 | - }); | |
| 29 | - }); | |
| 30 | - | |
| 31 | 22 | /** Start a Worker with extra `--var`s and seed SQL; resolves to its base URL once /healthz answers. */ |
| 32 | 23 | async function worker(name, vars, seed = []) { |
| 33 | 24 | const port = await freePort(); |
modifiedapps/diffinery/test/e2e/tasks.test.mjs+1−1View file
| @@ -39,7 +39,6 @@ | ||
| 39 | 39 | token, |
| 40 | 40 | modelRequests, |
| 41 | 41 | jailToken, |
| 42 | - freePort, | |
| 43 | 42 | api, |
| 44 | 43 | ok, |
| 45 | 44 | asPerson, |
| @@ -49,6 +48,7 @@ | ||
| 49 | 48 | mcp, |
| 50 | 49 | poolSandboxes, |
| 51 | 50 | } from "./agents-harness.mjs"; |
| 51 | +import { freePort } from "./ports.mjs"; | |
| 52 | 52 | |
| 53 | 53 | test("model keys: write-only, browser session only, with an opt-in", async () => { |
| 54 | 54 | assert.deepEqual(await ok("GET", "/diffinery/model-keys"), []); |
modifiedapps/diffinery/test/e2e/terminals.test.mjs+1−1View file
| @@ -31,7 +31,6 @@ | ||
| 31 | 31 | poolLog, |
| 32 | 32 | token, |
| 33 | 33 | modelRequests, |
| 34 | - freePort, | |
| 35 | 34 | api, |
| 36 | 35 | ok, |
| 37 | 36 | asPerson, |
| @@ -41,6 +40,7 @@ | ||
| 41 | 40 | mcp, |
| 42 | 41 | poolSandboxes, |
| 43 | 42 | } from "./agents-harness.mjs"; |
| 43 | +import { freePort } from "./ports.mjs"; | |
| 44 | 44 | |
| 45 | 45 | /** Opens a terminal's WebSocket; resolves with the HTTP status when the upgrade is refused. */ |
| 46 | 46 | function termSocket(url, headers) { |