// @effect-diagnostics nodeBuiltinImport:off - CLI integration exercises Node HTTP and filesystem boundaries. import * as NodeHttp from "node:http"; import * as NodeFS from "node:fs"; import * as NodeOS from "node:os"; import * as NodePath from "node:path"; import * as NodeServices from "@effect/platform-node/NodeServices"; import * as NetService from "@t3tools/shared/Net"; import { assert, describe, expect, it } from "@effect/vitest"; import * as Effect from "effect/Effect"; import * as Layer from "effect/Layer"; import * as TestConsole from "effect/testing/TestConsole"; import { Command } from "effect/unstable/cli"; import { cli } from "../bin.ts"; import { makePersistedServerRuntimeState, persistServerRuntimeState, type PersistedServerRuntimeState, } from "../serverRuntimeState.ts"; import { DevServerNotProxiableError, resolveDirectPairingBaseUrl, resolveTailscaleLocalTarget, } from "./pair.ts"; const CliRuntimeLayer = Layer.mergeAll(NodeServices.layer, NetService.layer); const baseState = { version: 1, pid: 123, port: 3_773, origin: "http://127.0.0.1:3773", startedAt: "2026-06-20T00:00:00.000Z", } as const satisfies PersistedServerRuntimeState; describe("pair base URL selection", () => { it("pairs through the dev web origin when the server fronts a dev server", () => { expect(resolveDirectPairingBaseUrl({ ...baseState, devUrl: "http://localhost:5733/" })).toBe( "http://localhost:5733/", ); }); it("pairs through the bound host when there is no dev server", () => { expect(resolveDirectPairingBaseUrl({ ...baseState, host: "100.64.0.7" })).toBe( "http://100.64.0.7:3773", ); expect(resolveDirectPairingBaseUrl(baseState)).toBe("http://localhost:3773"); }); }); describe("pair tailscale local target", () => { it("proxies the dev web port for dev servers", () => { expect(resolveTailscaleLocalTarget({ ...baseState, devUrl: "http://localhost:5733/" })).toEqual( { localPort: 5_733 }, ); // A dev server on a non-loopback interface must be proxied at that // interface; tailscale serve defaults to 127.0.0.1 otherwise. expect( resolveTailscaleLocalTarget({ ...baseState, devUrl: "http://192.168.1.10:5733/" }), ).toEqual({ localPort: 5_733, localHost: "192.168.1.10" }); // URL.hostname keeps IPv6 brackets, so the serve target stays valid. expect( resolveTailscaleLocalTarget({ ...baseState, devUrl: "http://[fd7a:115c::1]:5733/" }), ).toEqual({ localPort: 5_733, localHost: "[fd7a:115c::1]" }); }); it("rejects HTTPS dev URLs, which tailscale serve cannot proxy", () => { expect( resolveTailscaleLocalTarget({ ...baseState, devUrl: "https://localhost:5733/" }), ).toBeInstanceOf(DevServerNotProxiableError); }); it("proxies the backend port directly otherwise", () => { expect(resolveTailscaleLocalTarget(baseState)).toEqual({ localPort: 3_773 }); expect(resolveTailscaleLocalTarget({ ...baseState, host: "0.0.0.0" })).toEqual({ localPort: 3_773, }); expect(resolveTailscaleLocalTarget({ ...baseState, host: "192.168.1.42" })).toEqual({ localPort: 3_773, localHost: "192.168.1.42", }); }); }); const runCli = (args: ReadonlyArray) => Command.runWith(cli, { version: "0.0.0" })(args); const provideCliTestLayers = (effect: Effect.Effect) => Effect.provide(effect, Layer.mergeAll(CliRuntimeLayer, TestConsole.layer)); // Console output accumulates across CLI runs within a test, and each // Console.log call is one entry — so the latest command's output is the last // entry, even when it spans many lines. const captureStdout = (effect: Effect.Effect) => provideCliTestLayers( Effect.gen(function* () { yield* effect; return ( (yield* TestConsole.logLines).findLast( (line): line is string => typeof line === "string", ) ?? "" ); }), ); const testDescriptor = { environmentId: "pair-test-environment", label: "pair-test", platform: { os: "linux", arch: "x64" }, serverVersion: "0.0.1", capabilities: { repositoryIdentity: true }, }; const withDescriptorServer = (run: (origin: string) => Effect.Effect) => 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" }); response.end(JSON.stringify(testDescriptor)); return; } response.writeHead(404); response.end(); }); server.listen(0, "127.0.0.1", () => resume(Effect.succeed(server))); }), (server) => { const address = server.address(); if (address === null || typeof address === "string") { return Effect.die(new Error("Expected a TCP address")); } return run(`http://127.0.0.1:${String(address.port)}`); }, (server) => Effect.sync(() => server.close()), ); describe("t3 pair", () => { it.effect("mints a token and prints a QR pairing URL for a live server", () => withDescriptorServer((origin) => Effect.gen(function* () { const baseDir = NodeFS.mkdtempSync(NodePath.join(NodeOS.tmpdir(), "t3-pair-test-")); const port = Number(new URL(origin).port); 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, }), }); const output = yield* captureStdout(runCli(["pair", "--base-dir", baseDir])); assert.include(output, `Pairing with pair-test (${origin})`); assert.include(output, `Pairing URL: ${origin}/pair#token=`); assert.isTrue(output.includes("█") || output.includes("▀") || output.includes("▄")); // Loopback origins are not reachable from a phone; the output must say so. assert.include(output, "only reachable from this machine"); const token = /#token=([A-Z2-9]+)/.exec(output)?.[1]; assert.isString(token); // The token must be in the same store the running server reads. const listed = yield* captureStdout( runCli(["auth", "pairing", "list", "--base-dir", baseDir, "--json"]), ); // @effect-diagnostics-next-line preferSchemaOverJson:off - CLI JSON output is decoded as a presentation DTO. const credentials = JSON.parse(listed) as ReadonlyArray<{ readonly label?: string }>; assert.equal(credentials.length, 1); assert.equal(credentials[0]?.label, "t3 pair"); }), ).pipe(Effect.provide(NodeServices.layer)), ); it.effect("pairs through the recorded dev web URL for dev servers", () => withDescriptorServer((origin) => Effect.gen(function* () { const baseDir = NodeFS.mkdtempSync(NodePath.join(NodeOS.tmpdir(), "t3-pair-dev-test-")); const port = Number(new URL(origin).port); const statePath = NodePath.join(baseDir, "dev", "server-runtime.json"); yield* persistServerRuntimeState({ path: statePath, state: yield* makePersistedServerRuntimeState({ config: { host: undefined, devUrl: new URL("http://localhost:5733") }, port, }), }); const output = yield* captureStdout(runCli(["pair", "--base-dir", baseDir])); assert.include(output, "Pairing URL: http://localhost:5733/pair#token="); }), ).pipe(Effect.provide(NodeServices.layer)), ); it.effect("directs to t3 serve or t3 connect when no server is running", () => Effect.gen(function* () { const baseDir = NodeFS.mkdtempSync(NodePath.join(NodeOS.tmpdir(), "t3-pair-none-test-")); 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."); assert.include(rendered, "npx t3 serve"); assert.include(rendered, "npx t3 connect"); }).pipe(Effect.provide(NodeServices.layer)), ); it.effect("ignores runtime state whose recorded pid is no longer alive", () => withDescriptorServer((origin) => Effect.gen(function* () { const baseDir = NodeFS.mkdtempSync(NodePath.join(NodeOS.tmpdir(), "t3-pair-pid-test-")); const statePath = NodePath.join(baseDir, "userdata", "server-runtime.json"); // The origin answers (another server reused the port), but the pid // that wrote this state file is dead — pairing must not mint a token // into the dead server's database. const state = yield* makePersistedServerRuntimeState({ config: { host: "127.0.0.1", devUrl: undefined }, port: Number(new URL(origin).port), }); yield* persistServerRuntimeState({ path: statePath, // pid 2**22 + 1 exceeds any default Linux/macOS pid range. state: { ...state, pid: 4_194_305 }, }); 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."); }), ).pipe(Effect.provide(NodeServices.layer)), ); it.effect("ignores stale runtime state pointing at a dead server", () => Effect.gen(function* () { const baseDir = NodeFS.mkdtempSync(NodePath.join(NodeOS.tmpdir(), "t3-pair-stale-test-")); const statePath = NodePath.join(baseDir, "userdata", "server-runtime.json"); // A port from the dynamic range with nothing listening: the probe fails // fast with ECONNREFUSED and discovery moves on. yield* persistServerRuntimeState({ path: statePath, state: yield* makePersistedServerRuntimeState({ config: { host: "127.0.0.1", devUrl: undefined }, port: 1, }), }); 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."); }).pipe(Effect.provide(NodeServices.layer)), ); });