247 lines · 8.8 KB · TypeScript
RawDownload
import * as NodeServices from "@effect/platform-node/NodeServices";import { expect, it } from "@effect/vitest";import type { DesktopUpdateState, DesktopUpdateStatusReport } from "@t3tools/contracts";import * as Deferred from "effect/Deferred";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 Stream from "effect/Stream"; import * as ServerConfig from "../config.ts";import * as DesktopTelemetryReceiver from "../resourceTelemetry/DesktopTelemetryReceiver.ts";import * as DesktopAppUpdate from "./DesktopAppUpdate.ts"; function makeState(overrides: Partial<DesktopUpdateState> = {}): DesktopUpdateState {  return {    enabled: true,    status: "idle",    channel: "latest",    currentVersion: "1.2.3",    hostArch: "arm64",    appArch: "arm64",    runningUnderArm64Translation: false,    availableVersion: null,    downloadedVersion: null,    releaseNotes: [],    downloadPercent: null,    checkedAt: null,    message: null,    errorContext: null,    canRetry: false,    omittedReleaseCount: 0,    ...overrides,  };} function report(  requestId: string,  state: DesktopUpdateState,  terminal?: {    readonly outcome: DesktopUpdateStatusReport["outcome"];    readonly reason?: string;  },): DesktopUpdateStatusReport {  return {    version: 1,    type: "desktopUpdateStatus",    requestId,    ...(terminal?.outcome === undefined ? {} : { outcome: terminal.outcome }),    ...(terminal?.reason === undefined ? {} : { reason: terminal.reason }),    state,  };} interface HarnessOptions {  readonly mode?: "web" | "desktop";  readonly controlFd?: number | undefined;  /** Reports emitted for the run, given the requestId the service generated.      The stream ends after the last one unless `keepOpen` is set. */  readonly reports?: (requestId: string) => readonly DesktopUpdateStatusReport[];  readonly keepOpen?: boolean;} const makeHarness = Effect.fn("test.make_desktop_app_update_harness")(function* (  options: HarnessOptions = {},) {  const fs = yield* FileSystem.FileSystem;  const baseDir = yield* fs.makeTempDirectoryScoped({ prefix: "t3-desktop-app-update-test-" });  const requestIdDeferred = yield* Deferred.make<string>();  const baseConfig = yield* ServerConfig.ServerConfig.pipe(    Effect.provide(ServerConfig.layerTest(process.cwd(), baseDir)),  );  const config: ServerConfig.ServerConfig["Service"] = {    ...baseConfig,    mode: options.mode ?? "desktop",    ...("controlFd" in options      ? { desktopTelemetryControlFd: options.controlFd }      : { desktopTelemetryControlFd: 5 }),  };  const reportsForRun = options.reports ?? (() => []);  const changes = Stream.unwrap(    Deferred.await(requestIdDeferred).pipe(      Effect.map((requestId) => {        const emitted = Stream.fromIterable(reportsForRun(requestId));        return options.keepOpen ? Stream.concat(emitted, Stream.never) : emitted;      }),    ),  );   const service = yield* DesktopAppUpdate.make().pipe(    Effect.provide(      Layer.mergeAll(        DesktopTelemetryReceiver.layerTest({          requestDesktopUpdate: (requestId) =>            Deferred.succeed(requestIdDeferred, requestId).pipe(Effect.asVoid),          desktopUpdates: Effect.succeed({            latest: Option.none<DesktopUpdateStatusReport>(),            changes,          }),        }),        ServerConfig.layer(config),      ),    ),  );  return { service };}); it.layer(NodeServices.layer)("desktop app update", (it) => {  it.effect("is unavailable without desktop mode or the control fd", () =>    Effect.gen(function* () {      const web = yield* makeHarness({ mode: "web" });      expect(web.service.available).toBe(false);      const noFd = yield* makeHarness({ controlFd: undefined });      expect(noFd.service.available).toBe(false);      expect((yield* noFd.service.run(() => Effect.void).pipe(Effect.flip)).reason).toContain(        "not started by the T3 Code desktop app",      );      const desktop = yield* makeHarness();      expect(desktop.service.available).toBe(true);    }),  );   it.effect("collapses state reports into progress stages and succeeds on installing", () =>    Effect.gen(function* () {      const { service } = yield* makeHarness({        reports: (requestId) => [          report(requestId, makeState({ status: "checking" })),          report(requestId, makeState({ status: "available", availableVersion: "1.2.4" })),          report(requestId, makeState({ status: "downloading", downloadPercent: 40 })),          // Reports from another run must be ignored.          report("other-run", makeState({ status: "error", message: "unrelated" })),          report(requestId, makeState({ status: "downloaded", downloadedVersion: "1.2.4" }), {            outcome: "ready-to-install",          }),        ],      });      const stages: string[] = [];      const result = yield* service.run((stage) => Effect.sync(() => void stages.push(stage)));      expect(result).toEqual({        targetVersion: "1.2.4",        method: "desktop-app",        desktopUpdateToken: expect.any(String),      });      // "downloading" is not repeated for every download report.      expect(stages).toEqual(["downloading", "installing"]);       // Success releases the in-flight guard: if the desktop rejected the      // install after reporting, the server must accept a retry instead of      // refusing until restart. (The second run fails differently because      // the stub report stream is exhausted.)      const retry = yield* service.run(() => Effect.void).pipe(Effect.flip);      expect(retry.reason).not.toBe("A desktop app update is already in progress.");    }),  );   it.effect("maps up-to-date and failed outcomes to readable errors", () =>    Effect.gen(function* () {      const upToDate = yield* makeHarness({        reports: (requestId) => [          report(requestId, makeState({ status: "up-to-date" }), { outcome: "up-to-date" }),        ],      });      expect((yield* upToDate.service.run(() => Effect.void).pipe(Effect.flip)).reason).toBe(        "The T3 Code desktop app on this machine is already up to date on 1.2.3.",      );       const failed = yield* makeHarness({        reports: (requestId) => [          report(requestId, makeState({ status: "error", message: "feed unreachable" }), {            outcome: "failed",            reason: "feed unreachable",          }),        ],      });      expect((yield* failed.service.run(() => Effect.void).pipe(Effect.flip)).reason).toBe(        "feed unreachable",      );    }),  );   it.effect("replays a retained commit failure for the preparation token", () =>    Effect.gen(function* () {      const { service } = yield* makeHarness({        reports: (requestId) => [          report(requestId, makeState({ status: "downloaded", downloadedVersion: "1.2.4" }), {            outcome: "ready-to-install",          }),          report(            requestId,            makeState({              status: "downloaded",              downloadedVersion: "1.2.4",              errorContext: "install",              message: "installer refused",            }),            { outcome: "failed", reason: "installer refused" },          ),        ],      });      const prepared = yield* service.run(() => Effect.void);       expect(        (yield* service.commit(prepared.desktopUpdateToken ?? "missing").pipe(Effect.flip)).reason,      ).toBe("installer refused");    }),  );   it.effect("fails when the desktop stops reporting before a terminal outcome", () =>    Effect.gen(function* () {      const { service } = yield* makeHarness({        reports: (requestId) => [report(requestId, makeState({ status: "checking" }))],      });      expect((yield* service.run(() => Effect.void).pipe(Effect.flip)).reason).toBe(        "The desktop app stopped reporting its update.",      );    }),  );   it.effect("allows only one desktop update at a time", () =>    Effect.gen(function* () {      const { service } = yield* makeHarness({ reports: () => [], keepOpen: true });      const first = yield* Effect.forkChild(        service.run(() => Effect.void),        {          startImmediately: true,        },      );      expect((yield* service.run(() => Effect.void).pipe(Effect.flip)).reason).toBe(        "A desktop app update is already in progress.",      );      yield* Fiber.interrupt(first);       const retry = yield* Effect.forkChild(        service.run(() => Effect.void),        {          startImmediately: true,        },      );      yield* Effect.yieldNow;      expect((yield* service.run(() => Effect.void).pipe(Effect.flip)).reason).toBe(        "A desktop app update is already in progress.",      );      yield* Fiber.interrupt(retry);    }),  );});