752 lines · 27.9 KB · TypeScript
RawDownload
import {  DesktopBackendBootstrap,  type DesktopBackendBootstrap as DesktopBackendBootstrapValue,} from "@t3tools/contracts";import { assert, describe, it } from "@effect/vitest";import * as Cause from "effect/Cause";import * as Deferred from "effect/Deferred";import * as Duration from "effect/Duration";import * as Effect from "effect/Effect";import * as FileSystem from "effect/FileSystem";import * as Fiber from "effect/Fiber";import * as Layer from "effect/Layer";import * as Option from "effect/Option";import * as PlatformError from "effect/PlatformError";import * as Queue from "effect/Queue";import * as Ref from "effect/Ref";import * as Schema from "effect/Schema";import * as Sink from "effect/Sink";import * as Scope from "effect/Scope";import * as Stream from "effect/Stream";import * as TestClock from "effect/testing/TestClock";import { HttpClient, HttpClientRequest, HttpClientResponse } from "effect/unstable/http";import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process"; import * as DesktopBackendManager from "./DesktopBackendManager.ts";import * as DesktopBackendConfiguration from "./DesktopBackendConfiguration.ts";import * as DesktopObservability from "../app/DesktopObservability.ts";import * as DesktopState from "../app/DesktopState.ts";import * as DesktopWindow from "../window/DesktopWindow.ts"; const decodeDesktopBackendBootstrap = Schema.decodeEffect(  Schema.fromJsonString(DesktopBackendBootstrap),);const isBackendProcessError = Schema.is(DesktopBackendManager.BackendProcessError); const baseConfig: DesktopBackendManager.DesktopBackendStartConfig = {  executablePath: "/electron",  entryPath: "/server/bin.mjs",  cwd: "/server",  env: { ELECTRON_RUN_AS_NODE: "1" },  bootstrap: {    mode: "desktop",    noBrowser: true,    port: 3773,    t3Home: "/tmp/t3",    host: "127.0.0.1",    desktopBootstrapToken: "token",    tailscaleServeEnabled: false,    tailscaleServePort: 443,  },  httpBaseUrl: new URL("http://127.0.0.1:3773"),  captureOutput: true,}; const configWithObservability: DesktopBackendBootstrapValue = {  ...baseConfig.bootstrap,  tailscaleServeEnabled: true,  otlpTracesUrl: "http://127.0.0.1:4318/v1/traces",}; function makeProcess(options?: {  readonly stdout?: Stream.Stream<Uint8Array, PlatformError.PlatformError>;  readonly stderr?: Stream.Stream<Uint8Array, PlatformError.PlatformError>;  readonly exitCode?: Effect.Effect<ChildProcessSpawner.ExitCode, PlatformError.PlatformError>;  readonly kill?: ChildProcessSpawner.ChildProcessHandle["kill"];}): ChildProcessSpawner.ChildProcessHandle {  return ChildProcessSpawner.makeHandle({    pid: ChildProcessSpawner.ProcessId(123),    stdout: options?.stdout ?? Stream.empty,    stderr: options?.stderr ?? Stream.empty,    all: Stream.merge(options?.stdout ?? Stream.empty, options?.stderr ?? Stream.empty),    exitCode: options?.exitCode ?? Effect.succeed(ChildProcessSpawner.ExitCode(0)),    isRunning: Effect.succeed(false),    kill: options?.kill ?? (() => Effect.void),    stdin: Sink.drain,    getInputFd: () => Sink.drain,    getOutputFd: () => Stream.empty,    unref: Effect.succeed(Effect.void),  });} function responseForRequest(  request: HttpClientRequest.HttpClientRequest,  status: number,): HttpClientResponse.HttpClientResponse {  return HttpClientResponse.fromWeb(request, new Response(null, { status }));} function httpClientLayer(  handler: (    request: HttpClientRequest.HttpClientRequest,  ) => Effect.Effect<HttpClientResponse.HttpClientResponse>,) {  return Layer.succeed(    HttpClient.HttpClient,    HttpClient.make((request) => handler(request)),  );} const healthyHttpClientLayer = httpClientLayer((request) =>  Effect.succeed(responseForRequest(request, 200)),); function decodeBootstrap(raw: string) {  return decodeDesktopBackendBootstrap(raw);} function makeManagerLayer(input: {  readonly spawnerLayer: Layer.Layer<ChildProcessSpawner.ChildProcessSpawner>;  readonly httpClientLayer?: Layer.Layer<HttpClient.HttpClient>;  readonly backendOutputLog?: Partial<DesktopObservability.DesktopBackendOutputLog["Service"]>;  readonly desktopState?: DesktopState.DesktopState["Service"];  readonly desktopWindow?: Partial<DesktopWindow.DesktopWindow["Service"]>;  readonly config?: DesktopBackendManager.DesktopBackendStartConfig;}) {  return DesktopBackendManager.layer.pipe(    Layer.provide(      Layer.mergeAll(        FileSystem.layerNoop({          exists: () => Effect.succeed(true),        }),        Layer.succeed(DesktopBackendConfiguration.DesktopBackendConfiguration, {          resolve: Effect.succeed(input.config ?? baseConfig),        }),        input.spawnerLayer,        input.httpClientLayer ?? healthyHttpClientLayer,        input.desktopState          ? Layer.succeed(DesktopState.DesktopState, input.desktopState)          : DesktopState.layer,        Layer.succeed(DesktopObservability.DesktopBackendOutputLog, {          writeSessionBoundary: () => Effect.void,          writeOutputChunk: () => Effect.void,          ...input.backendOutputLog,        } satisfies DesktopObservability.DesktopBackendOutputLog["Service"]),        Layer.succeed(DesktopWindow.DesktopWindow, {          createMain: Effect.die("unexpected createMain"),          ensureMain: Effect.die("unexpected ensureMain"),          revealOrCreateMain: Effect.die("unexpected revealOrCreateMain"),          activate: Effect.void,          createMainIfBackendReady: Effect.void,          handleBackendReady: Effect.void,          dispatchMenuAction: () => Effect.void,          syncAppearance: Effect.void,          ...input.desktopWindow,        } satisfies DesktopWindow.DesktopWindow["Service"]),      ),    ),  );} describe("DesktopBackendManager", () => {  it("preserves the complete restart cause and schedule context", () => {    const cause = Cause.combine(      Cause.fail(new Error("start failed")),      Cause.die(new Error("restart defect")),    );    const error = new DesktopBackendManager.DesktopBackendRestartError({      reason: "backend exited with code 1",      delayMs: 500,      cause,    });     assert.strictEqual(error.cause, cause);    assert.equal(error.reason, "backend exited with code 1");    assert.equal(error.delayMs, 500);    assert.equal(error.message, "Desktop backend restart failed after a scheduled 500ms delay.");  });   it.effect("spawns the backend with fd3 bootstrap JSON and reports HTTP readiness", () =>    Effect.gen(function* () {      let spawnedCommand: ChildProcess.Command | undefined;      let bootstrapJson = "";      let readyCount = 0;      const ready = yield* Deferred.make<void>();      const exited = yield* Queue.unbounded<void>();       const spawnerLayer = Layer.succeed(        ChildProcessSpawner.ChildProcessSpawner,        ChildProcessSpawner.make((command) =>          Effect.gen(function* () {            spawnedCommand = command;            if (command._tag === "StandardCommand") {              const fd3 = command.options.additionalFds?.fd3;              if (fd3?.type === "input" && fd3.stream) {                bootstrapJson = yield* fd3.stream.pipe(Stream.decodeText(), Stream.mkString);              }            }             return makeProcess({              exitCode: Deferred.await(ready).pipe(Effect.as(ChildProcessSpawner.ExitCode(0))),            });          }),        ),      );       const managerLayer = makeManagerLayer({        config: {          ...baseConfig,          bootstrap: configWithObservability,        },        spawnerLayer,        desktopWindow: {          handleBackendReady: Effect.sync(() => {            readyCount += 1;          }).pipe(Effect.andThen(Deferred.succeed(ready, void 0))),        },        backendOutputLog: {          writeSessionBoundary: ({ phase }) =>            phase === "END" ? Queue.offer(exited, void 0).pipe(Effect.asVoid) : Effect.void,        },      });       yield* Effect.gen(function* () {        const manager = yield* DesktopBackendManager.DesktopBackendManager;        yield* manager.start;        yield* Queue.take(exited);         assert.equal(readyCount, 1);        assert.isDefined(spawnedCommand);        if (spawnedCommand._tag !== "StandardCommand") {          throw new Error("Expected backend to spawn a standard command.");        }         assert.equal(spawnedCommand.command, "/electron");        assert.deepEqual(spawnedCommand.args, ["/server/bin.mjs", "--bootstrap-fd", "3"]);        assert.equal(spawnedCommand.options.cwd, "/server");        assert.equal(spawnedCommand.options.extendEnv, true);        assert.equal(spawnedCommand.options.stdout, "pipe");        assert.equal(spawnedCommand.options.stderr, "pipe");        assert.equal(spawnedCommand.options.killSignal, "SIGTERM");        assert.isDefined(spawnedCommand.options.forceKillAfter);        assert.equal(          Duration.toMillis(Duration.fromInputUnsafe(spawnedCommand.options.forceKillAfter)),          2_000,        );         assert.deepEqual(yield* decodeBootstrap(bootstrapJson), configWithObservability);      }).pipe(Effect.provide(managerLayer));    }),  );   it.effect("preserves the readiness timeout cause and process context", () =>    Effect.gen(function* () {      const requested = yield* Deferred.make<HttpClientRequest.HttpClientRequest>();      const layer = Layer.merge(        TestClock.layer(),        httpClientLayer((request) =>          Deferred.succeed(requested, request).pipe(Effect.andThen(Effect.never)),        ),      );       yield* Effect.gen(function* () {        const readiness = yield* DesktopBackendManager.waitForHttpReady({          executablePath: baseConfig.executablePath,          entryPath: baseConfig.entryPath,          cwd: baseConfig.cwd,          httpBaseUrl: baseConfig.httpBaseUrl,          timeout: Duration.millis(50),        }).pipe(Effect.flip, Effect.forkChild);         const request = yield* Deferred.await(requested);        assert.equal(request.url, "http://127.0.0.1:3773/.well-known/t3/environment");         yield* TestClock.adjust(Duration.millis(50));        const error = yield* Fiber.join(readiness);         assert.instanceOf(error, DesktopBackendManager.BackendReadinessTimeoutError);        assert.equal(error.executablePath, "/electron");        assert.equal(error.entryPath, "/server/bin.mjs");        assert.equal(error.cwd, "/server");        assert.equal(error.httpBaseUrl.href, "http://127.0.0.1:3773/");        assert.equal(error.readinessUrl.href, "http://127.0.0.1:3773/.well-known/t3/environment");        assert.equal(error.timeoutMs, 50);        assert.isTrue(Cause.isTimeoutError(error.cause));        assert.equal(          error.message,          "Timed out after 50ms waiting for desktop backend readiness at http://127.0.0.1:3773/.well-known/t3/environment.",        );      }).pipe(Effect.provide(layer));    }),  );   it.effect("reports bootstrap encoding failures with stable process context", () =>    Effect.gen(function* () {      const spawnerLayer = Layer.succeed(        ChildProcessSpawner.ChildProcessSpawner,        ChildProcessSpawner.make(() => Effect.die("unexpected backend spawn")),      );      const error = yield* DesktopBackendManager.runBackendProcess({        ...baseConfig,        bootstrap: {          ...baseConfig.bootstrap,          port: 0,        },      }).pipe(        Effect.flip,        Effect.scoped,        Effect.provide(Layer.merge(spawnerLayer, healthyHttpClientLayer)),      );       if (error._tag !== "BackendProcessBootstrapEncodeError") {        return assert.fail(`Expected bootstrap encode error, received ${error._tag}`);      }      assert.equal(error.executablePath, "/electron");      assert.equal(error.entryPath, "/server/bin.mjs");      assert.equal(error.cwd, "/server");      assert.equal(error.httpBaseUrl.href, "http://127.0.0.1:3773/");      assert.isDefined(error.cause);      assert.equal(        error.message,        "Failed to encode the desktop backend bootstrap payload for /server/bin.mjs.",      );      assert.isTrue(isBackendProcessError(error));    }),  );   it.effect("preserves spawn failures without deriving their message from the cause", () =>    Effect.gen(function* () {      const spawnCause = PlatformError.systemError({        _tag: "PermissionDenied",        module: "ChildProcessSpawner",        method: "spawn",        pathOrDescriptor: baseConfig.executablePath,        description: "low-level detail that must not become the public message",      });      const spawnerLayer = Layer.succeed(        ChildProcessSpawner.ChildProcessSpawner,        ChildProcessSpawner.make(() => Effect.fail(spawnCause)),      );      const error = yield* DesktopBackendManager.runBackendProcess(baseConfig).pipe(        Effect.flip,        Effect.scoped,        Effect.provide(Layer.merge(spawnerLayer, healthyHttpClientLayer)),      );       if (error._tag !== "BackendProcessSpawnError") {        return assert.fail(`Expected backend spawn error, received ${error._tag}`);      }      assert.equal(error.executablePath, "/electron");      assert.equal(error.entryPath, "/server/bin.mjs");      assert.equal(error.cwd, "/server");      assert.equal(error.httpBaseUrl.href, "http://127.0.0.1:3773/");      assert.strictEqual(error.cause, spawnCause);      assert.equal(        error.message,        "Failed to spawn desktop backend entry /server/bin.mjs with /electron.",      );      assert.notInclude(error.message, spawnCause.message);      assert.isTrue(isBackendProcessError(error));    }),  );   it.effect("preserves exit-status failures without copying their detail into the message", () =>    Effect.gen(function* () {      const exitCause = PlatformError.systemError({        _tag: "PermissionDenied",        module: "ChildProcess",        method: "exitCode",        description: "exit-status-secret-sentinel",      });      const spawnerLayer = Layer.succeed(        ChildProcessSpawner.ChildProcessSpawner,        ChildProcessSpawner.make(() =>          Effect.succeed(            makeProcess({              exitCode: Effect.fail(exitCause),            }),          ),        ),      );      const error = yield* DesktopBackendManager.runBackendProcess(baseConfig).pipe(        Effect.flip,        Effect.scoped,        Effect.provide(Layer.merge(spawnerLayer, healthyHttpClientLayer)),      );       if (error._tag !== "BackendProcessExitStatusError") {        return assert.fail(`Expected backend exit-status error, received ${error._tag}`);      }      assert.equal(error.pid, 123);      assert.equal(error.executablePath, "/electron");      assert.equal(error.entryPath, "/server/bin.mjs");      assert.equal(error.cwd, "/server");      assert.equal(error.httpBaseUrl.href, "http://127.0.0.1:3773/");      assert.strictEqual(error.cause, exitCause);      assert.equal(error.message, "Failed to read the exit status of desktop backend process 123.");      assert.notInclude(error.message, "exit-status-secret-sentinel");      assert.isTrue(isBackendProcessError(error));    }),  );   it.effect("reports output stream failures with process and stream context", () =>    Effect.gen(function* () {      const outputCause = PlatformError.systemError({        _tag: "BadResource",        module: "ChildProcess",        method: "stdout",        description: "output-stream-secret-sentinel",      });      const reported = yield* Deferred.make<DesktopBackendManager.BackendProcessOutputError>();      const spawnerLayer = Layer.succeed(        ChildProcessSpawner.ChildProcessSpawner,        ChildProcessSpawner.make(() =>          Effect.succeed(            makeProcess({              stdout: Stream.fail(outputCause),              exitCode: Deferred.await(reported).pipe(Effect.as(ChildProcessSpawner.ExitCode(0))),            }),          ),        ),      );       const exit = yield* DesktopBackendManager.runBackendProcess({        ...baseConfig,        onOutputFailure: (error) => Deferred.succeed(reported, error).pipe(Effect.asVoid),      }).pipe(Effect.scoped, Effect.provide(Layer.merge(spawnerLayer, healthyHttpClientLayer)));      const error = yield* Deferred.await(reported);       assert.equal(exit.code.pipe(Option.getOrUndefined), 0);      if (error._tag !== "BackendProcessOutputReadError") {        return assert.fail(`Expected output read error, received ${error._tag}`);      }      assert.equal(error.executablePath, "/electron");      assert.equal(error.entryPath, "/server/bin.mjs");      assert.equal(error.cwd, "/server");      assert.equal(error.httpBaseUrl.href, "http://127.0.0.1:3773/");      assert.equal(error.pid, 123);      assert.equal(error.streamName, "stdout");      assert.strictEqual(error.cause, outputCause);      assert.equal(error.message, "Failed to read stdout from desktop backend process 123.");      assert.notInclude(error.message, "output-stream-secret-sentinel");    }),  );   it.effect("reports output handler failures separately from stream read failures", () =>    Effect.gen(function* () {      const chunk = new TextEncoder().encode("backend output");      const outputCause = new Error("output-handler-secret-sentinel");      const reported = yield* Deferred.make<DesktopBackendManager.BackendProcessOutputError>();      const spawnerLayer = Layer.succeed(        ChildProcessSpawner.ChildProcessSpawner,        ChildProcessSpawner.make(() =>          Effect.succeed(            makeProcess({              stdout: Stream.make(chunk),              exitCode: Deferred.await(reported).pipe(Effect.as(ChildProcessSpawner.ExitCode(0))),            }),          ),        ),      );       const exit = yield* DesktopBackendManager.runBackendProcess({        ...baseConfig,        onOutput: () => Effect.fail(outputCause),        onOutputFailure: (error) => Deferred.succeed(reported, error).pipe(Effect.asVoid),      }).pipe(Effect.scoped, Effect.provide(Layer.merge(spawnerLayer, healthyHttpClientLayer)));      const error = yield* Deferred.await(reported);       assert.equal(exit.code.pipe(Option.getOrUndefined), 0);      if (error._tag !== "BackendProcessOutputHandlingError") {        return assert.fail(`Expected output handling error, received ${error._tag}`);      }      assert.equal(error.executablePath, "/electron");      assert.equal(error.entryPath, "/server/bin.mjs");      assert.equal(error.cwd, "/server");      assert.equal(error.httpBaseUrl.href, "http://127.0.0.1:3773/");      assert.equal(error.pid, 123);      assert.equal(error.streamName, "stdout");      assert.equal(error.chunkByteLength, chunk.byteLength);      assert.strictEqual(error.cause, outputCause);      assert.equal(        error.message,        `Failed to handle ${chunk.byteLength} bytes from stdout of desktop backend process 123.`,      );      assert.notInclude(error.message, "output-handler-secret-sentinel");    }),  );   it.effect("retries HTTP readiness before reporting the backend ready", () =>    Effect.gen(function* () {      const requestUrls: Array<string> = [];      const statuses = [503, 200];      let readyCount = 0;      const firstRequest = yield* Deferred.make<void>();      const ready = yield* Deferred.make<void>();      const exited = yield* Queue.unbounded<void>();       const spawnerLayer = Layer.succeed(        ChildProcessSpawner.ChildProcessSpawner,        ChildProcessSpawner.make(() =>          Effect.succeed(            makeProcess({              exitCode: Deferred.await(ready).pipe(Effect.as(ChildProcessSpawner.ExitCode(0))),            }),          ),        ),      );       const managerLayer = makeManagerLayer({        spawnerLayer,

Lines 1–500 of 752.