809 lines · 30.8 KB · TypeScript
RawDownload
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 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 Stream from "effect/Stream";import * as TestClock from "effect/testing/TestClock"; import * as ElectronUpdater from "../electron/ElectronUpdater.ts";import * as DesktopAppSettings from "../settings/DesktopAppSettings.ts";import * as DesktopState from "../app/DesktopState.ts";import * as DesktopUpdates from "./DesktopUpdates.ts";import { flushCallbacks, makeHarness } from "./updatesTestHarness.ts"; 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("subscribe delivers the latest state plus subsequent changes", () => {    const harness = makeHarness();     return Effect.scoped(      Effect.gen(function* () {        const updates = yield* DesktopUpdates.DesktopUpdates;        yield* updates.configure;         const { latest, changes } = yield* updates.subscribe;        assert.equal(latest.status, "idle");         const nextState = yield* Stream.runHead(changes).pipe(Effect.forkChild);        yield* flushCallbacks;        harness.emit("update-available", { version: "1.2.4" });        yield* flushCallbacks;         const observed = yield* Fiber.join(nextState);        assert.equal(Option.getOrThrow(observed).status, "available");        assert.equal(Option.getOrThrow(observed).availableVersion, "1.2.4");      }),    ).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",            },            { version: "1.2.4-nightly.20260709.764", note: "- Change 764" },            { version: "1.2.4-nightly.20260709.763", note: "- Change 763" },            { version: "1.2.4-nightly.20260709.762", note: "- Change 762" },            { version: "1.2.4-nightly.20260709.761", note: "- Change 761" },            { version: "1.2.4-nightly.20260709.760", note: "- Change 760" },          ],        });        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"],            totalItems: 1,          },          {            version: "1.2.4-nightly.20260709.765",            items: ["[codex] Upgrade Clerk stack by @juliusmarminge in #3821"],            totalItems: 1,          },          { version: "1.2.4-nightly.20260709.764", items: ["Change 764"], totalItems: 1 },          { version: "1.2.4-nightly.20260709.763", items: ["Change 763"], totalItems: 1 },          { version: "1.2.4-nightly.20260709.762", items: ["Change 762"], totalItems: 1 },          { version: "1.2.4-nightly.20260709.761", items: ["Change 761"], totalItems: 1 },        ]);        assert.equal(state.omittedReleaseCount, 1);        assert.deepEqual(harness.sentStates.at(-1)?.releaseNotes, state.releaseNotes);        assert.equal(harness.sentStates.at(-1)?.omittedReleaseCount, 1);      }),    ).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"], totalItems: 1 },        ]);        assert.equal(unchangedState.omittedReleaseCount, 0);         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"], totalItems: 1 },        ]);        assert.equal(state.omittedReleaseCount, 0);        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"], totalItems: 1 },        ]);        assert.equal(state.omittedReleaseCount, 0);        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", () =>    Effect.gen(function* () {      const installStarted = yield* Deferred.make<void>();      const releaseInstall = yield* Deferred.make<void>();      const harness = makeHarness({        stopBackend: Deferred.succeed(installStarted, undefined).pipe(          Effect.andThen(Deferred.await(releaseInstall)),        ),      });       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 installFiber = yield* updates.install.pipe(Effect.forkScoped);          yield* Deferred.await(installStarted);           const checkResult = yield* updates.check("manual");          assert.isFalse(checkResult.checked);          assert.equal(harness.checkCount(), 0);           yield* Deferred.succeed(releaseInstall, undefined);          const installResult = yield* Fiber.join(installFiber);          assert.isTrue(installResult.accepted);        }),      ).pipe(Effect.provide(Layer.merge(TestClock.layer(), harness.layer)));    }),  );   it.effect("keeps raw updater event failures out of update state", () => {    const harness = makeHarness();    const cause = new Error(      "request failed for https://user:secret@example.com/update?token=secret",    );     return Effect.scoped(      Effect.gen(function* () {        const updates = yield* DesktopUpdates.DesktopUpdates;        yield* updates.configure;         harness.emit("error", cause);        yield* flushCallbacks;         const state = yield* updates.getState;        assert.equal(state.status, "error");        assert.equal(state.message, "Desktop updater background operation reported an error.");        assert.notInclude(state.message ?? "", "secret");      }),    ).pipe(Effect.provide(Layer.merge(TestClock.layer(), harness.layer)));  });   it.effect("preserves a queued installer after a background updater error", () => {    const harness = makeHarness();     return Effect.scoped(      Effect.gen(function* () {        const updates = yield* DesktopUpdates.DesktopUpdates;        yield* updates.configure;        harness.emit("update-downloaded", { version: "1.2.4" });        yield* flushCallbacks;         harness.emit("error", new Error("background updater failure"));        yield* flushCallbacks;         const state = yield* updates.getState;        assert.equal(state.status, "error");        assert.equal(state.downloadedVersion, "1.2.4");        assert.isNull(state.errorContext);         const result = yield* updates.install;        assert.isTrue(result.accepted);      }),    ).pipe(Effect.provide(Layer.merge(TestClock.layer(), harness.layer)));  });   it.effect("logs bounded updater failure context without exposing the cause", () => {    const cause = new Error(      "request failed for https://user:secret@example.com/update?token=secret",    );    const updaterError = new ElectronUpdater.ElectronUpdaterCheckForUpdatesError({      channel: null,      cause,    });    const harness = makeHarness({ checkForUpdates: Effect.fail(updaterError) });    const loggedAnnotations: Array<Record<string, unknown>> = [];    const logger = Logger.make(({ fiber }) => {      const annotations = fiber.getRef(References.CurrentLogAnnotations);      if (annotations.errorTag === "ElectronUpdaterCheckForUpdatesError") {        loggedAnnotations.push(annotations);      }    });     return Effect.scoped(      Effect.gen(function* () {        const updates = yield* DesktopUpdates.DesktopUpdates;        yield* updates.configure;         yield* updates.check("manual");         const state = yield* updates.getState;        const loggedAnnotation = loggedAnnotations.at(-1);        assert.isDefined(loggedAnnotation);        assert.equal(loggedAnnotation.errorTag, "ElectronUpdaterCheckForUpdatesError");        assert.isNull(loggedAnnotation.channel);        assert.notProperty(loggedAnnotation, "error");        assert.notInclude(Object.values(loggedAnnotation).map(String).join(" "), "secret");        assert.equal(          state.message,          "Electron updater failed to check for updates on channel default.",        );        assert.notInclude(state.message ?? "", "secret");      }),    ).pipe(      Effect.provide(        Layer.mergeAll(          TestClock.layer(),          harness.layer,          Logger.layer([logger], { mergeWithExisting: false }),        ),      ),    );  });   it.effect("recovers download state after an unexpected setup failure", () => {    let disableDifferentialCalls = 0;    const harness = makeHarness({      setDisableDifferentialDownload: Effect.suspend(() => {        disableDifferentialCalls += 1;        return disableDifferentialCalls === 1          ? Effect.void          : Effect.die(new Error("download setup failed"));      }),    });     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 result = yield* updates.download;        assert.isTrue(result.accepted);        assert.isFalse(result.completed);         const failedState = yield* updates.getState;        assert.equal(failedState.status, "available");        assert.equal(failedState.errorContext, "download");        assert.equal(failedState.message, "Desktop update download action failed unexpectedly.");         const changedState = yield* updates.setChannel("nightly");        assert.equal(changedState.channel, "nightly");      }),    ).pipe(Effect.provide(Layer.merge(TestClock.layer(), harness.layer)));  });   it.effect("restores download state and permits retry after interruption", () =>    Effect.gen(function* () {      const actionStarted = yield* Deferred.make<void>();      let disableDifferentialCalls = 0;      const harness = makeHarness({        setDisableDifferentialDownload: Effect.suspend(() => {          disableDifferentialCalls += 1;          if (disableDifferentialCalls === 1) {

Lines 1–500 of 809.