826 lines · 30.9 KB · TypeScript
RawDownload
import { assert, describe, it } from "@effect/vitest";import * as NodeServices from "@effect/platform-node/NodeServices";import * as NetService from "@t3tools/shared/Net";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 Logger from "effect/Logger";import * as PlatformError from "effect/PlatformError";import * as Result from "effect/Result";import * as Sink from "effect/Sink";import * as Stream from "effect/Stream";import * as TestClock from "effect/testing/TestClock";import * as HttpClient from "effect/unstable/http/HttpClient";import * as HttpClientError from "effect/unstable/http/HttpClientError";import * as HttpClientResponse from "effect/unstable/http/HttpClientResponse";import * as ChildProcess from "effect/unstable/process/ChildProcess";import * as ChildProcessSpawner from "effect/unstable/process/ChildProcessSpawner"; import * as SshAuth from "./auth.ts";import {  SshHttpBridgeInvalidUrlError,  SshHttpBridgeMissingUrlError,  SshHttpBridgeNonLoopbackUrlError,  SshPairingOutputParseError,  SshReadinessProbeError,  SshReadinessProbeTimeoutError,  SshReadinessTimeoutError,  SshTunnelSpawnError,} from "./errors.ts";import * as SshTunnel from "./tunnel.ts"; const TEST_NODE_ENGINE_RANGE = "^22.16 || ^23.11 || >=24.10"; const makeSuccessfulProcess = (stdout: string) => {  const stdoutStream = Stream.make(new TextEncoder().encode(stdout));  return ChildProcessSpawner.makeHandle({    pid: ChildProcessSpawner.ProcessId(123),    stdout: stdoutStream,    stderr: Stream.empty,    all: stdoutStream,    exitCode: Effect.succeed(ChildProcessSpawner.ExitCode(0)),    isRunning: Effect.succeed(false),    kill: () => Effect.void,    stdin: Sink.drain,    getInputFd: () => Sink.drain,    getOutputFd: () => Stream.empty,    unref: Effect.succeed(Effect.void),  });}; const makeFailedProcess = (input: { readonly stdout?: string; readonly stderr?: string }) => {  const stdout = input.stdout ?? "";  const stderr = input.stderr ?? "";  const stdoutStream = stdout === "" ? Stream.empty : Stream.make(new TextEncoder().encode(stdout));  const stderrStream = stderr === "" ? Stream.empty : Stream.make(new TextEncoder().encode(stderr));  return ChildProcessSpawner.makeHandle({    pid: ChildProcessSpawner.ProcessId(123),    stdout: stdoutStream,    stderr: stderrStream,    all: Stream.empty,    exitCode: Effect.succeed(ChildProcessSpawner.ExitCode(1)),    isRunning: Effect.succeed(false),    kill: () => Effect.void,    stdin: Sink.drain,    getInputFd: () => Sink.drain,    getOutputFd: () => Stream.empty,    unref: Effect.succeed(Effect.void),  });}; const makeRunningProcess = (onKill: () => void) => {  let finish: ((exitCode: ChildProcessSpawner.ExitCode) => void) | null = null;  return ChildProcessSpawner.makeHandle({    pid: ChildProcessSpawner.ProcessId(123),    stdout: Stream.empty,    stderr: Stream.empty,    all: Stream.empty,    exitCode: Effect.callback<ChildProcessSpawner.ExitCode>((resume) => {      finish = (exitCode) => resume(Effect.succeed(exitCode));      return Effect.sync(() => {        finish = null;      });    }),    isRunning: Effect.succeed(true),    kill: () =>      Effect.sync(() => {        onKill();        finish?.(ChildProcessSpawner.ExitCode(143));      }),    stdin: Sink.drain,    getInputFd: () => Sink.drain,    getOutputFd: () => Stream.empty,    unref: Effect.succeed(Effect.void),  });}; const testHttpClient = HttpClient.make((request) =>  Effect.succeed(HttpClientResponse.fromWeb(request, new Response("", { status: 200 }))),); const hangingHttpClient = HttpClient.make(() => Effect.never); const testNetService = NetService.NetService.of({  canListenOnHost: () => Effect.succeed(true),  isPortAvailableOnLoopback: () => Effect.succeed(true),  reserveLoopbackPort: () => Effect.succeed(41_773),  findAvailablePort: (preferred) => Effect.succeed(preferred),}); function commandArgs(command: ChildProcess.Command): ReadonlyArray<string> {  return command._tag === "StandardCommand" ? command.args : [];} function flattenedLogText(value: unknown): string {  if (typeof value === "string") return value;  if (typeof value !== "object" || value === null) return String(value);  return Object.entries(value)    .flatMap(([key, nested]) => [key, flattenedLogText(nested)])    .join("\n");} describe("ssh tunnel scripts", () => {  it("builds the remote t3 runner with npx and npm fallbacks", () => {    const script = SshTunnel.buildRemoteT3RunnerScript({ nodeEngineRange: TEST_NODE_ENGINE_RANGE });     assert.include(script, "T3_NODE_SCRIPT_PATH=''");    assert.include(script, 'exec t3 "$@"');    assert.include(script, "exec npx --yes 't3@latest' \"$@\"");    assert.include(script, "exec npm exec --yes 't3@latest' -- \"$@\"");    assert.include(script, "could not install 't3@latest'");    assert.include(script, 'prepend_path_if_dir "$HOME/.local/bin"');    assert.include(script, `T3_NODE_ENGINE_RANGE='${TEST_NODE_ENGINE_RANGE}'`);    assert.include(script, "remote_node_satisfies_engine()");    assert.include(script, "function satisfiesSemverRange");    assert.include(script, "satisfiesSemverRange(rawVersion, range)");    assert.include(script, 'prepend_path_if_dir "$VOLTA_HOME/bin"');    assert.include(script, 'prepend_path_if_dir "$HOME/.asdf/shims"');    assert.include(script, 'prepend_path_if_dir "$HOME/.local/share/mise/shims"');    assert.include(script, 'eval "$(fnm env --shell bash)"');    assert.include(script, "fnm use --silent-if-unchanged");    assert.include(script, "fnm use default");    assert.include(script, 'prepend_path_if_dir "$HOME/.nodenv/shims"');    assert.include(script, 'NVM_DIR="$HOME/.nvm"');    assert.include(script, "nvm use --silent default");    assert.include(script, 'for T3_NODE_BIN in "$NVM_DIR"/versions/node/*/bin');    assert.notInclude(script, "ensure $NVM_DIR/nvm.sh is available");  });   it("does not hard-code a remote node engine range", () => {    const script = SshTunnel.buildRemoteT3RunnerScript();     assert.include(script, "T3_NODE_ENGINE_RANGE=''");    assert.notInclude(script, TEST_NODE_ENGINE_RANGE);  });   it("shell-quotes package specs in the remote t3 runner", () => {    const script = SshTunnel.buildRemoteT3RunnerScript({      packageSpec: "t3@nightly; touch /tmp/t3-owned",    });     assert.include(script, "exec npx --yes 't3@nightly; touch /tmp/t3-owned' \"$@\"");    assert.include(script, "exec npm exec --yes 't3@nightly; touch /tmp/t3-owned' -- \"$@\"");    assert.notInclude(script, "exec npx --yes t3@nightly; touch /tmp/t3-owned");  });   it("builds the remote t3 runner with a node script override", () => {    const script = SshTunnel.buildRemoteT3RunnerScript({      nodeScriptPath: "/Users/julius/Development/Work/codething-mvp/apps/server/dist/bin.mjs",    });     assert.include(      script,      "T3_NODE_SCRIPT_PATH='/Users/julius/Development/Work/codething-mvp/apps/server/dist/bin.mjs'",    );    assert.include(script, 'exec node "$T3_NODE_SCRIPT_PATH" "$@"');  });   it("uses the remote t3 runner for launch and pairing scripts", () => {    const target = {      alias: "devbox",      hostname: "devbox.example.com",      username: "julius",      port: 2222,    } as const;     assert.include(      SshTunnel.buildRemoteLaunchScript({ nodeEngineRange: TEST_NODE_ENGINE_RANGE }),      '[ -n "$REMOTE_PID" ] && [ -n "$REMOTE_PORT" ] && kill -0 "$REMOTE_PID" 2>/dev/null',    );    assert.include(SshTunnel.buildRemoteLaunchScript(), "RUNNER_CHANGED=1");    assert.include(SshTunnel.buildRemoteLaunchScript(), "ensure_remote_node_path()");    assert.include(SshTunnel.buildRemoteLaunchScript(), "if ! ensure_remote_node_path; then");    assert.include(      SshTunnel.buildRemoteLaunchScript({ nodeEngineRange: TEST_NODE_ENGINE_RANGE }),      `T3_NODE_ENGINE_RANGE='${TEST_NODE_ENGINE_RANGE}'`,    );    assert.include(      SshTunnel.buildRemoteLaunchScript({ nodeEngineRange: TEST_NODE_ENGINE_RANGE }),      "does not satisfy required range ",    );    assert.include(SshTunnel.buildRemoteLaunchScript(), 'kill "$REMOTE_PID" 2>/dev/null || true');    assert.include(SshTunnel.buildRemoteLaunchScript(), "wait_ready");    assert.include(SshTunnel.buildRemoteLaunchScript(), '"$RUNNER_FILE" serve --host 127.0.0.1');    assert.include(SshTunnel.buildRemoteLaunchScript(), '--base-dir "$DEFAULT_SERVER_HOME"');    assert.notInclude(SshTunnel.buildRemoteLaunchScript(), "server-home");    assert.include(SshTunnel.buildRemoteLaunchScript(), "Remote T3 server did not become ready");    assert.include(SshTunnel.buildRemoteLaunchScript({ packageSpec: "t3@nightly" }), "t3@nightly");    assert.include(      SshTunnel.buildRemotePairingScript(target),      '"$RUNNER_FILE" auth pairing create --base-dir "$PAIRING_BASE_DIR" --json',    );    assert.include(      SshTunnel.buildRemotePairingScript(target),      'PAIRING_BASE_DIR="$DEFAULT_SERVER_HOME"',    );    assert.notInclude(SshTunnel.buildRemotePairingScript(target), "server-home");    assert.include(      SshTunnel.buildRemotePairingScript(target, { packageSpec: "t3@nightly" }),      "t3@nightly",    );    assert.include(      SshTunnel.buildRemoteStopScript(target),      'if [ "$REMOTE_MANAGED" != "external" ] && [ -n "$REMOTE_PID" ]',    );    assert.include(      SshTunnel.buildRemoteStopScript(target),      'kill "$REMOTE_PID" 2>/dev/null || true',    );    assert.include(      SshTunnel.buildRemoteStopScript(target),      'rm -f "$PID_FILE" "$PORT_FILE" "$MANAGED_FILE"',    );    assert.include(      SshTunnel.buildRemoteLaunchScript(),      'DEFAULT_RUNTIME_FILE="$DEFAULT_SERVER_HOME/userdata/server-runtime.json"',    );    assert.include(SshTunnel.buildRemoteLaunchScript(), "resolve_default_runtime_port()");    assert.include(      SshTunnel.buildRemoteLaunchScript(),      'DEFAULT_RUNTIME_INFO="$(resolve_default_runtime_port',    );    assert.include(      SshTunnel.buildRemoteLaunchScript(),      "if (!Number.isInteger(pid) || pid <= 0 || !Number.isInteger(port))",    );    assert.include(      SshTunnel.buildRemoteLaunchScript(),      'PID_TO_STOP="${REMOTE_PID:-$DEFAULT_RUNTIME_PID}"',    );    assert.include(SshTunnel.buildRemoteLaunchScript(), 'REMOTE_PORT="$DEFAULT_REMOTE_PORT"');    assert.include(SshTunnel.buildRemoteLaunchScript(), 'rm -f "$PID_FILE"');    assert.include(SshTunnel.buildRemoteLaunchScript(), "printf 'external\\n' >\"$MANAGED_FILE\"");    assert.include(SshTunnel.buildRemoteLaunchScript(), 'if [ -z "$REMOTE_PORT" ]; then');    assert.isBelow(      SshTunnel.buildRemoteLaunchScript().indexOf('if [ "$REMOTE_MANAGED" = "managed" ]'),      SshTunnel.buildRemoteLaunchScript().indexOf("printf 'external\\n' >\"$MANAGED_FILE\""),    );    assert.isBelow(      SshTunnel.buildRemoteLaunchScript().indexOf(        'DEFAULT_RUNTIME_INFO="$(resolve_default_runtime_port',      ),      SshTunnel.buildRemoteLaunchScript().indexOf('elif [ -n "$REMOTE_PID" ]'),    );  });   it.effect("accepts launch JSON after remote shell startup noise", () => {    const target = {      alias: "devbox",      hostname: "devbox.example.com",      username: "julius",      port: 2222,    } as const;    const spawner = ChildProcessSpawner.make(() =>      Effect.succeed(makeSuccessfulProcess('loaded nvm default\n{"remotePort":3774}\n')),    );    const spawnerLayer = Layer.succeed(ChildProcessSpawner.ChildProcessSpawner, spawner);    const processLayer = Layer.merge(NodeServices.layer, spawnerLayer);     return Effect.gen(function* () {      const result = yield* SshTunnel.launchOrReuseRemoteServer(target);      assert.equal(result.remotePort, 3774);    }).pipe(Effect.provide(processLayer));  });   it("allows the remote port picker to run without a state file path", () => {    assert.include(SshTunnel.REMOTE_PICK_PORT_SCRIPT, 'const filePath = process.argv[2] ?? "";');  });   it.effect("bounds each HTTP readiness probe so retries cannot hang on one request", () => {    const baseUrl =      "http://ssh-user:ssh-password@127.0.0.1:41773/private/base?token=base-secret#base-fragment";    const path = "/ready/private?credential=request-secret#request-fragment";    const requestUrl = new URL(path, baseUrl).toString();    const capturedLogs: Array<ReadonlyArray<unknown>> = [];    const logger = Logger.make(({ message }) => {      capturedLogs.push(Array.isArray(message) ? message : [message]);    });     return Effect.gen(function* () {      const fiber = yield* Effect.forkChild(        Effect.result(          SshTunnel.waitForHttpReady({            baseUrl,            path,            timeoutMs: 1_000,            intervalMs: 100,            probeTimeoutMs: 250,          }),        ),      );      yield* Effect.yieldNow;      yield* TestClock.adjust(Duration.millis(1_000));       const result = yield* Fiber.join(fiber);       assert.isTrue(Result.isFailure(result));      if (Result.isFailure(result)) {        assert.instanceOf(result.failure, SshReadinessTimeoutError);        const timeoutError = result.failure as SshReadinessTimeoutError;        assert.deepEqual(timeoutError.base, {          protocol: "http:",          hostname: "127.0.0.1",          port: "41773",          urlLength: baseUrl.length,          pathnameLength: new TextEncoder().encode(new URL(baseUrl).pathname).length,          hasQuery: true,          hasFragment: true,        });        assert.deepEqual(timeoutError.request, {          protocol: "http:",          hostname: "127.0.0.1",          port: "41773",          urlLength: requestUrl.length,          pathnameLength: new TextEncoder().encode(new URL(requestUrl).pathname).length,          hasQuery: true,          hasFragment: true,        });        assert.isFalse("baseUrl" in timeoutError);        assert.isFalse("requestUrl" in timeoutError);        assert.isFalse("baseTarget" in timeoutError);        assert.isFalse("requestTarget" in timeoutError);        assert.notInclude(timeoutError.message, "secret");        assert.notInclude(timeoutError.message, "/ready");        assert.notInclude(timeoutError.message, "request-secret");        assert.equal(          timeoutError.message,          "Timed out waiting 1000ms for backend readiness at http://127.0.0.1:41773.",        );        assert.isAbove(timeoutError.attempts, 0);        assert.instanceOf(timeoutError.cause, SshReadinessProbeTimeoutError);        const probeTimeout = timeoutError.cause as SshReadinessProbeTimeoutError;        assert.equal(probeTimeout.attempt, timeoutError.attempts);        assert.isFalse("cause" in probeTimeout);         const logText = flattenedLogText(capturedLogs);        assert.include(logText, "127.0.0.1");        assert.notInclude(logText, "ssh-user");        assert.notInclude(logText, "ssh-password");        assert.notInclude(logText, "/private/base");        assert.notInclude(logText, "base-secret");        assert.notInclude(logText, "/ready/private");        assert.notInclude(logText, "request-secret");        assert.notInclude(logText, "base-fragment");        assert.notInclude(logText, "request-fragment");      }    }).pipe(      Effect.provide(        Layer.mergeAll(          TestClock.layer(),          Layer.succeed(HttpClient.HttpClient, hangingHttpClient),          Logger.layer([logger], { mergeWithExisting: false }),        ),      ),    );  });   it.effect("preserves the exact HTTP transport cause without logging request secrets", () => {    const baseUrl =      "http://transport-user:transport-password@127.0.0.1:41773/private?token=transport-secret#fragment";    const transportCause = new Error("socket closed");    const clientErrors: Array<HttpClientError.HttpClientError> = [];    const failingHttpClient = HttpClient.make((request) => {      const error = new HttpClientError.HttpClientError({        reason: new HttpClientError.TransportError({ request, cause: transportCause }),      });      clientErrors.push(error);      return Effect.fail(error);    });    const capturedLogs: Array<ReadonlyArray<unknown>> = [];    const logger = Logger.make(({ message }) => {      capturedLogs.push(Array.isArray(message) ? message : [message]);    });     return Effect.gen(function* () {      const fiber = yield* Effect.forkChild(        Effect.result(          SshTunnel.waitForHttpReady({            baseUrl,            timeoutMs: 300,            intervalMs: 100,            probeTimeoutMs: 250,          }),        ),      );      yield* Effect.yieldNow;      yield* TestClock.adjust(Duration.millis(300));       const result = yield* Fiber.join(fiber);      assert.isTrue(Result.isFailure(result));      if (Result.isFailure(result)) {        assert.instanceOf(result.failure, SshReadinessTimeoutError);        const timeoutError = result.failure as SshReadinessTimeoutError;        assert.instanceOf(timeoutError.cause, SshReadinessProbeError);        const probeError = timeoutError.cause as SshReadinessProbeError;        assert.strictEqual(probeError.cause, clientErrors.at(-1));        assert.strictEqual(          (probeError.cause as HttpClientError.HttpClientError).reason.cause,          transportCause,        );         const logText = flattenedLogText(capturedLogs);        assert.include(logText, "HttpClientError");        assert.include(logText, "TransportError");        assert.notInclude(logText, "transport-user");        assert.notInclude(logText, "transport-password");        assert.notInclude(logText, "/private");        assert.notInclude(logText, "transport-secret");      }    }).pipe(      Effect.provide(        Layer.mergeAll(          TestClock.layer(),          Layer.succeed(HttpClient.HttpClient, failingHttpClient),          Logger.layer([logger], { mergeWithExisting: false }),        ),      ),    );  });   it.effect("preserves forwarded HTTP URL error messages", () =>    Effect.gen(function* () {      const missing = yield* Effect.result(SshTunnel.resolveLoopbackSshHttpBaseUrl(""));      assert.isTrue(Result.isFailure(missing));      if (Result.isFailure(missing)) {        assert.instanceOf(missing.failure, SshHttpBridgeMissingUrlError);        assert.equal(missing.failure.message, "Invalid SSH forwarded http base URL.");      }       const invalid = yield* Effect.result(SshTunnel.resolveLoopbackSshHttpBaseUrl("not-a-url"));      assert.isTrue(Result.isFailure(invalid));      if (Result.isFailure(invalid)) {        assert.instanceOf(invalid.failure, SshHttpBridgeInvalidUrlError);        assert.equal(invalid.failure.message, "Invalid SSH forwarded http base URL.");      }       const nonLoopback = yield* Effect.result(        SshTunnel.resolveLoopbackSshHttpBaseUrl("https://example.com"),      );      assert.isTrue(Result.isFailure(nonLoopback));      if (Result.isFailure(nonLoopback)) {        assert.instanceOf(nonLoopback.failure, SshHttpBridgeNonLoopbackUrlError);        assert.equal(          nonLoopback.failure.message,          "SSH desktop bridge only supports loopback forwarded URLs.",        );      }    }),  );   it("describes readiness causes without copying arbitrary messages", () => {    assert.deepEqual(      SshTunnel.describeReadinessCause({        _tag: "HttpClientError",        message: "Backend readiness probe failed.",        reason: "authentication failed",        cause: "upstream closed",      }),      {        _tag: "HttpClientError",        reason: { type: "string" },        cause: { type: "string" },      },    );     const cyclicCause: { readonly _tag: string; cause?: unknown } = { _tag: "CyclicCause" };    cyclicCause.cause = cyclicCause;    assert.include(flattenedLogText(SshTunnel.describeReadinessCause(cyclicCause)), "truncated");  });   it("preserves structured readiness attributes in diagnostic output", () => {    assert.deepEqual(      SshTunnel.describeReadinessCause(        new SshReadinessProbeTimeoutError({          request: {            protocol: "http:",            hostname: "127.0.0.1",            port: "41773",            urlLength: 28,            pathnameLength: 6,

Lines 1–500 of 826.