862 lines · 32.3 KB · TypeScript
RawDownload
import { assert, describe, it } from "@effect/vitest";import * as NodeServices from "@effect/platform-node/NodeServices";import type { DesktopUpdateState } from "@t3tools/contracts";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 Fiber from "effect/Fiber";import * as Layer from "effect/Layer";import * as Logger from "effect/Logger";import * as Option from "effect/Option";import * as References from "effect/References";import * as Ref from "effect/Ref";import * as TestClock from "effect/testing/TestClock"; import * as DesktopBackendPool from "../backend/DesktopBackendPool.ts";import * as DesktopConfig from "../app/DesktopConfig.ts";import * as DesktopEnvironment from "../app/DesktopEnvironment.ts";import * as ElectronUpdater from "../electron/ElectronUpdater.ts";import * as ElectronWindow from "../electron/ElectronWindow.ts";import * as DesktopAppSettings from "../settings/DesktopAppSettings.ts";import * as DesktopState from "../app/DesktopState.ts";import * as DesktopUpdates from "./DesktopUpdates.ts"; interface UpdatesHarnessOptions {  readonly checkForUpdates?: Effect.Effect<    void,    ElectronUpdater.ElectronUpdaterCheckForUpdatesError  >;  readonly beforeSetUpdateChannel?: Effect.Effect<void>;  readonly setUpdateChannelError?: DesktopAppSettings.DesktopSettingsWriteError;  readonly setDisableDifferentialDownload?: Effect.Effect<void>;  readonly stopBackend?: Effect.Effect<void>;  readonly env?: Record<string, string | undefined>;} const flushCallbacks = Effect.yieldNow; function makeHarness(options: UpdatesHarnessOptions = {}) {  let checkCount = 0;  let allowDowngrade = false;  let fullChangelog = false;  const feedUrls: ElectronUpdater.ElectronUpdaterFeedUrl[] = [];  const listeners = new Map<string, Set<(...args: readonly unknown[]) => void>>();  const sentStates: DesktopUpdateState[] = [];   const addListener = (eventName: string, listener: (...args: readonly unknown[]) => void) => {    const eventListeners = listeners.get(eventName) ?? new Set();    eventListeners.add(listener);    listeners.set(eventName, eventListeners);  };   const removeListener = (eventName: string, listener: (...args: readonly unknown[]) => void) => {    const eventListeners = listeners.get(eventName);    if (!eventListeners) {      return;    }    eventListeners.delete(listener);    if (eventListeners.size === 0) {      listeners.delete(eventName);    }  };   const updaterLayer = Layer.succeed(ElectronUpdater.ElectronUpdater, {    setFeedURL: (options) =>      Effect.sync(() => {        feedUrls.push(options);      }),    setAutoDownload: () => Effect.void,    setAutoInstallOnAppQuit: () => Effect.void,    setChannel: () => Effect.void,    setAllowPrerelease: () => Effect.void,    allowDowngrade: Effect.sync(() => allowDowngrade),    setAllowDowngrade: (value) =>      Effect.sync(() => {        allowDowngrade = value;      }),    setFullChangelog: (value) =>      Effect.sync(() => {        fullChangelog = value;      }),    setDisableDifferentialDownload: () => options.setDisableDifferentialDownload ?? Effect.void,    checkForUpdates: Effect.sync(() => {      checkCount += 1;    }).pipe(Effect.andThen(options.checkForUpdates ?? Effect.void)),    downloadUpdate: Effect.void,    quitAndInstall: () => Effect.void,    on: (eventName, listener) =>      Effect.acquireRelease(        Effect.sync(() => {          addListener(eventName, listener as unknown as (...args: readonly unknown[]) => void);        }),        () =>          Effect.sync(() => {            removeListener(eventName, listener as unknown as (...args: readonly unknown[]) => void);          }),      ).pipe(Effect.asVoid),  } satisfies ElectronUpdater.ElectronUpdater["Service"]);   const windowLayer = Layer.succeed(ElectronWindow.ElectronWindow, {    create: () => Effect.die("unexpected BrowserWindow creation"),    main: Effect.succeed(Option.none()),    currentMainOrFirst: Effect.succeed(Option.none()),    focusedMainOrFirst: Effect.succeed(Option.none()),    setMain: () => Effect.void,    clearMain: () => Effect.void,    reveal: () => Effect.void,    sendAll: (_channel, state) =>      Effect.sync(() => {        sentStates.push(state as DesktopUpdateState);      }),    destroyAll: Effect.void,    syncAllAppearance: () => Effect.void,  } satisfies ElectronWindow.ElectronWindow["Service"]);   const stubBackendInstance: DesktopBackendPool.DesktopBackendInstance = {    id: DesktopBackendPool.PRIMARY_INSTANCE_ID,    label: Effect.succeed("Windows"),    start: Effect.void,    stop: () => options.stopBackend ?? Effect.void,    currentConfig: Effect.succeed(Option.none()),    snapshot: Effect.succeed({      desiredRunning: false,      ready: false,      activePid: Option.none(),      restartAttempt: 0,      restartScheduled: false,    }),    waitForReady: () => Effect.succeed(true),  };  const backendLayer = DesktopBackendPool.layerTest([stubBackendInstance]);   const environmentLayer = DesktopEnvironment.layer({    dirname: "/repo/apps/desktop/src",    homeDirectory: `/tmp/t3-desktop-updates-home-${process.pid}`,    platform: "darwin",    processArch: "x64",    appVersion: "1.2.3",    appPath: "/repo",    isPackaged: true,    resourcesPath: "/missing/resources",    runningUnderArm64Translation: false,  }).pipe(    Layer.provide(      Layer.mergeAll(        NodeServices.layer,        DesktopConfig.layerTest({          T3CODE_HOME: `/tmp/t3-desktop-updates-test-${process.pid}`,          T3CODE_DESKTOP_MOCK_UPDATES: "true",          T3CODE_DESKTOP_MOCK_UPDATE_SERVER_PORT: "4141",          ...options.env,        }),      ),    ),  );   let testSettings: DesktopAppSettings.DesktopSettings = {    ...DesktopAppSettings.DEFAULT_DESKTOP_SETTINGS,  };  const setUpdateChannelError = options.setUpdateChannelError;  const settingsLayer =    setUpdateChannelError || options.beforeSetUpdateChannel      ? Layer.succeed(DesktopAppSettings.DesktopAppSettings, {          get: Effect.sync(() => testSettings),          load: Effect.sync(() => testSettings),          setMainWindowBounds: () => Effect.die("unexpected main window bounds update"),          setServerExposureMode: () => Effect.die("unexpected server exposure update"),          setTailscaleServe: () => Effect.die("unexpected Tailscale Serve update"),          setUpdateChannel: (channel) =>            setUpdateChannelError              ? Effect.fail(setUpdateChannelError)              : (options.beforeSetUpdateChannel ?? Effect.void).pipe(                  Effect.andThen(                    Effect.sync(() => {                      const changed = testSettings.updateChannel !== channel;                      testSettings = {                        ...testSettings,                        updateChannel: channel,                        updateChannelConfiguredByUser: true,                      };                      return { settings: testSettings, changed };                    }),                  ),                ),          setWslBackendEnabled: () => Effect.die("unexpected WSL backend toggle"),          setWslDistro: () => Effect.die("unexpected WSL distro change"),          setWslOnly: () => Effect.die("unexpected WSL-only toggle"),          applyWslWindowsFallback: Effect.die("unexpected WSL Windows fallback"),          applyWslWindowsFallbackInMemory: Effect.die("unexpected WSL Windows fallback"),        } satisfies DesktopAppSettings.DesktopAppSettings["Service"])      : DesktopAppSettings.layer;   const layer = DesktopUpdates.layer.pipe(    Layer.provideMerge(updaterLayer),    Layer.provideMerge(windowLayer),    Layer.provideMerge(backendLayer),    Layer.provideMerge(DesktopState.layer),    Layer.provideMerge(settingsLayer),    Layer.provideMerge(      DesktopConfig.layerTest({        T3CODE_HOME: `/tmp/t3-desktop-updates-test-${process.pid}`,        T3CODE_DESKTOP_MOCK_UPDATES: "true",        T3CODE_DESKTOP_MOCK_UPDATE_SERVER_PORT: "4141",        ...options.env,      }),    ),    Layer.provideMerge(environmentLayer),    Layer.provideMerge(NodeServices.layer),  );   return {    layer,    checkCount: () => checkCount,    feedUrls: () => feedUrls,    fullChangelog: () => fullChangelog,    listenerCount: () =>      Array.from(listeners.values()).reduce(        (total, eventListeners) => total + eventListeners.size,        0,      ),    sentStates,    emit: (eventName: string, payload?: unknown) => {      for (const listener of listeners.get(eventName) ?? []) {        listener(payload);      }    },  };} describe("DesktopUpdates", () => {  it("preserves complete causes for update poller and event failures", () => {    const cause = Cause.combine(      Cause.fail(new Error("updater failed")),      Cause.die(new Error("updater defect")),    );    const pollerError = new DesktopUpdates.DesktopUpdatePollerError({      poller: "startup",      cause,    });    const eventError = new DesktopUpdates.DesktopUpdateEventHandlingError({      event: "download-progress",      cause,    });    const reportedError = new DesktopUpdates.DesktopUpdaterReportedError({      operation: "download",      cause,    });    const unexpectedActionError = new DesktopUpdates.DesktopUpdateUnexpectedActionError({      action: "install",      cause,    });     assert.strictEqual(pollerError.cause, cause);    assert.equal(pollerError.poller, "startup");    assert.equal(pollerError.message, "Desktop update startup poller failed.");    assert.strictEqual(eventError.cause, cause);    assert.equal(eventError.event, "download-progress");    assert.equal(eventError.message, "Failed to handle desktop update download-progress event.");    assert.strictEqual(reportedError.cause, cause);    assert.equal(reportedError.operation, "download");    assert.equal(reportedError.message, "Desktop updater download operation reported an error.");    assert.strictEqual(unexpectedActionError.cause, cause);    assert.equal(unexpectedActionError.action, "install");    assert.equal(      unexpectedActionError.message,      "Desktop update install action failed unexpectedly.",    );  });   it.effect("configures the updater and runs startup checks on the test clock", () => {    const harness = makeHarness();     return Effect.gen(function* () {      yield* Effect.scoped(        Effect.gen(function* () {          const updates = yield* DesktopUpdates.DesktopUpdates;          yield* updates.configure;           const state = yield* updates.getState;          assert.equal(state.enabled, true);          assert.equal(state.status, "idle");          assert.deepEqual(harness.feedUrls(), [            { provider: "generic", url: "http://localhost:4141" },          ]);          assert.equal(harness.listenerCount(), 6);          assert.equal(harness.checkCount(), 0);           yield* TestClock.adjust(Duration.millis(15_000));          assert.equal(harness.checkCount(), 1);        }),      );       assert.equal(harness.listenerCount(), 0);    }).pipe(Effect.provide(Layer.merge(TestClock.layer(), harness.layer)));  });   it.effect("updates and broadcasts state from updater events", () => {    const harness = makeHarness();     return Effect.scoped(      Effect.gen(function* () {        const updates = yield* DesktopUpdates.DesktopUpdates;        yield* updates.configure;         harness.emit("update-available", { version: "1.2.4" });        yield* flushCallbacks;         const state = yield* updates.getState;        assert.equal(state.status, "available");        assert.equal(state.availableVersion, "1.2.4");        assert.isNotNull(state.checkedAt);        assert.equal(harness.sentStates.at(-1)?.status, "available");      }),    ).pipe(Effect.provide(Layer.merge(TestClock.layer(), harness.layer)));  });   it.effect("enables nightly full changelog release notes and broadcasts summaries", () => {    const harness = makeHarness();     return Effect.scoped(      Effect.gen(function* () {        const updates = yield* DesktopUpdates.DesktopUpdates;        yield* updates.configure;         yield* updates.setChannel("nightly");        assert.equal(harness.fullChangelog(), true);         harness.emit("update-available", {          version: "1.2.4-nightly.20260709.766",          releaseNotes: [            {              version: "1.2.4-nightly.20260709.766",              note: `<h2>What's Changed</h2><ul><li>feat(client): persist offline environment data by <a>@juliusmarminge</a> in <a>#3795</a></li></ul><h2>Full Changelog</h2>`,            },            {              version: "1.2.4-nightly.20260709.765",              note: "- [codex] Upgrade Clerk stack by @juliusmarminge in #3821",            },          ],        });        yield* flushCallbacks;         const state = yield* updates.getState;        assert.equal(state.status, "available");        assert.deepEqual(state.releaseNotes, [          {            version: "1.2.4-nightly.20260709.766",            items: ["feat(client): persist offline environment data by @juliusmarminge in #3795"],          },          {            version: "1.2.4-nightly.20260709.765",            items: ["[codex] Upgrade Clerk stack by @juliusmarminge in #3821"],          },        ]);        assert.deepEqual(harness.sentStates.at(-1)?.releaseNotes, state.releaseNotes);      }),    ).pipe(Effect.provide(Layer.merge(TestClock.layer(), harness.layer)));  });   it.effect("checks for newer releases after an update has been downloaded", () => {    const harness = makeHarness();     return Effect.scoped(      Effect.gen(function* () {        const updates = yield* DesktopUpdates.DesktopUpdates;        yield* updates.configure;         harness.emit("update-available", {          version: "1.2.4",          releaseNotes: "## What's changed\n- fix: queued update",        });        yield* flushCallbacks;        harness.emit("update-downloaded", { version: "1.2.4" });        yield* flushCallbacks;         const result = yield* updates.check("poll");        assert.isTrue(result.checked);         harness.emit("update-available", { version: "1.2.4" });        yield* flushCallbacks;         const unchangedState = yield* updates.getState;        assert.equal(unchangedState.status, "downloaded");        assert.equal(unchangedState.downloadedVersion, "1.2.4");        assert.deepEqual(unchangedState.releaseNotes, [          { version: "1.2.4", items: ["fix: queued update"] },        ]);         const nextResult = yield* updates.check("poll");        assert.isTrue(nextResult.checked);         harness.emit("update-available", { version: "1.2.5" });        yield* flushCallbacks;         const state = yield* updates.getState;        assert.equal(state.status, "available");        assert.equal(state.availableVersion, "1.2.5");        assert.isNull(state.downloadedVersion);      }),    ).pipe(Effect.provide(Layer.merge(TestClock.layer(), harness.layer)));  });   it.effect("preserves a queued installer when the feed has no update", () => {    const harness = makeHarness();     return Effect.scoped(      Effect.gen(function* () {        const updates = yield* DesktopUpdates.DesktopUpdates;        yield* updates.configure;         harness.emit("update-available", {          version: "1.2.4",          releaseNotes: "## What's changed\n- fix: queued update",        });        yield* flushCallbacks;        harness.emit("update-downloaded", { version: "1.2.4" });        yield* flushCallbacks;         yield* updates.check("poll");        harness.emit("update-not-available");        yield* flushCallbacks;         const state = yield* updates.getState;        assert.equal(state.status, "downloaded");        assert.equal(state.availableVersion, "1.2.4");        assert.equal(state.downloadedVersion, "1.2.4");        assert.deepEqual(state.releaseNotes, [{ version: "1.2.4", items: ["fix: queued update"] }]);        assert.equal(state.downloadPercent, 100);      }),    ).pipe(Effect.provide(Layer.merge(TestClock.layer(), harness.layer)));  });   it.effect("preserves a queued installer when the feed offers another channel", () => {    const harness = makeHarness();     return Effect.scoped(      Effect.gen(function* () {        const updates = yield* DesktopUpdates.DesktopUpdates;        yield* updates.configure;         harness.emit("update-available", {          version: "1.2.4",          releaseNotes: "## What's changed\n- fix: queued update",        });        yield* flushCallbacks;        harness.emit("update-downloaded", { version: "1.2.4" });        yield* flushCallbacks;         yield* updates.check("poll");        harness.emit("update-available", { version: "1.2.5-nightly.20260710.1" });        yield* flushCallbacks;         const state = yield* updates.getState;        assert.equal(state.status, "downloaded");        assert.equal(state.availableVersion, "1.2.4");        assert.equal(state.downloadedVersion, "1.2.4");        assert.deepEqual(state.releaseNotes, [{ version: "1.2.4", items: ["fix: queued update"] }]);        assert.equal(state.downloadPercent, 100);      }),    ).pipe(Effect.provide(Layer.merge(TestClock.layer(), harness.layer)));  });   it.effect(    "rejects install while a refresh check is in progress and releases the reservation",    () =>      Effect.gen(function* () {        const checkStarted = yield* Deferred.make<void>();        const releaseCheck = yield* Deferred.make<void>();        const harness = makeHarness({          checkForUpdates: Deferred.succeed(checkStarted, undefined).pipe(            Effect.andThen(Deferred.await(releaseCheck)),          ),        });         yield* Effect.scoped(          Effect.gen(function* () {            const updates = yield* DesktopUpdates.DesktopUpdates;            yield* updates.configure;            harness.emit("update-downloaded", { version: "1.2.4" });            yield* flushCallbacks;             const checkFiber = yield* updates.check("manual").pipe(Effect.forkScoped);            yield* Deferred.await(checkStarted);             const installResult = yield* updates.install;            assert.isFalse(installResult.accepted);             yield* Deferred.succeed(releaseCheck, undefined);            const checkResult = yield* Fiber.join(checkFiber);            assert.isTrue(checkResult.checked);             const followUpCheck = yield* updates.check("manual");            assert.isTrue(followUpCheck.checked);            assert.equal(harness.checkCount(), 2);          }),        ).pipe(Effect.provide(Layer.merge(TestClock.layer(), harness.layer)));      }),  );   it.effect("rejects refresh checks while install is in progress", () =>

Lines 1–500 of 862.