From a31ec5a2623edd6339f82655b77c4d6f63ca2648 Mon Sep 17 00:00:00 2001 From: Adamulek123 Date: Wed, 16 Sep 2026 07:29:20 +0200 Subject: [PATCH 01/16] fix(server): bound foreign-body parse in pair probe --- apps/server/src/cli/pair.test.ts | 45 ++++++++++++++++++++++++++++++++ apps/server/src/cli/pair.ts | 24 ++++++++++++++--- 2 files changed, 66 insertions(+), 3 deletions(-) diff --git a/apps/server/src/cli/pair.test.ts b/apps/server/src/cli/pair.test.ts index dd15c41fdd91..ae49bda03020 100644 --- a/apps/server/src/cli/pair.test.ts +++ b/apps/server/src/cli/pair.test.ts @@ -288,4 +288,49 @@ describe("t3 pair", () => { assert.include(rendered, "No running T3 Code server found."); }).pipe(Effect.provide(NodeServices.layer)), ); + + it.effect("does not decode an oversized stranger body as a server descriptor", () => + Effect.acquireUseRelease( + Effect.callback((resume) => { + const server = NodeHttp.createServer((request, response) => { + if (request.url === "/.well-known/t3/environment") { + response.writeHead(200, { "content-type": "application/json" }); + // Valid JSON shape, far beyond any real descriptor: discovery must + // classify the occupant without a Schema decode, not pair with it. + response.end(JSON.stringify({ padding: "x".repeat(128 * 1024) })); + return; + } + response.writeHead(404); + response.end(); + }); + server.listen(0, "127.0.0.1", () => resume(Effect.succeed(server))); + }), + (server) => + Effect.gen(function* () { + const address = server.address(); + if (address === null || typeof address === "string") { + return Effect.die(new Error("Expected a TCP address")); + } + const baseDir = NodeFS.mkdtempSync(NodePath.join(NodeOS.tmpdir(), "t3-pair-big-test-")); + const statePath = NodePath.join(baseDir, "userdata", "server-runtime.json"); + yield* persistServerRuntimeState({ + path: statePath, + state: yield* makePersistedServerRuntimeState({ + config: { host: "127.0.0.1", devUrl: undefined }, + port: address.port, + }), + }); + + const error = yield* provideCliTestLayers( + runCli(["pair", "--base-dir", baseDir]).pipe(Effect.flip), + ); + + const rendered = String( + typeof error === "object" && error !== null && "cause" in error ? error.cause : error, + ); + assert.include(rendered, "No running T3 Code server found."); + }), + (server) => Effect.sync(() => server.close()), + ).pipe(Effect.provide(NodeServices.layer)), + ); }); diff --git a/apps/server/src/cli/pair.ts b/apps/server/src/cli/pair.ts index 493e6b719416..d67857d17a86 100644 --- a/apps/server/src/cli/pair.ts +++ b/apps/server/src/cli/pair.ts @@ -59,6 +59,9 @@ import { baseDirFlag, DurationFromString } from "./config.ts"; const WELL_KNOWN_ENVIRONMENT_PATH = "/.well-known/t3/environment"; const PAIR_PROBE_TIMEOUT = Duration.millis(2_500); +// The environment descriptor is a few hundred bytes; anything larger served +// from the well-known path cannot be a T3 server. +const MAX_PROBE_BODY_BYTES = 64 * 1024; // Tailscale provisions an HTTPS certificate on the first request to a fresh // serve mapping, which can take a few seconds. const TAILSCALE_PROBE_ATTEMPTS = 5; @@ -218,14 +221,29 @@ const probeEnvironmentDescriptor = ( // Bad-gateway family means a proxy (Tailscale Serve) answered for a // backend that is gone — a stale mapping, not a live occupant. Treating // it as unreachable lets `t3 pair --tailscale` repair its own mapping - // after the server's port changed. + // after the server's port changed. Drain the body so the pooled + // connection is reusable for the re-configured mapping. if (response.status === 502 || response.status === 503 || response.status === 504) { + yield* Effect.ignore(response.text); return { _tag: "unreachable" } as const; } // Anything else that answered HTTP but not with a valid descriptor is - // some other service. + // some other service. Refuse to buffer + decode a stranger's body blind: + // the descriptor is a few hundred bytes of JSON, so non-JSON content or + // anything over the cap is classified without a Schema decode. + const contentType = response.headers["content-type"] ?? ""; + if (!contentType.includes("json")) { + return { _tag: "not-a-t3-server" } as const; + } const descriptor = yield* HttpClientResponse.filterStatusOk(response).pipe( - Effect.flatMap(HttpClientResponse.schemaBodyJson(ExecutionEnvironmentDescriptor)), + Effect.flatMap((ok) => ok.text), + Effect.flatMap((body) => + body.length > MAX_PROBE_BODY_BYTES + ? Effect.fail({ _tag: "not-a-t3-server" } as const) + : Schema.decodeUnknownEffect(Schema.fromJsonString(ExecutionEnvironmentDescriptor))( + body, + ).pipe(Effect.mapError(() => ({ _tag: "not-a-t3-server" }) as const)), + ), Effect.mapError(() => ({ _tag: "not-a-t3-server" }) as const), ); return { _tag: "descriptor", descriptor } as const; From 1af5d21105da67440e38faa10aa270753f1f54e2 Mon Sep 17 00:00:00 2001 From: Adamulek123 Date: Wed, 16 Sep 2026 07:40:50 +0200 Subject: [PATCH 02/16] test(server): pin the pair probe body cap with a schema-valid oversized body --- apps/server/src/cli/pair.test.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/apps/server/src/cli/pair.test.ts b/apps/server/src/cli/pair.test.ts index ae49bda03020..446d95dc36ca 100644 --- a/apps/server/src/cli/pair.test.ts +++ b/apps/server/src/cli/pair.test.ts @@ -295,9 +295,10 @@ describe("t3 pair", () => { const server = NodeHttp.createServer((request, response) => { if (request.url === "/.well-known/t3/environment") { response.writeHead(200, { "content-type": "application/json" }); - // Valid JSON shape, far beyond any real descriptor: discovery must - // classify the occupant without a Schema decode, not pair with it. - response.end(JSON.stringify({ padding: "x".repeat(128 * 1024) })); + // A schema-valid descriptor padded far beyond any real one: + // discovery must reject it on size without a Schema decode + // (without the cap this decodes fine and pairs), not pair with it. + response.end(JSON.stringify({ ...testDescriptor, padding: "x".repeat(128 * 1024) })); return; } response.writeHead(404); From 116ae825d03f669882d89de8e4f075fd474284ef Mon Sep 17 00:00:00 2001 From: Adamulek123 Date: Fri, 18 Sep 2026 23:47:59 +0200 Subject: [PATCH 03/16] fix(server): bound pair probe body reads in bytes with read timeout Stream the descriptor and 502/503/504 drain through a byte-counted read capped at MAX_PROBE_BODY_BYTES with PAIR_PROBE_TIMEOUT, before decoding to text. Normalize the JSON content-type check. Covers uppercase content types, slow-drip bodies, and multibyte UTF-8 oversize. --- apps/server/src/cli/pair.test.ts | 145 +++++++++++++++++++++++++++++++ apps/server/src/cli/pair.ts | 52 ++++++++--- 2 files changed, 184 insertions(+), 13 deletions(-) diff --git a/apps/server/src/cli/pair.test.ts b/apps/server/src/cli/pair.test.ts index 446d95dc36ca..44f7ba3c6e47 100644 --- a/apps/server/src/cli/pair.test.ts +++ b/apps/server/src/cli/pair.test.ts @@ -334,4 +334,149 @@ describe("t3 pair", () => { (server) => Effect.sync(() => server.close()), ).pipe(Effect.provide(NodeServices.layer)), ); + + it.effect("rejects a descriptor that exceeds the cap in UTF-8 bytes only", () => + Effect.acquireUseRelease( + Effect.callback((resume) => { + const server = NodeHttp.createServer((request, response) => { + if (request.url === "/.well-known/t3/environment") { + response.writeHead(200, { "content-type": "application/json" }); + // "é" is two bytes in UTF-8 but one UTF-16 code unit: this body + // is ~80 KiB on the wire yet under 64 KiB in string length, so a + // string-length check would accept and pair with it. The probe + // must enforce the cap in bytes, before decoding. + response.end(JSON.stringify({ ...testDescriptor, label: "é".repeat(40 * 1024) })); + return; + } + response.writeHead(404); + response.end(); + }); + server.listen(0, "127.0.0.1", () => resume(Effect.succeed(server))); + }), + (server) => + Effect.gen(function* () { + const address = server.address(); + if (address === null || typeof address === "string") { + return Effect.die(new Error("Expected a TCP address")); + } + const baseDir = NodeFS.mkdtempSync(NodePath.join(NodeOS.tmpdir(), "t3-pair-utf8-test-")); + const statePath = NodePath.join(baseDir, "userdata", "server-runtime.json"); + yield* persistServerRuntimeState({ + path: statePath, + state: yield* makePersistedServerRuntimeState({ + config: { host: "127.0.0.1", devUrl: undefined }, + port: address.port, + }), + }); + + const error = yield* provideCliTestLayers( + runCli(["pair", "--base-dir", baseDir]).pipe(Effect.flip), + ); + + const rendered = String( + typeof error === "object" && error !== null && "cause" in error ? error.cause : error, + ); + assert.include(rendered, "No running T3 Code server found."); + }), + (server) => Effect.sync(() => server.close()), + ).pipe(Effect.provide(NodeServices.layer)), + ); + + it.effect("pairs when the descriptor arrives with an uppercase JSON content type", () => + Effect.acquireUseRelease( + Effect.callback((resume) => { + const server = NodeHttp.createServer((request, response) => { + if (request.url === "/.well-known/t3/environment") { + response.writeHead(200, { "content-type": "Application/JSON; charset=utf-8" }); + response.end(JSON.stringify(testDescriptor)); + return; + } + response.writeHead(404); + response.end(); + }); + server.listen(0, "127.0.0.1", () => resume(Effect.succeed(server))); + }), + (server) => + Effect.gen(function* () { + const address = server.address(); + if (address === null || typeof address === "string") { + return Effect.die(new Error("Expected a TCP address")); + } + const baseDir = NodeFS.mkdtempSync(NodePath.join(NodeOS.tmpdir(), "t3-pair-ctype-test-")); + const statePath = NodePath.join(baseDir, "userdata", "server-runtime.json"); + yield* persistServerRuntimeState({ + path: statePath, + state: yield* makePersistedServerRuntimeState({ + config: { host: "127.0.0.1", devUrl: undefined }, + port: address.port, + }), + }); + + const output = yield* captureStdout(runCli(["pair", "--base-dir", baseDir])); + + assert.include(output, "Pairing with pair-test ("); + assert.include(output, "/pair#token="); + }), + (server) => Effect.sync(() => server.close()), + ).pipe(Effect.provide(NodeServices.layer)), + ); + + it.live("times out a slow-drip stranger body instead of hanging discovery", () => + Effect.acquireUseRelease( + Effect.callback((resume) => { + const server = NodeHttp.createServer((request, response) => { + if (request.url === "/.well-known/t3/environment") { + response.writeHead(200, { "content-type": "application/json" }); + // A never-ending drip that stays under the size cap: discovery + // must give up via the body timeout rather than hang on the open + // stream. + response.write(`{"environmentId":`); + const timer = setInterval(() => { + if (response.destroyed) { + clearInterval(timer); + return; + } + response.write(" "); + }, 200); + timer.unref(); + response.on("close", () => clearInterval(timer)); + return; + } + response.writeHead(404); + response.end(); + }); + server.listen(0, "127.0.0.1", () => resume(Effect.succeed(server))); + }), + (server) => + Effect.gen(function* () { + const address = server.address(); + if (address === null || typeof address === "string") { + return Effect.die(new Error("Expected a TCP address")); + } + const baseDir = NodeFS.mkdtempSync(NodePath.join(NodeOS.tmpdir(), "t3-pair-drip-test-")); + const statePath = NodePath.join(baseDir, "userdata", "server-runtime.json"); + yield* persistServerRuntimeState({ + path: statePath, + state: yield* makePersistedServerRuntimeState({ + config: { host: "127.0.0.1", devUrl: undefined }, + port: address.port, + }), + }); + + const error = yield* provideCliTestLayers( + runCli(["pair", "--base-dir", baseDir]).pipe(Effect.flip), + ); + + const rendered = String( + typeof error === "object" && error !== null && "cause" in error ? error.cause : error, + ); + assert.include(rendered, "No running T3 Code server found."); + }), + (server) => + Effect.sync(() => { + server.closeAllConnections(); + server.close(); + }), + ).pipe(Effect.provide(NodeServices.layer)), + ); }); diff --git a/apps/server/src/cli/pair.ts b/apps/server/src/cli/pair.ts index d67857d17a86..eacfb9c004ee 100644 --- a/apps/server/src/cli/pair.ts +++ b/apps/server/src/cli/pair.ts @@ -31,6 +31,7 @@ import * as Layer from "effect/Layer"; import * as Option from "effect/Option"; import * as References from "effect/References"; import * as Schema from "effect/Schema"; +import * as Stream from "effect/Stream"; import { Command, Flag, GlobalFlag } from "effect/unstable/cli"; import { FetchHttpClient, @@ -207,7 +208,32 @@ type EnvironmentProbeResult = | { readonly _tag: "unreachable" } | { readonly _tag: "not-a-t3-server" }; -const probeEnvironmentDescriptor = ( +const readBoundedProbeBody = (ok: HttpClientResponse.HttpClientResponse) => + ok.stream.pipe( + Stream.runFoldEffect( + () => ({ bytes: 0, chunks: [] as Array }), + (acc, chunk) => { + if (acc.bytes + chunk.byteLength > MAX_PROBE_BODY_BYTES) { + return Effect.fail({ _tag: "not-a-t3-server" } as const); + } + acc.bytes += chunk.byteLength; + acc.chunks.push(chunk); + return Effect.succeed(acc); + }, + ), + Effect.map(({ bytes, chunks }) => { + const merged = new Uint8Array(bytes); + let offset = 0; + for (const chunk of chunks) { + merged.set(chunk, offset); + offset += chunk.byteLength; + } + return new TextDecoder().decode(merged); + }), + Effect.timeout(PAIR_PROBE_TIMEOUT), + ); + +export const probeEnvironmentDescriptor = ( baseUrl: string, ): Effect.Effect => Effect.gen(function* () { @@ -222,27 +248,27 @@ const probeEnvironmentDescriptor = ( // backend that is gone — a stale mapping, not a live occupant. Treating // it as unreachable lets `t3 pair --tailscale` repair its own mapping // after the server's port changed. Drain the body so the pooled - // connection is reusable for the re-configured mapping. + // connection is reusable for the re-configured mapping; the drain itself + // is time-bounded and best-effort. if (response.status === 502 || response.status === 503 || response.status === 504) { - yield* Effect.ignore(response.text); + yield* Effect.ignore(readBoundedProbeBody(response)); return { _tag: "unreachable" } as const; } // Anything else that answered HTTP but not with a valid descriptor is - // some other service. Refuse to buffer + decode a stranger's body blind: - // the descriptor is a few hundred bytes of JSON, so non-JSON content or - // anything over the cap is classified without a Schema decode. + // some other service. Refuse to decode a stranger's body blind: the + // descriptor is a few hundred bytes of JSON, so non-JSON content is + // classified without reading the body at all, and JSON bodies are read + // through the bounded stream above — never buffered-then-checked. const contentType = response.headers["content-type"] ?? ""; - if (!contentType.includes("json")) { + if (!contentType.toLowerCase().includes("json")) { return { _tag: "not-a-t3-server" } as const; } const descriptor = yield* HttpClientResponse.filterStatusOk(response).pipe( - Effect.flatMap((ok) => ok.text), + Effect.flatMap(readBoundedProbeBody), Effect.flatMap((body) => - body.length > MAX_PROBE_BODY_BYTES - ? Effect.fail({ _tag: "not-a-t3-server" } as const) - : Schema.decodeUnknownEffect(Schema.fromJsonString(ExecutionEnvironmentDescriptor))( - body, - ).pipe(Effect.mapError(() => ({ _tag: "not-a-t3-server" }) as const)), + Schema.decodeUnknownEffect(Schema.fromJsonString(ExecutionEnvironmentDescriptor))( + body, + ).pipe(Effect.mapError(() => ({ _tag: "not-a-t3-server" }) as const)), ), Effect.mapError(() => ({ _tag: "not-a-t3-server" }) as const), ); From 14e56e15f10a7b3ab1e755dac9c7ecac188d329f Mon Sep 17 00:00:00 2001 From: Adamulek123 Date: Sat, 19 Sep 2026 00:02:31 +0200 Subject: [PATCH 04/16] fix(server): keep pair probe helper private for knip --- apps/server/src/cli/pair.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/server/src/cli/pair.ts b/apps/server/src/cli/pair.ts index eacfb9c004ee..c98fb6a0d1ad 100644 --- a/apps/server/src/cli/pair.ts +++ b/apps/server/src/cli/pair.ts @@ -233,7 +233,7 @@ const readBoundedProbeBody = (ok: HttpClientResponse.HttpClientResponse) => Effect.timeout(PAIR_PROBE_TIMEOUT), ); -export const probeEnvironmentDescriptor = ( +const probeEnvironmentDescriptor = ( baseUrl: string, ): Effect.Effect => Effect.gen(function* () { From 0c77bd0034ba0ea4dbeb49dd596d018e190771b2 Mon Sep 17 00:00:00 2001 From: Adamulek123 Date: Sat, 19 Sep 2026 00:14:36 +0200 Subject: [PATCH 05/16] fix(server): use Effect timer for pair slow-drip test and strict JSON media-type check --- apps/server/src/cli/pair.test.ts | 22 +++++++++++++--------- apps/server/src/cli/pair.ts | 3 ++- 2 files changed, 15 insertions(+), 10 deletions(-) diff --git a/apps/server/src/cli/pair.test.ts b/apps/server/src/cli/pair.test.ts index 44f7ba3c6e47..59f650f74d14 100644 --- a/apps/server/src/cli/pair.test.ts +++ b/apps/server/src/cli/pair.test.ts @@ -9,7 +9,9 @@ import * as NetService from "@t3tools/shared/Net"; import { HostProcessEnvironment } from "@t3tools/shared/hostProcess"; import { assert, describe, expect, it } from "@effect/vitest"; import * as Effect from "effect/Effect"; +import * as Fiber from "effect/Fiber"; import * as Layer from "effect/Layer"; +import * as Schedule from "effect/Schedule"; import * as TestConsole from "effect/testing/TestConsole"; import { Command } from "effect/unstable/cli"; @@ -431,15 +433,17 @@ describe("t3 pair", () => { // must give up via the body timeout rather than hang on the open // stream. response.write(`{"environmentId":`); - const timer = setInterval(() => { - if (response.destroyed) { - clearInterval(timer); - return; - } - response.write(" "); - }, 200); - timer.unref(); - response.on("close", () => clearInterval(timer)); + const drip = Effect.runFork( + Effect.repeat( + Effect.sync(() => { + if (!response.destroyed) { + response.write(" "); + } + }), + Schedule.spaced("200 millis"), + ), + ); + response.on("close", () => Effect.runFork(Fiber.interrupt(drip))); return; } response.writeHead(404); diff --git a/apps/server/src/cli/pair.ts b/apps/server/src/cli/pair.ts index c98fb6a0d1ad..e85726af71d3 100644 --- a/apps/server/src/cli/pair.ts +++ b/apps/server/src/cli/pair.ts @@ -260,7 +260,8 @@ const probeEnvironmentDescriptor = ( // classified without reading the body at all, and JSON bodies are read // through the bounded stream above — never buffered-then-checked. const contentType = response.headers["content-type"] ?? ""; - if (!contentType.toLowerCase().includes("json")) { + const mediaType = contentType.split(";", 1)[0]?.trim().toLowerCase(); + if (mediaType !== "application/json" && !mediaType?.endsWith("+json")) { return { _tag: "not-a-t3-server" } as const; } const descriptor = yield* HttpClientResponse.filterStatusOk(response).pipe( From 12a357a0f63159cfe77de32ecd76b6522cf81b43 Mon Sep 17 00:00:00 2001 From: Adamulek123 Date: Sat, 19 Sep 2026 00:42:11 +0200 Subject: [PATCH 06/16] fix(server): fork pair drip test in-effect and hoist probe schema compile --- apps/server/src/cli/pair.test.ts | 91 ++++++++++++++++++-------------- apps/server/src/cli/pair.ts | 11 ++-- 2 files changed, 60 insertions(+), 42 deletions(-) diff --git a/apps/server/src/cli/pair.test.ts b/apps/server/src/cli/pair.test.ts index 59f650f74d14..b621c8e9c298 100644 --- a/apps/server/src/cli/pair.test.ts +++ b/apps/server/src/cli/pair.test.ts @@ -9,6 +9,7 @@ import * as NetService from "@t3tools/shared/Net"; import { HostProcessEnvironment } from "@t3tools/shared/hostProcess"; import { assert, describe, expect, it } from "@effect/vitest"; import * as Effect from "effect/Effect"; +import * as Deferred from "effect/Deferred"; import * as Fiber from "effect/Fiber"; import * as Layer from "effect/Layer"; import * as Schedule from "effect/Schedule"; @@ -423,8 +424,13 @@ describe("t3 pair", () => { ).pipe(Effect.provide(NodeServices.layer)), ); - it.live("times out a slow-drip stranger body instead of hanging discovery", () => - Effect.acquireUseRelease( + it.live("times out a slow-drip stranger body instead of hanging discovery", () => { + // Handoff from the raw Node request handler below: it holds the response + // open so the test fiber can drip into it. Completed synchronously from + // the callback via `doneUnsafe`, so no manual Effect runtime is created + // in the test (see `t3code(no-manual-effect-runtime-in-tests)`). + const dripTarget = Deferred.makeUnsafe(); + return Effect.acquireUseRelease( Effect.callback((resume) => { const server = NodeHttp.createServer((request, response) => { if (request.url === "/.well-known/t3/environment") { @@ -433,17 +439,7 @@ describe("t3 pair", () => { // must give up via the body timeout rather than hang on the open // stream. response.write(`{"environmentId":`); - const drip = Effect.runFork( - Effect.repeat( - Effect.sync(() => { - if (!response.destroyed) { - response.write(" "); - } - }), - Schedule.spaced("200 millis"), - ), - ); - response.on("close", () => Effect.runFork(Fiber.interrupt(drip))); + Deferred.doneUnsafe(dripTarget, Effect.succeed(response)); return; } response.writeHead(404); @@ -452,35 +448,52 @@ describe("t3 pair", () => { server.listen(0, "127.0.0.1", () => resume(Effect.succeed(server))); }), (server) => - Effect.gen(function* () { - const address = server.address(); - if (address === null || typeof address === "string") { - return Effect.die(new Error("Expected a TCP address")); - } - const baseDir = NodeFS.mkdtempSync(NodePath.join(NodeOS.tmpdir(), "t3-pair-drip-test-")); - const statePath = NodePath.join(baseDir, "userdata", "server-runtime.json"); - yield* persistServerRuntimeState({ - path: statePath, - state: yield* makePersistedServerRuntimeState({ - config: { host: "127.0.0.1", devUrl: undefined }, - port: address.port, - }), - }); - - const error = yield* provideCliTestLayers( - runCli(["pair", "--base-dir", baseDir]).pipe(Effect.flip), - ); - - const rendered = String( - typeof error === "object" && error !== null && "cause" in error ? error.cause : error, - ); - assert.include(rendered, "No running T3 Code server found."); - }), + Effect.scoped( + Effect.gen(function* () { + const address = server.address(); + if (address === null || typeof address === "string") { + return Effect.die(new Error("Expected a TCP address")); + } + const baseDir = NodeFS.mkdtempSync( + NodePath.join(NodeOS.tmpdir(), "t3-pair-drip-test-"), + ); + const statePath = NodePath.join(baseDir, "userdata", "server-runtime.json"); + yield* persistServerRuntimeState({ + path: statePath, + state: yield* makePersistedServerRuntimeState({ + config: { host: "127.0.0.1", devUrl: undefined }, + port: address.port, + }), + }); + + const cliFiber = yield* provideCliTestLayers( + runCli(["pair", "--base-dir", baseDir]).pipe(Effect.flip), + ).pipe(Effect.forkChild); + // The probe request holds the response open; the drip loop is a + // scoped fork, so it is interrupted when the test settles. + const dripResponse = yield* Deferred.await(dripTarget); + yield* Effect.repeat( + Effect.sync(() => { + if (!dripResponse.destroyed) { + dripResponse.write(" "); + } + }), + Schedule.spaced("200 millis"), + ).pipe(Effect.forkScoped); + + const error = yield* Fiber.join(cliFiber); + + const rendered = String( + typeof error === "object" && error !== null && "cause" in error ? error.cause : error, + ); + assert.include(rendered, "No running T3 Code server found."); + }), + ), (server) => Effect.sync(() => { server.closeAllConnections(); server.close(); }), - ).pipe(Effect.provide(NodeServices.layer)), - ); + ).pipe(Effect.provide(NodeServices.layer)); + }); }); diff --git a/apps/server/src/cli/pair.ts b/apps/server/src/cli/pair.ts index e85726af71d3..fd18a99739b0 100644 --- a/apps/server/src/cli/pair.ts +++ b/apps/server/src/cli/pair.ts @@ -151,6 +151,11 @@ export class DevServerNotProxiableError extends Schema.TaggedError - Schema.decodeUnknownEffect(Schema.fromJsonString(ExecutionEnvironmentDescriptor))( - body, - ).pipe(Effect.mapError(() => ({ _tag: "not-a-t3-server" }) as const)), + decodeProbeDescriptor(body).pipe( + Effect.mapError(() => ({ _tag: "not-a-t3-server" }) as const), + ), ), Effect.mapError(() => ({ _tag: "not-a-t3-server" }) as const), ); From 978b1337c41579518eb096854e6eacd9763fc365 Mon Sep 17 00:00:00 2001 From: Adamulek123 Date: Sat, 19 Sep 2026 10:39:15 +0200 Subject: [PATCH 07/16] fix(server): drain non-JSON probe bodies for connection reuse --- apps/server/src/cli/pair.test.ts | 46 ++++++++++++++++++++++++++++++++ apps/server/src/cli/pair.ts | 6 +++-- 2 files changed, 50 insertions(+), 2 deletions(-) diff --git a/apps/server/src/cli/pair.test.ts b/apps/server/src/cli/pair.test.ts index b621c8e9c298..8704a3de8eb3 100644 --- a/apps/server/src/cli/pair.test.ts +++ b/apps/server/src/cli/pair.test.ts @@ -385,6 +385,52 @@ describe("t3 pair", () => { ).pipe(Effect.provide(NodeServices.layer)), ); + it.effect("does not pair when a valid descriptor arrives as non-JSON", () => + Effect.acquireUseRelease( + Effect.callback((resume) => { + const server = NodeHttp.createServer((request, response) => { + if (request.url === "/.well-known/t3/environment") { + // Schema-valid JSON served as HTML: the probe must classify it + // as a stranger (and drain the body for connection reuse) + // instead of decoding and pairing with it. + response.writeHead(200, { "content-type": "text/html; charset=utf-8" }); + response.end(JSON.stringify(testDescriptor)); + return; + } + response.writeHead(404); + response.end(); + }); + server.listen(0, "127.0.0.1", () => resume(Effect.succeed(server))); + }), + (server) => + Effect.gen(function* () { + const address = server.address(); + if (address === null || typeof address === "string") { + return Effect.die(new Error("Expected a TCP address")); + } + const baseDir = NodeFS.mkdtempSync(NodePath.join(NodeOS.tmpdir(), "t3-pair-ctype-test-")); + const statePath = NodePath.join(baseDir, "userdata", "server-runtime.json"); + yield* persistServerRuntimeState({ + path: statePath, + state: yield* makePersistedServerRuntimeState({ + config: { host: "127.0.0.1", devUrl: undefined }, + port: address.port, + }), + }); + + const error = yield* provideCliTestLayers( + runCli(["pair", "--base-dir", baseDir]).pipe(Effect.flip), + ); + + const rendered = String( + typeof error === "object" && error !== null && "cause" in error ? error.cause : error, + ); + assert.include(rendered, "No running T3 Code server found."); + }), + (server) => Effect.sync(() => server.close()), + ).pipe(Effect.provide(NodeServices.layer)), + ); + it.effect("pairs when the descriptor arrives with an uppercase JSON content type", () => Effect.acquireUseRelease( Effect.callback((resume) => { diff --git a/apps/server/src/cli/pair.ts b/apps/server/src/cli/pair.ts index fd18a99739b0..d75556c12853 100644 --- a/apps/server/src/cli/pair.ts +++ b/apps/server/src/cli/pair.ts @@ -262,11 +262,13 @@ const probeEnvironmentDescriptor = ( // Anything else that answered HTTP but not with a valid descriptor is // some other service. Refuse to decode a stranger's body blind: the // descriptor is a few hundred bytes of JSON, so non-JSON content is - // classified without reading the body at all, and JSON bodies are read - // through the bounded stream above — never buffered-then-checked. + // classified without decoding, but the body is still drained boundedly + // so the pooled connection is reusable — a stranger that holds its body + // open must not pin pool slots across repeated probes. const contentType = response.headers["content-type"] ?? ""; const mediaType = contentType.split(";", 1)[0]?.trim().toLowerCase(); if (mediaType !== "application/json" && !mediaType?.endsWith("+json")) { + yield* Effect.ignore(readBoundedProbeBody(response)); return { _tag: "not-a-t3-server" } as const; } const descriptor = yield* HttpClientResponse.filterStatusOk(response).pipe( From 797c70b816cb458e6f5fe8b5493d3f40a408aaa1 Mon Sep 17 00:00:00 2001 From: Adamulek123 Date: Sat, 19 Sep 2026 11:07:26 +0200 Subject: [PATCH 08/16] fix(server): drain non-2xx probe bodies for connection reuse --- apps/server/src/cli/pair.test.ts | 48 ++++++++++++++++++++++++++++++++ apps/server/src/cli/pair.ts | 10 +++++-- 2 files changed, 56 insertions(+), 2 deletions(-) diff --git a/apps/server/src/cli/pair.test.ts b/apps/server/src/cli/pair.test.ts index 8704a3de8eb3..e18d239c1c44 100644 --- a/apps/server/src/cli/pair.test.ts +++ b/apps/server/src/cli/pair.test.ts @@ -431,6 +431,54 @@ describe("t3 pair", () => { ).pipe(Effect.provide(NodeServices.layer)), ); + it.effect("does not pair when a valid descriptor arrives with an error status", () => + Effect.acquireUseRelease( + Effect.callback((resume) => { + const server = NodeHttp.createServer((request, response) => { + if (request.url === "/.well-known/t3/environment") { + // A 500 carrying a schema-valid descriptor body: the probe must + // classify it as a stranger (draining the body for connection + // reuse) instead of decoding and pairing with it. + response.writeHead(500, { "content-type": "application/json" }); + response.end(JSON.stringify(testDescriptor)); + return; + } + response.writeHead(404); + response.end(); + }); + server.listen(0, "127.0.0.1", () => resume(Effect.succeed(server))); + }), + (server) => + Effect.gen(function* () { + const address = server.address(); + if (address === null || typeof address === "string") { + return Effect.die(new Error("Expected a TCP address")); + } + const baseDir = NodeFS.mkdtempSync( + NodePath.join(NodeOS.tmpdir(), "t3-pair-status-test-"), + ); + const statePath = NodePath.join(baseDir, "userdata", "server-runtime.json"); + yield* persistServerRuntimeState({ + path: statePath, + state: yield* makePersistedServerRuntimeState({ + config: { host: "127.0.0.1", devUrl: undefined }, + port: address.port, + }), + }); + + const error = yield* provideCliTestLayers( + runCli(["pair", "--base-dir", baseDir]).pipe(Effect.flip), + ); + + const rendered = String( + typeof error === "object" && error !== null && "cause" in error ? error.cause : error, + ); + assert.include(rendered, "No running T3 Code server found."); + }), + (server) => Effect.sync(() => server.close()), + ).pipe(Effect.provide(NodeServices.layer)), + ); + it.effect("pairs when the descriptor arrives with an uppercase JSON content type", () => Effect.acquireUseRelease( Effect.callback((resume) => { diff --git a/apps/server/src/cli/pair.ts b/apps/server/src/cli/pair.ts index d75556c12853..6119575d9752 100644 --- a/apps/server/src/cli/pair.ts +++ b/apps/server/src/cli/pair.ts @@ -271,8 +271,14 @@ const probeEnvironmentDescriptor = ( yield* Effect.ignore(readBoundedProbeBody(response)); return { _tag: "not-a-t3-server" } as const; } - const descriptor = yield* HttpClientResponse.filterStatusOk(response).pipe( - Effect.flatMap(readBoundedProbeBody), + // A non-2xx answer is still a stranger, but its body must be drained + // boundedly for the same reason: an unconsumed stream pins the pooled + // connection across repeated probes. + if (response.status < 200 || response.status >= 300) { + yield* Effect.ignore(readBoundedProbeBody(response)); + return { _tag: "not-a-t3-server" } as const; + } + const descriptor = yield* readBoundedProbeBody(response).pipe( Effect.flatMap((body) => decodeProbeDescriptor(body).pipe( Effect.mapError(() => ({ _tag: "not-a-t3-server" }) as const), From 3e6fba67426864fa186f65a29a62d50b8a8e011c Mon Sep 17 00:00:00 2001 From: Adamulek123 Date: Wed, 16 Sep 2026 07:29:38 +0200 Subject: [PATCH 09/16] perf(server): tighten pair probe budgets --- apps/server/src/cli/pair.ts | 44 +++++++++++++++++++++++++++---------- 1 file changed, 33 insertions(+), 11 deletions(-) diff --git a/apps/server/src/cli/pair.ts b/apps/server/src/cli/pair.ts index 6119575d9752..f019f4342c33 100644 --- a/apps/server/src/cli/pair.ts +++ b/apps/server/src/cli/pair.ts @@ -315,6 +315,16 @@ const discoverPairTarget = Effect.fn("pair.discoverPairTarget")(function* ( } const checkedStatePaths: Array = []; + // Cheap local checks (state file present, pid alive) stay sequential so + // precedence and the checked-paths error read in discovery order; only the + // network probes — up to PAIR_PROBE_TIMEOUT each — run concurrently. The + // candidate set is bounded (bases × two variants), and the first hit in + // precedence order still wins. + const candidates: Array<{ + readonly baseDir: string; + readonly variant: PairStateVariant; + readonly state: PersistedServerRuntimeState; + }> = []; for (const baseDir of new Set(bases)) { for (const variant of ["userdata", "dev"] as const) { const derivedPaths = yield* ServerConfig.deriveServerPaths( @@ -334,18 +344,27 @@ const discoverPairTarget = Effect.fn("pair.discoverPairTarget")(function* ( if (!isProcessAlive(state.value.pid)) { continue; } - const probed = yield* probeEnvironmentDescriptor(state.value.origin); - if (probed._tag !== "descriptor") { - continue; - } - return { - baseDir, - variant, - state: state.value, - descriptor: probed.descriptor, - } satisfies DiscoveredPairTarget; + candidates.push({ baseDir, variant, state: state.value }); } } + const probed = yield* Effect.forEach( + candidates, + (candidate) => + Effect.map(probeEnvironmentDescriptor(candidate.state.origin), (result) => ({ + ...candidate, + result, + })), + { concurrency: "unbounded" }, + ); + const hit = probed.find((candidate) => candidate.result._tag === "descriptor"); + if (hit !== undefined && hit.result._tag === "descriptor") { + return { + baseDir: hit.baseDir, + variant: hit.variant, + state: hit.state, + descriptor: hit.result.descriptor, + } satisfies DiscoveredPairTarget; + } return yield* new NoRunningServerError({ checkedStatePaths }); }); @@ -413,7 +432,10 @@ const awaitEnvironmentDescriptor = Effect.fn(function* (baseUrl: string) { if (last._tag === "descriptor") { return last; } - yield* Effect.sleep(TAILSCALE_PROBE_RETRY_DELAY); + // No sleep after the final attempt: nothing else will use the wait. + if (attempt + 1 < TAILSCALE_PROBE_ATTEMPTS) { + yield* Effect.sleep(TAILSCALE_PROBE_RETRY_DELAY); + } } return last; }); From 6a9fec5a34ae2aea06a1d24f9743ba7a18b84eb0 Mon Sep 17 00:00:00 2001 From: Adamulek123 Date: Fri, 18 Sep 2026 23:49:28 +0200 Subject: [PATCH 10/16] perf(server): cancel lower-priority pair probes once precedence winner is known Fork all candidate probes under a scope, join fibers in discovery order, and return the first descriptor without waiting for losers (scope teardown interrupts them). Adds overlap/cancellation, discovery-order, and test-clock retry-budget coverage. --- apps/server/src/cli/pair.test.ts | 148 ++++++++++++++++++++++++++++++- apps/server/src/cli/pair.ts | 49 +++++----- 2 files changed, 173 insertions(+), 24 deletions(-) diff --git a/apps/server/src/cli/pair.test.ts b/apps/server/src/cli/pair.test.ts index e18d239c1c44..343d7670c5c7 100644 --- a/apps/server/src/cli/pair.test.ts +++ b/apps/server/src/cli/pair.test.ts @@ -8,13 +8,19 @@ import * as NodeServices from "@effect/platform-node/NodeServices"; import * as NetService from "@t3tools/shared/Net"; import { HostProcessEnvironment } from "@t3tools/shared/hostProcess"; import { assert, describe, expect, it } from "@effect/vitest"; -import * as Effect from "effect/Effect"; +import * as Clock from "effect/Clock"; import * as Deferred from "effect/Deferred"; +import * as Duration from "effect/Duration"; +import * as FileSystem from "effect/FileSystem"; +import * as Effect from "effect/Effect"; import * as Fiber from "effect/Fiber"; import * as Layer from "effect/Layer"; +import * as Ref from "effect/Ref"; import * as Schedule from "effect/Schedule"; +import * as TestClock from "effect/testing/TestClock"; import * as TestConsole from "effect/testing/TestConsole"; import { Command } from "effect/unstable/cli"; +import { HttpClient, HttpClientResponse } from "effect/unstable/http"; import { cli } from "../bin.ts"; import { @@ -28,7 +34,9 @@ import { type PersistedServerRuntimeState, } from "../serverRuntimeState.ts"; import { + awaitEnvironmentDescriptor, DevServerNotProxiableError, + discoverPairTarget, resolveDirectPairingBaseUrl, resolveTailscaleLocalTarget, } from "./pair.ts"; @@ -146,6 +154,25 @@ const withDescriptorServer = (run: (origin: string) => Effect.Effect Effect.sync(() => server.close()), ); +const makeDiscoveryFixture = Effect.gen(function* () { + const fs = yield* FileSystem.FileSystem; + const baseDir = yield* fs.makeTempDirectoryScoped({ prefix: "t3-pair-probes-" }); + const candidates = yield* Effect.forEach(["userdata", "dev"] as const, (variant, index) => + Effect.gen(function* () { + const state = yield* makePersistedServerRuntimeState({ + config: { host: "127.0.0.1", devUrl: undefined }, + port: 10_000 + index, + }); + yield* persistServerRuntimeState({ + path: NodePath.join(baseDir, variant, "server-runtime.json"), + state, + }); + return { variant, state }; + }), + ); + return { baseDir, candidates }; +}); + describe("t3 pair", () => { it.effect("mints a token and prints a QR pairing URL for a live server", () => withDescriptorServer((origin) => @@ -590,4 +617,123 @@ describe("t3 pair", () => { }), ).pipe(Effect.provide(NodeServices.layer)); }); + it.effect("overlaps probes and cancels a pending lower-priority request", () => + Effect.gen(function* () { + const { baseDir } = yield* makeDiscoveryFixture; + const lowerStarted = yield* Deferred.make(); + const interrupted = yield* Ref.make(false); + const client = HttpClient.make((request, url, signal) => + url.port === "10000" + ? Deferred.await(lowerStarted).pipe( + Effect.as(HttpClientResponse.fromWeb(request, Response.json(testDescriptor))), + ) + : Deferred.succeed(lowerStarted, signal).pipe( + Effect.andThen(Effect.never), + Effect.onInterrupt(() => Ref.set(interrupted, true)), + ), + ); + const target = yield* discoverPairTarget(baseDir).pipe( + Effect.provideService(HttpClient.HttpClient, client), + ); + expect(target.variant).toBe("userdata"); + expect(yield* Ref.get(interrupted)).toBe(true); + expect((yield* Deferred.await(lowerStarted)).aborted).toBe(true); + }).pipe(Effect.provide(NodeServices.layer)), + ); +}); + +describe("pair discovery order", () => { + it.effect.each([ + { status: 200, winner: "userdata" }, + { status: 503, winner: "dev" }, + { status: 404, winner: "dev" }, + ])( + "selects $winner when the earlier probe returns $status after the later success", + ({ status, winner }) => + Effect.gen(function* () { + const { baseDir, candidates } = yield* makeDiscoveryFixture; + const lowerCompleted = yield* Deferred.make(); + const releaseFirst = yield* Deferred.make(); + const firstStarted = yield* Deferred.make(); + const client = HttpClient.make((request, url) => + Effect.gen(function* () { + if (url.port === "10000") { + yield* Deferred.succeed(firstStarted, undefined); + yield* Deferred.await(lowerCompleted); + yield* Deferred.await(releaseFirst); + return HttpClientResponse.fromWeb(request, Response.json(testDescriptor, { status })); + } + yield* Deferred.await(firstStarted); + yield* Deferred.succeed(lowerCompleted, undefined); + return HttpClientResponse.fromWeb(request, Response.json(testDescriptor)); + }), + ); + const fiber = yield* discoverPairTarget(baseDir).pipe( + Effect.provideService(HttpClient.HttpClient, client), + Effect.forkScoped, + ); + yield* Deferred.await(lowerCompleted); + yield* Deferred.succeed(releaseFirst, undefined); + const target = yield* Fiber.join(fiber); + expect(target.variant).toBe(winner); + expect(target.state).toEqual( + candidates.find((candidate) => candidate.variant === winner)?.state, + ); + expect(target.descriptor).toEqual(testDescriptor); + }).pipe(Effect.provide(NodeServices.layer)), + ); + + it.effect("interrupts all pending requests when discovery is cancelled", () => + Effect.gen(function* () { + const { baseDir } = yield* makeDiscoveryFixture; + const started = yield* Deferred.make(); + const signals = yield* Ref.make>([]); + const client = HttpClient.make((_request, _url, signal) => + Effect.gen(function* () { + const pending = yield* Ref.updateAndGet(signals, (current) => [...current, signal]); + if (pending.length === 2) { + yield* Deferred.succeed(started, undefined); + } + return yield* Effect.never; + }), + ); + const fiber = yield* discoverPairTarget(baseDir).pipe( + Effect.provideService(HttpClient.HttpClient, client), + Effect.forkScoped, + ); + yield* Deferred.await(started); + yield* Fiber.interrupt(fiber); + expect((yield* Ref.get(signals)).map((signal) => signal.aborted)).toEqual([true, true]); + }).pipe(Effect.provide(NodeServices.layer)), + ); +}); + +describe("pair tailscale probe budget", () => { + it.effect("retries all attempts but sleeps only between them", () => + Effect.gen(function* () { + const startedAt = yield* Clock.currentTimeMillis; + const attempts = yield* Ref.make>([]); + const firstAttempt = yield* Deferred.make(); + const client = HttpClient.make((request) => + Effect.gen(function* () { + const now = yield* Clock.currentTimeMillis; + yield* Ref.update(attempts, (times) => [...times, now - startedAt]); + yield* Deferred.succeed(firstAttempt, undefined); + return HttpClientResponse.fromWeb(request, new Response(null, { status: 503 })); + }), + ); + const fiber = yield* awaitEnvironmentDescriptor("http://127.0.0.1:1").pipe( + Effect.provide(Layer.succeed(HttpClient.HttpClient, client)), + Effect.forkScoped, + ); + yield* Deferred.await(firstAttempt); + yield* TestClock.adjust(Duration.millis(3_999)); + expect(yield* Ref.get(attempts)).toEqual([0, 1_000, 2_000, 3_000]); + yield* TestClock.adjust(Duration.millis(1)); + const result = yield* Fiber.join(fiber); + expect(result._tag).toBe("unreachable"); + expect(yield* Ref.get(attempts)).toEqual([0, 1_000, 2_000, 3_000, 4_000]); + expect((yield* Clock.currentTimeMillis) - startedAt).toBe(4_000); + }), + ); }); diff --git a/apps/server/src/cli/pair.ts b/apps/server/src/cli/pair.ts index f019f4342c33..d1c93871a38c 100644 --- a/apps/server/src/cli/pair.ts +++ b/apps/server/src/cli/pair.ts @@ -27,6 +27,7 @@ import * as Console from "effect/Console"; import * as DateTime from "effect/DateTime"; import * as Duration from "effect/Duration"; import * as Effect from "effect/Effect"; +import * as Fiber from "effect/Fiber"; import * as Layer from "effect/Layer"; import * as Option from "effect/Option"; import * as References from "effect/References"; @@ -296,7 +297,7 @@ interface DiscoveredPairTarget { readonly descriptor: ExecutionEnvironmentDescriptor; } -const discoverPairTarget = Effect.fn("pair.discoverPairTarget")(function* ( +export const discoverPairTarget = Effect.fn("pair.discoverPairTarget")(function* ( explicitBaseDir: string | undefined, ) { const bases: Array = []; @@ -315,11 +316,6 @@ const discoverPairTarget = Effect.fn("pair.discoverPairTarget")(function* ( } const checkedStatePaths: Array = []; - // Cheap local checks (state file present, pid alive) stay sequential so - // precedence and the checked-paths error read in discovery order; only the - // network probes — up to PAIR_PROBE_TIMEOUT each — run concurrently. The - // candidate set is bounded (bases × two variants), and the first hit in - // precedence order still wins. const candidates: Array<{ readonly baseDir: string; readonly variant: PairStateVariant; @@ -347,23 +343,30 @@ const discoverPairTarget = Effect.fn("pair.discoverPairTarget")(function* ( candidates.push({ baseDir, variant, state: state.value }); } } - const probed = yield* Effect.forEach( - candidates, - (candidate) => - Effect.map(probeEnvironmentDescriptor(candidate.state.origin), (result) => ({ - ...candidate, - result, - })), - { concurrency: "unbounded" }, + const hit = yield* Effect.scoped( + Effect.gen(function* () { + const fibers = yield* Effect.forEach(candidates, (candidate) => + probeEnvironmentDescriptor(candidate.state.origin).pipe( + Effect.forkScoped, + Effect.map((fiber) => ({ ...candidate, fiber })), + ), + ); + for (const { fiber, baseDir, variant, state } of fibers) { + const result = yield* Fiber.join(fiber); + if (result._tag === "descriptor") { + return Option.some({ + baseDir, + variant, + state, + descriptor: result.descriptor, + } satisfies DiscoveredPairTarget); + } + } + return Option.none(); + }), ); - const hit = probed.find((candidate) => candidate.result._tag === "descriptor"); - if (hit !== undefined && hit.result._tag === "descriptor") { - return { - baseDir: hit.baseDir, - variant: hit.variant, - state: hit.state, - descriptor: hit.result.descriptor, - } satisfies DiscoveredPairTarget; + if (Option.isSome(hit)) { + return hit.value; } return yield* new NoRunningServerError({ checkedStatePaths }); }); @@ -425,7 +428,7 @@ const makePairServerConfig = Effect.fn(function* (input: { }); }); -const awaitEnvironmentDescriptor = Effect.fn(function* (baseUrl: string) { +export const awaitEnvironmentDescriptor = Effect.fn(function* (baseUrl: string) { let last: EnvironmentProbeResult = { _tag: "unreachable" }; for (let attempt = 0; attempt < TAILSCALE_PROBE_ATTEMPTS; attempt += 1) { last = yield* probeEnvironmentDescriptor(baseUrl); From 6947f220db3885c9c05b4a5c1eb761dd34e1072b Mon Sep 17 00:00:00 2001 From: Adamulek123 Date: Mon, 21 Sep 2026 21:05:42 +0200 Subject: [PATCH 11/16] chore(ci): retrigger pull request checks From 6b91dc5a2e29ce98f0dda695c91978a9f5e03ab3 Mon Sep 17 00:00:00 2001 From: Adamulek123 Date: Sat, 26 Sep 2026 18:09:58 +0200 Subject: [PATCH 12/16] fix(server): share pair probe deadline across response phases --- apps/server/src/cli/pair.test.ts | 109 +++++++++++++++++++++---------- apps/server/src/cli/pair.ts | 65 +++++++++--------- 2 files changed, 106 insertions(+), 68 deletions(-) diff --git a/apps/server/src/cli/pair.test.ts b/apps/server/src/cli/pair.test.ts index 343d7670c5c7..fd12f41554a4 100644 --- a/apps/server/src/cli/pair.test.ts +++ b/apps/server/src/cli/pair.test.ts @@ -506,46 +506,54 @@ describe("t3 pair", () => { ).pipe(Effect.provide(NodeServices.layer)), ); - it.effect("pairs when the descriptor arrives with an uppercase JSON content type", () => - Effect.acquireUseRelease( - Effect.callback((resume) => { - const server = NodeHttp.createServer((request, response) => { - if (request.url === "/.well-known/t3/environment") { - response.writeHead(200, { "content-type": "Application/JSON; charset=utf-8" }); - response.end(JSON.stringify(testDescriptor)); - return; - } - response.writeHead(404); - response.end(); - }); - server.listen(0, "127.0.0.1", () => resume(Effect.succeed(server))); - }), - (server) => - Effect.gen(function* () { - const address = server.address(); - if (address === null || typeof address === "string") { - return Effect.die(new Error("Expected a TCP address")); - } - const baseDir = NodeFS.mkdtempSync(NodePath.join(NodeOS.tmpdir(), "t3-pair-ctype-test-")); - const statePath = NodePath.join(baseDir, "userdata", "server-runtime.json"); - yield* persistServerRuntimeState({ - path: statePath, - state: yield* makePersistedServerRuntimeState({ - config: { host: "127.0.0.1", devUrl: undefined }, - port: address.port, - }), + it.effect.each(["Application/JSON; charset=utf-8", "application/vnd.t3+json"])( + "pairs when the descriptor arrives with content type %s", + (contentType) => + Effect.acquireUseRelease( + Effect.callback((resume) => { + const server = NodeHttp.createServer((request, response) => { + if (request.url === "/.well-known/t3/environment") { + response.writeHead(200, { "content-type": contentType }); + response.end(JSON.stringify(testDescriptor)); + return; + } + response.writeHead(404); + response.end(); }); + server.listen(0, "127.0.0.1", () => resume(Effect.succeed(server))); + }), + (server) => + Effect.gen(function* () { + const address = server.address(); + if (address === null || typeof address === "string") { + return Effect.die(new Error("Expected a TCP address")); + } + const baseDir = NodeFS.mkdtempSync( + NodePath.join(NodeOS.tmpdir(), "t3-pair-ctype-test-"), + ); + const statePath = NodePath.join(baseDir, "userdata", "server-runtime.json"); + yield* persistServerRuntimeState({ + path: statePath, + state: yield* makePersistedServerRuntimeState({ + config: { host: "127.0.0.1", devUrl: undefined }, + port: address.port, + }), + }); - const output = yield* captureStdout(runCli(["pair", "--base-dir", baseDir])); + const output = yield* captureStdout(runCli(["pair", "--base-dir", baseDir])); - assert.include(output, "Pairing with pair-test ("); - assert.include(output, "/pair#token="); - }), - (server) => Effect.sync(() => server.close()), - ).pipe(Effect.provide(NodeServices.layer)), + assert.include(output, "Pairing with pair-test ("); + assert.include(output, "/pair#token="); + }), + (server) => Effect.sync(() => server.close()), + ).pipe(Effect.provide(NodeServices.layer)), ); - it.live("times out a slow-drip stranger body instead of hanging discovery", () => { + it.live.each([ + { status: 200, contentType: "application/json" }, + { status: 200, contentType: "text/html" }, + { status: 500, contentType: "application/json" }, + ])("times out a slow-drip $status $contentType body", ({ status, contentType }) => { // Handoff from the raw Node request handler below: it holds the response // open so the test fiber can drip into it. Completed synchronously from // the callback via `doneUnsafe`, so no manual Effect runtime is created @@ -555,7 +563,7 @@ describe("t3 pair", () => { Effect.callback((resume) => { const server = NodeHttp.createServer((request, response) => { if (request.url === "/.well-known/t3/environment") { - response.writeHead(200, { "content-type": "application/json" }); + response.writeHead(status, { "content-type": contentType }); // A never-ending drip that stays under the size cap: discovery // must give up via the body timeout rather than hang on the open // stream. @@ -643,6 +651,35 @@ describe("t3 pair", () => { }); describe("pair discovery order", () => { + it.effect("shares the probe deadline between headers and body", () => + Effect.gen(function* () { + const { baseDir } = yield* makeDiscoveryFixture; + const firstRequest = yield* Deferred.make(); + const client = HttpClient.make((request) => + Effect.gen(function* () { + yield* Deferred.succeed(firstRequest, undefined); + yield* Effect.sleep("2 seconds"); + return HttpClientResponse.fromWeb( + request, + new Response(new ReadableStream(), { + headers: { "content-type": "application/json" }, + }), + ); + }), + ); + const fiber = yield* discoverPairTarget(baseDir).pipe( + Effect.provideService(HttpClient.HttpClient, client), + Effect.flip, + Effect.forkScoped, + ); + yield* Deferred.await(firstRequest); + yield* TestClock.adjust("2 seconds"); + yield* TestClock.adjust("500 millis"); + const error = yield* Fiber.join(fiber); + expect(error._tag).toBe("NoRunningServerError"); + }).pipe(Effect.provide(NodeServices.layer)), + ); + it.effect.each([ { status: 200, winner: "userdata" }, { status: 503, winner: "dev" }, diff --git a/apps/server/src/cli/pair.ts b/apps/server/src/cli/pair.ts index d1c93871a38c..ac9bfa071516 100644 --- a/apps/server/src/cli/pair.ts +++ b/apps/server/src/cli/pair.ts @@ -23,6 +23,7 @@ import { readTailscaleStatus, } from "@t3tools/tailscale"; import * as Config from "effect/Config"; +import * as Clock from "effect/Clock"; import * as Console from "effect/Console"; import * as DateTime from "effect/DateTime"; import * as Duration from "effect/Duration"; @@ -214,30 +215,33 @@ type EnvironmentProbeResult = | { readonly _tag: "unreachable" } | { readonly _tag: "not-a-t3-server" }; -const readBoundedProbeBody = (ok: HttpClientResponse.HttpClientResponse) => - ok.stream.pipe( - Stream.runFoldEffect( - () => ({ bytes: 0, chunks: [] as Array }), - (acc, chunk) => { - if (acc.bytes + chunk.byteLength > MAX_PROBE_BODY_BYTES) { - return Effect.fail({ _tag: "not-a-t3-server" } as const); +const readBoundedProbeBody = (ok: HttpClientResponse.HttpClientResponse, deadline: number) => + Effect.gen(function* () { + const remaining = Math.max(0, deadline - (yield* Clock.currentTimeMillis)); + return yield* ok.stream.pipe( + Stream.runFoldEffect( + () => ({ bytes: 0, chunks: [] as Array }), + (acc, chunk) => { + if (acc.bytes + chunk.byteLength > MAX_PROBE_BODY_BYTES) { + return Effect.fail({ _tag: "not-a-t3-server" } as const); + } + acc.bytes += chunk.byteLength; + acc.chunks.push(chunk); + return Effect.succeed(acc); + }, + ), + Effect.map(({ bytes, chunks }) => { + const merged = new Uint8Array(bytes); + let offset = 0; + for (const chunk of chunks) { + merged.set(chunk, offset); + offset += chunk.byteLength; } - acc.bytes += chunk.byteLength; - acc.chunks.push(chunk); - return Effect.succeed(acc); - }, - ), - Effect.map(({ bytes, chunks }) => { - const merged = new Uint8Array(bytes); - let offset = 0; - for (const chunk of chunks) { - merged.set(chunk, offset); - offset += chunk.byteLength; - } - return new TextDecoder().decode(merged); - }), - Effect.timeout(PAIR_PROBE_TIMEOUT), - ); + return new TextDecoder().decode(merged); + }), + Effect.timeout(Duration.millis(remaining)), + ); + }); const probeEnvironmentDescriptor = ( baseUrl: string, @@ -245,6 +249,7 @@ const probeEnvironmentDescriptor = ( Effect.gen(function* () { const client = yield* HttpClient.HttpClient; const request = HttpClientRequest.get(new URL(WELL_KNOWN_ENVIRONMENT_PATH, baseUrl).toString()); + const deadline = (yield* Clock.currentTimeMillis) + Duration.toMillis(PAIR_PROBE_TIMEOUT); const response = yield* client.execute(request).pipe( Effect.timeout(PAIR_PROBE_TIMEOUT), // Transport failure or timeout: nothing (reachable) is listening there. @@ -257,7 +262,7 @@ const probeEnvironmentDescriptor = ( // connection is reusable for the re-configured mapping; the drain itself // is time-bounded and best-effort. if (response.status === 502 || response.status === 503 || response.status === 504) { - yield* Effect.ignore(readBoundedProbeBody(response)); + yield* Effect.ignore(readBoundedProbeBody(response, deadline)); return { _tag: "unreachable" } as const; } // Anything else that answered HTTP but not with a valid descriptor is @@ -269,22 +274,18 @@ const probeEnvironmentDescriptor = ( const contentType = response.headers["content-type"] ?? ""; const mediaType = contentType.split(";", 1)[0]?.trim().toLowerCase(); if (mediaType !== "application/json" && !mediaType?.endsWith("+json")) { - yield* Effect.ignore(readBoundedProbeBody(response)); + yield* Effect.ignore(readBoundedProbeBody(response, deadline)); return { _tag: "not-a-t3-server" } as const; } // A non-2xx answer is still a stranger, but its body must be drained // boundedly for the same reason: an unconsumed stream pins the pooled // connection across repeated probes. if (response.status < 200 || response.status >= 300) { - yield* Effect.ignore(readBoundedProbeBody(response)); + yield* Effect.ignore(readBoundedProbeBody(response, deadline)); return { _tag: "not-a-t3-server" } as const; } - const descriptor = yield* readBoundedProbeBody(response).pipe( - Effect.flatMap((body) => - decodeProbeDescriptor(body).pipe( - Effect.mapError(() => ({ _tag: "not-a-t3-server" }) as const), - ), - ), + const descriptor = yield* readBoundedProbeBody(response, deadline).pipe( + Effect.flatMap(decodeProbeDescriptor), Effect.mapError(() => ({ _tag: "not-a-t3-server" }) as const), ); return { _tag: "descriptor", descriptor } as const; From fa0a8999b81b88943d000e49393eb00cdf9eff66 Mon Sep 17 00:00:00 2001 From: Adamulek123 Date: Thu, 1 Oct 2026 15:23:35 +0200 Subject: [PATCH 13/16] docs(server): explain pair probe selection precedence --- apps/server/src/cli/pair.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/apps/server/src/cli/pair.ts b/apps/server/src/cli/pair.ts index ac9bfa071516..1764abf0ee72 100644 --- a/apps/server/src/cli/pair.ts +++ b/apps/server/src/cli/pair.ts @@ -317,6 +317,8 @@ export const discoverPairTarget = Effect.fn("pair.discoverPairTarget")(function* } const checkedStatePaths: Array = []; + // Probe concurrently, but keep state-file precedence when choosing a hit: + // a later success must wait for every earlier candidate to finish. const candidates: Array<{ readonly baseDir: string; readonly variant: PairStateVariant; From f7fc8985f3dae8935b812d8cd049047b8905f7d2 Mon Sep 17 00:00:00 2001 From: Adamulek123 Date: Thu, 1 Oct 2026 15:23:51 +0200 Subject: [PATCH 14/16] fix(server): measure pair probe deadlines monotonically --- apps/server/src/cli/pair.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/apps/server/src/cli/pair.ts b/apps/server/src/cli/pair.ts index 1764abf0ee72..5f8ac6023924 100644 --- a/apps/server/src/cli/pair.ts +++ b/apps/server/src/cli/pair.ts @@ -215,9 +215,9 @@ type EnvironmentProbeResult = | { readonly _tag: "unreachable" } | { readonly _tag: "not-a-t3-server" }; -const readBoundedProbeBody = (ok: HttpClientResponse.HttpClientResponse, deadline: number) => +const readBoundedProbeBody = (ok: HttpClientResponse.HttpClientResponse, deadline: bigint) => Effect.gen(function* () { - const remaining = Math.max(0, deadline - (yield* Clock.currentTimeMillis)); + const remaining = deadline - (yield* Clock.monotonicTimeNanos); return yield* ok.stream.pipe( Stream.runFoldEffect( () => ({ bytes: 0, chunks: [] as Array }), @@ -239,7 +239,7 @@ const readBoundedProbeBody = (ok: HttpClientResponse.HttpClientResponse, deadlin } return new TextDecoder().decode(merged); }), - Effect.timeout(Duration.millis(remaining)), + Effect.timeout(Duration.nanos(remaining > 0n ? remaining : 0n)), ); }); @@ -249,7 +249,7 @@ const probeEnvironmentDescriptor = ( Effect.gen(function* () { const client = yield* HttpClient.HttpClient; const request = HttpClientRequest.get(new URL(WELL_KNOWN_ENVIRONMENT_PATH, baseUrl).toString()); - const deadline = (yield* Clock.currentTimeMillis) + Duration.toMillis(PAIR_PROBE_TIMEOUT); + const deadline = (yield* Clock.monotonicTimeNanos) + Duration.toNanosUnsafe(PAIR_PROBE_TIMEOUT); const response = yield* client.execute(request).pipe( Effect.timeout(PAIR_PROBE_TIMEOUT), // Transport failure or timeout: nothing (reachable) is listening there. From feec3eb3652298a10272ab729775c87e2c73c9da Mon Sep 17 00:00:00 2001 From: Adamulek123 Date: Thu, 1 Oct 2026 15:26:15 +0200 Subject: [PATCH 15/16] test(server): share pair probe fixtures and yield defects --- apps/server/src/cli/pair.test.ts | 292 ++++++++++--------------------- 1 file changed, 90 insertions(+), 202 deletions(-) diff --git a/apps/server/src/cli/pair.test.ts b/apps/server/src/cli/pair.test.ts index e18d239c1c44..b9248cbcf3f7 100644 --- a/apps/server/src/cli/pair.test.ts +++ b/apps/server/src/cli/pair.test.ts @@ -11,6 +11,7 @@ import { assert, describe, expect, it } from "@effect/vitest"; import * as Effect from "effect/Effect"; import * as Deferred from "effect/Deferred"; import * as Fiber from "effect/Fiber"; +import * as FileSystem from "effect/FileSystem"; import * as Layer from "effect/Layer"; import * as Schedule from "effect/Schedule"; import * as TestConsole from "effect/testing/TestConsole"; @@ -122,6 +123,60 @@ const testDescriptor = { capabilities: { repositoryIdentity: true }, }; +const withProbeServer = ( + { status, contentType, body }: { status: number; contentType: string; body: string }, + run: (baseDir: string, origin: string) => Effect.Effect, +) => + Effect.scoped( + Effect.acquireUseRelease( + Effect.callback((resume) => { + const server = NodeHttp.createServer((request, response) => { + if (request.url === "/.well-known/t3/environment") { + response.writeHead(status, { "content-type": contentType }); + response.end(body); + return; + } + response.writeHead(404); + response.end(); + }); + server.listen(0, "127.0.0.1", () => resume(Effect.succeed(server))); + }), + (server) => + Effect.gen(function* () { + const address = server.address(); + if (address === null || typeof address === "string") { + return yield* Effect.die(new Error("Expected a TCP address")); + } + const fs = yield* FileSystem.FileSystem; + const baseDir = yield* fs.makeTempDirectoryScoped({ prefix: "t3-pair-probe-test-" }); + yield* persistServerRuntimeState({ + path: NodePath.join(baseDir, "userdata", "server-runtime.json"), + state: yield* makePersistedServerRuntimeState({ + config: { host: "127.0.0.1", devUrl: undefined }, + port: address.port, + }), + }); + return yield* run(baseDir, `http://127.0.0.1:${String(address.port)}`); + }), + (server) => + Effect.sync(() => { + server.closeAllConnections(); + server.close(); + }), + ), + ); + +const assertPairRejected = (baseDir: string) => + Effect.gen(function* () { + const error = yield* provideCliTestLayers( + runCli(["pair", "--base-dir", baseDir]).pipe(Effect.flip), + ); + const rendered = String( + typeof error === "object" && error !== null && "cause" in error ? error.cause : error, + ); + assert.include(rendered, "No running T3 Code server found."); + }); + const withDescriptorServer = (run: (origin: string) => Effect.Effect) => Effect.acquireUseRelease( Effect.callback((resume) => { @@ -293,228 +348,61 @@ describe("t3 pair", () => { ); it.effect("does not decode an oversized stranger body as a server descriptor", () => - Effect.acquireUseRelease( - Effect.callback((resume) => { - const server = NodeHttp.createServer((request, response) => { - if (request.url === "/.well-known/t3/environment") { - response.writeHead(200, { "content-type": "application/json" }); - // A schema-valid descriptor padded far beyond any real one: - // discovery must reject it on size without a Schema decode - // (without the cap this decodes fine and pairs), not pair with it. - response.end(JSON.stringify({ ...testDescriptor, padding: "x".repeat(128 * 1024) })); - return; - } - response.writeHead(404); - response.end(); - }); - server.listen(0, "127.0.0.1", () => resume(Effect.succeed(server))); - }), - (server) => - Effect.gen(function* () { - const address = server.address(); - if (address === null || typeof address === "string") { - return Effect.die(new Error("Expected a TCP address")); - } - const baseDir = NodeFS.mkdtempSync(NodePath.join(NodeOS.tmpdir(), "t3-pair-big-test-")); - const statePath = NodePath.join(baseDir, "userdata", "server-runtime.json"); - yield* persistServerRuntimeState({ - path: statePath, - state: yield* makePersistedServerRuntimeState({ - config: { host: "127.0.0.1", devUrl: undefined }, - port: address.port, - }), - }); - - const error = yield* provideCliTestLayers( - runCli(["pair", "--base-dir", baseDir]).pipe(Effect.flip), - ); - - const rendered = String( - typeof error === "object" && error !== null && "cause" in error ? error.cause : error, - ); - assert.include(rendered, "No running T3 Code server found."); - }), - (server) => Effect.sync(() => server.close()), + withProbeServer( + { + status: 200, + contentType: "application/json", + // Schema-valid JSON that would pair without the byte cap. + body: JSON.stringify({ ...testDescriptor, padding: "x".repeat(128 * 1024) }), + }, + assertPairRejected, ).pipe(Effect.provide(NodeServices.layer)), ); it.effect("rejects a descriptor that exceeds the cap in UTF-8 bytes only", () => - Effect.acquireUseRelease( - Effect.callback((resume) => { - const server = NodeHttp.createServer((request, response) => { - if (request.url === "/.well-known/t3/environment") { - response.writeHead(200, { "content-type": "application/json" }); - // "é" is two bytes in UTF-8 but one UTF-16 code unit: this body - // is ~80 KiB on the wire yet under 64 KiB in string length, so a - // string-length check would accept and pair with it. The probe - // must enforce the cap in bytes, before decoding. - response.end(JSON.stringify({ ...testDescriptor, label: "é".repeat(40 * 1024) })); - return; - } - response.writeHead(404); - response.end(); - }); - server.listen(0, "127.0.0.1", () => resume(Effect.succeed(server))); - }), - (server) => - Effect.gen(function* () { - const address = server.address(); - if (address === null || typeof address === "string") { - return Effect.die(new Error("Expected a TCP address")); - } - const baseDir = NodeFS.mkdtempSync(NodePath.join(NodeOS.tmpdir(), "t3-pair-utf8-test-")); - const statePath = NodePath.join(baseDir, "userdata", "server-runtime.json"); - yield* persistServerRuntimeState({ - path: statePath, - state: yield* makePersistedServerRuntimeState({ - config: { host: "127.0.0.1", devUrl: undefined }, - port: address.port, - }), - }); - - const error = yield* provideCliTestLayers( - runCli(["pair", "--base-dir", baseDir]).pipe(Effect.flip), - ); - - const rendered = String( - typeof error === "object" && error !== null && "cause" in error ? error.cause : error, - ); - assert.include(rendered, "No running T3 Code server found."); - }), - (server) => Effect.sync(() => server.close()), + withProbeServer( + { + status: 200, + contentType: "application/json", + // Two UTF-8 bytes per character: below 64 KiB in string length, + // above it on the wire. A string-length cap would accept this. + body: JSON.stringify({ ...testDescriptor, label: "é".repeat(40 * 1024) }), + }, + assertPairRejected, ).pipe(Effect.provide(NodeServices.layer)), ); it.effect("does not pair when a valid descriptor arrives as non-JSON", () => - Effect.acquireUseRelease( - Effect.callback((resume) => { - const server = NodeHttp.createServer((request, response) => { - if (request.url === "/.well-known/t3/environment") { - // Schema-valid JSON served as HTML: the probe must classify it - // as a stranger (and drain the body for connection reuse) - // instead of decoding and pairing with it. - response.writeHead(200, { "content-type": "text/html; charset=utf-8" }); - response.end(JSON.stringify(testDescriptor)); - return; - } - response.writeHead(404); - response.end(); - }); - server.listen(0, "127.0.0.1", () => resume(Effect.succeed(server))); - }), - (server) => - Effect.gen(function* () { - const address = server.address(); - if (address === null || typeof address === "string") { - return Effect.die(new Error("Expected a TCP address")); - } - const baseDir = NodeFS.mkdtempSync(NodePath.join(NodeOS.tmpdir(), "t3-pair-ctype-test-")); - const statePath = NodePath.join(baseDir, "userdata", "server-runtime.json"); - yield* persistServerRuntimeState({ - path: statePath, - state: yield* makePersistedServerRuntimeState({ - config: { host: "127.0.0.1", devUrl: undefined }, - port: address.port, - }), - }); - - const error = yield* provideCliTestLayers( - runCli(["pair", "--base-dir", baseDir]).pipe(Effect.flip), - ); - - const rendered = String( - typeof error === "object" && error !== null && "cause" in error ? error.cause : error, - ); - assert.include(rendered, "No running T3 Code server found."); - }), - (server) => Effect.sync(() => server.close()), + withProbeServer( + { + status: 200, + contentType: "text/html; charset=utf-8", + body: JSON.stringify(testDescriptor), + }, + assertPairRejected, ).pipe(Effect.provide(NodeServices.layer)), ); it.effect("does not pair when a valid descriptor arrives with an error status", () => - Effect.acquireUseRelease( - Effect.callback((resume) => { - const server = NodeHttp.createServer((request, response) => { - if (request.url === "/.well-known/t3/environment") { - // A 500 carrying a schema-valid descriptor body: the probe must - // classify it as a stranger (draining the body for connection - // reuse) instead of decoding and pairing with it. - response.writeHead(500, { "content-type": "application/json" }); - response.end(JSON.stringify(testDescriptor)); - return; - } - response.writeHead(404); - response.end(); - }); - server.listen(0, "127.0.0.1", () => resume(Effect.succeed(server))); - }), - (server) => - Effect.gen(function* () { - const address = server.address(); - if (address === null || typeof address === "string") { - return Effect.die(new Error("Expected a TCP address")); - } - const baseDir = NodeFS.mkdtempSync( - NodePath.join(NodeOS.tmpdir(), "t3-pair-status-test-"), - ); - const statePath = NodePath.join(baseDir, "userdata", "server-runtime.json"); - yield* persistServerRuntimeState({ - path: statePath, - state: yield* makePersistedServerRuntimeState({ - config: { host: "127.0.0.1", devUrl: undefined }, - port: address.port, - }), - }); - - const error = yield* provideCliTestLayers( - runCli(["pair", "--base-dir", baseDir]).pipe(Effect.flip), - ); - - const rendered = String( - typeof error === "object" && error !== null && "cause" in error ? error.cause : error, - ); - assert.include(rendered, "No running T3 Code server found."); - }), - (server) => Effect.sync(() => server.close()), + withProbeServer( + { status: 500, contentType: "application/json", body: JSON.stringify(testDescriptor) }, + assertPairRejected, ).pipe(Effect.provide(NodeServices.layer)), ); it.effect("pairs when the descriptor arrives with an uppercase JSON content type", () => - Effect.acquireUseRelease( - Effect.callback((resume) => { - const server = NodeHttp.createServer((request, response) => { - if (request.url === "/.well-known/t3/environment") { - response.writeHead(200, { "content-type": "Application/JSON; charset=utf-8" }); - response.end(JSON.stringify(testDescriptor)); - return; - } - response.writeHead(404); - response.end(); - }); - server.listen(0, "127.0.0.1", () => resume(Effect.succeed(server))); - }), - (server) => + withProbeServer( + { + status: 200, + contentType: "Application/JSON; charset=utf-8", + body: JSON.stringify(testDescriptor), + }, + (baseDir) => Effect.gen(function* () { - const address = server.address(); - if (address === null || typeof address === "string") { - return Effect.die(new Error("Expected a TCP address")); - } - const baseDir = NodeFS.mkdtempSync(NodePath.join(NodeOS.tmpdir(), "t3-pair-ctype-test-")); - const statePath = NodePath.join(baseDir, "userdata", "server-runtime.json"); - yield* persistServerRuntimeState({ - path: statePath, - state: yield* makePersistedServerRuntimeState({ - config: { host: "127.0.0.1", devUrl: undefined }, - port: address.port, - }), - }); - const output = yield* captureStdout(runCli(["pair", "--base-dir", baseDir])); - assert.include(output, "Pairing with pair-test ("); assert.include(output, "/pair#token="); }), - (server) => Effect.sync(() => server.close()), ).pipe(Effect.provide(NodeServices.layer)), ); @@ -546,7 +434,7 @@ describe("t3 pair", () => { Effect.gen(function* () { const address = server.address(); if (address === null || typeof address === "string") { - return Effect.die(new Error("Expected a TCP address")); + return yield* Effect.die(new Error("Expected a TCP address")); } const baseDir = NodeFS.mkdtempSync( NodePath.join(NodeOS.tmpdir(), "t3-pair-drip-test-"), From 828d8d3dce93ca293e7524cffde0f765c0761db9 Mon Sep 17 00:00:00 2001 From: Adamulek123 Date: Thu, 1 Oct 2026 15:37:03 +0200 Subject: [PATCH 16/16] chore(ci): retry unrelated persistence test timeout