842 lines · 29.7 KB · TypeScript
RawDownload
import { assert, describe, it } from "@effect/vitest";import type {  DesktopTelemetryRequestDesktopUpdate,  DesktopTelemetryCommitDesktopUpdate,  DesktopTelemetryCancelDesktopUpdate,  DesktopUpdateStatusReport,} from "@t3tools/contracts";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 Queue from "effect/Queue";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 DesktopState from "../app/DesktopState.ts";import * as DesktopTelemetryPublisher from "../telemetry/DesktopTelemetryPublisher.ts";import * as DesktopRemoteUpdates from "./DesktopRemoteUpdates.ts";import * as DesktopUpdates from "./DesktopUpdates.ts";import { makeHarness } from "./updatesTestHarness.ts"; // The remote flow hops between the test runtime's fibers and the updater's// runPromise-driven event handlers, so settling needs real microtask turns,// not just fiber yields.const settle = Effect.gen(function* () {  for (let i = 0; i < 20; i += 1) {    yield* Effect.yieldNow;    yield* Effect.promise(() => Promise.resolve());  }}); const request = (requestId: string): DesktopTelemetryRequestDesktopUpdate => ({  version: 1,  type: "requestDesktopUpdate",  requestId,}); function runRemoteUpdatesTest(  harness: ReturnType<typeof makeHarness>,  body: (context: {    readonly reports: DesktopUpdateStatusReport[];    readonly requests: Queue.Queue<DesktopTelemetryRequestDesktopUpdate>;    readonly commits: Queue.Queue<DesktopTelemetryCommitDesktopUpdate>;    readonly cancellations: Queue.Queue<DesktopTelemetryCancelDesktopUpdate>;  }) => Effect.Effect<void, never, DesktopUpdates.DesktopUpdates | DesktopState.DesktopState>,) {  return Effect.scoped(    Effect.gen(function* () {      const requests = yield* Queue.unbounded<DesktopTelemetryRequestDesktopUpdate>();      const commits = yield* Queue.unbounded<DesktopTelemetryCommitDesktopUpdate>();      const cancellations = yield* Queue.unbounded<DesktopTelemetryCancelDesktopUpdate>();      const reports: DesktopUpdateStatusReport[] = [];      const publisher = DesktopTelemetryPublisher.DesktopTelemetryPublisher.of({        latest: Effect.succeedNone,        changes: Stream.empty,        encoded: Stream.empty,        handleControl: () => Effect.void,        handleControlForSource: () => Effect.void,        removeControlSource: () => Effect.void,        publishUpdateReport: (report) =>          Effect.sync(() => {            reports.push(report);          }),        updateRequests: Stream.fromQueue(requests),        updateCommits: Stream.fromQueue(commits),        updateCancellations: Stream.fromQueue(cancellations),      });       const updates = yield* DesktopUpdates.DesktopUpdates;      yield* updates.configure;      yield* DesktopRemoteUpdates.listen.pipe(        Effect.provideService(DesktopTelemetryPublisher.DesktopTelemetryPublisher, publisher),      );      yield* settle;      yield* body({ reports, requests, commits, cancellations });    }),  ).pipe(Effect.provide(Layer.merge(TestClock.layer(), harness.layer)));} function terminalReports(reports: DesktopUpdateStatusReport[]): DesktopUpdateStatusReport[] {  return reports.filter((report) => report.outcome !== undefined);} describe("DesktopRemoteUpdates", () => {  it.effect("drives check, download, and install with no local confirmation", () => {    const harness = makeHarness();     return runRemoteUpdatesTest(harness, ({ reports, requests, commits }) =>      Effect.gen(function* () {        yield* Queue.offer(requests, request("req-1"));        yield* settle;        assert.equal(harness.checkCount(), 1);         harness.emit("update-available", { version: "1.2.4" });        yield* settle;        harness.emit("update-downloaded", { version: "1.2.4" });        yield* settle;         const terminals = terminalReports(reports);        assert.equal(terminals.length, 1);        assert.equal(terminals[0]?.outcome, "ready-to-install");        assert.equal(terminals[0]?.requestId, "req-1");        assert.equal(harness.quitAndInstalls(), 0);         yield* Queue.offer(commits, {          version: 1,          type: "commitDesktopUpdate",          requestId: "req-1",        });        yield* settle;        assert.equal(harness.quitAndInstalls(), 1);         // The mirror stamped the in-run state changes with the request id.        const statuses = reports          .filter((report) => report.requestId === "req-1")          .map((report) => report.state.status);        assert.include(statuses, "available");        assert.include(statuses, "downloaded");      }),    );  });   it.effect("reports a failed outcome when quitAndInstall fails", () => {    const harness = makeHarness({      quitAndInstall: Effect.fail(        new ElectronUpdater.ElectronUpdaterQuitAndInstallError({          channel: "latest",          isSilent: true,          isForceRunAfter: true,          cause: new Error("spawn failed"),        }),      ),    });     return runRemoteUpdatesTest(harness, ({ reports, requests, commits }) =>      Effect.gen(function* () {        yield* Queue.offer(requests, request("req-4"));        yield* settle;        harness.emit("update-available", { version: "1.2.4" });        yield* settle;        harness.emit("update-downloaded", { version: "1.2.4" });        yield* settle;         yield* Queue.offer(commits, {          version: 1,          type: "commitDesktopUpdate",          requestId: "req-4",        });        yield* settle;         // Install failures reduce to status "downloaded" + errorContext        // "install"; the prepared result is followed by the commit failure.        const terminals = terminalReports(reports);        assert.deepEqual(          terminals.map((report) => report.outcome),          ["ready-to-install", "failed"],        );        assert.equal(terminals[1]?.state.errorContext, "install");      }),    );  });   it.effect("joins an in-progress install on commit instead of failing the token", () => {    const installStarted = Deferred.makeUnsafe<void>();    const releaseInstall = Deferred.makeUnsafe<void>();    const harness = makeHarness({      stopBackend: Deferred.succeed(installStarted, undefined).pipe(        Effect.andThen(Deferred.await(releaseInstall)),      ),    });     return runRemoteUpdatesTest(harness, ({ reports, requests, commits }) =>      Effect.gen(function* () {        const updates = yield* DesktopUpdates.DesktopUpdates;        yield* Queue.offer(requests, request("req-join-commit"));        yield* settle;        harness.emit("update-available", { version: "1.2.4" });        yield* settle;        harness.emit("update-downloaded", { version: "1.2.4" });        yield* settle;        assert.deepEqual(          terminalReports(reports).map((report) => report.outcome),          ["ready-to-install"],        );         // A local install takes the updater reservation and starts shutdown.        const localInstall = yield* updates.install.pipe(Effect.forkChild);        yield* Deferred.await(installStarted);        yield* Queue.offer(commits, {          version: 1,          type: "commitDesktopUpdate",          requestId: "req-join-commit",        });        yield* settle;         // No "failed" marker: that install relaunches the app and the        // client proves the handoff by reconnecting on the target version.        assert.deepEqual(          terminalReports(reports).map((report) => report.outcome),          ["ready-to-install"],        );        assert.equal(harness.quitAndInstalls(), 0);         yield* Deferred.succeed(releaseInstall, undefined);        yield* Fiber.join(localInstall);      }),    );  });   it.effect("does not join a normal app quit as an update install", () => {    const harness = makeHarness();     return runRemoteUpdatesTest(harness, ({ reports, requests, commits }) =>      Effect.gen(function* () {        const desktopState = yield* DesktopState.DesktopState;        yield* Queue.offer(requests, request("req-normal-quit"));        yield* settle;        harness.emit("update-available", { version: "1.2.4" });        yield* settle;        harness.emit("update-downloaded", { version: "1.2.4" });        yield* settle;         yield* Ref.set(desktopState.quitting, true);        yield* Queue.offer(commits, {          version: 1,          type: "commitDesktopUpdate",          requestId: "req-normal-quit",        });        yield* settle;         assert.deepEqual(          terminalReports(reports).map((report) => report.outcome),          ["ready-to-install", "failed"],        );        assert.equal(harness.quitAndInstalls(), 0);      }),    );  });   it.effect("does not misread a lingering install error as a failed retry", () => {    let installAttempts = 0;    const harness = makeHarness({      quitAndInstall: Effect.suspend(() => {        installAttempts += 1;        return installAttempts === 1          ? Effect.fail(              new ElectronUpdater.ElectronUpdaterQuitAndInstallError({                channel: "latest",                isSilent: true,                isForceRunAfter: true,                cause: new Error("first attempt failed"),              }),            )          : Effect.void;      }),    });     return runRemoteUpdatesTest(harness, ({ reports, requests, commits }) =>      Effect.gen(function* () {        yield* Queue.offer(requests, request("req-5"));        yield* settle;        harness.emit("update-available", { version: "1.2.4" });        yield* settle;        harness.emit("update-downloaded", { version: "1.2.4" });        yield* settle;        yield* Queue.offer(commits, {          version: 1,          type: "commitDesktopUpdate",          requestId: "req-5",        });        yield* settle;        // First run failed; state is "downloaded" with a lingering        // errorContext "install". The retry succeeds and must not report        // that leftover as a fresh failure.        yield* Queue.offer(requests, request("req-6"));        yield* settle;        yield* Queue.offer(commits, {          version: 1,          type: "commitDesktopUpdate",          requestId: "req-6",        });        yield* settle;         const retryTerminals = terminalReports(reports).filter(          (report) => report.requestId === "req-6",        );        assert.deepEqual(          retryTerminals.map((report) => report.outcome),          ["ready-to-install"],        );        assert.equal(harness.quitAndInstalls(), 2);      }),    );  });   it.effect("ignores an unrelated updater event while an install is starting", () => {    const stopStarted = Deferred.makeUnsafe<void>();    const releaseStop = Deferred.makeUnsafe<void>();    const harness = makeHarness({      stopBackend: Deferred.succeed(stopStarted, undefined).pipe(        Effect.andThen(Deferred.await(releaseStop)),      ),    });     return runRemoteUpdatesTest(harness, ({ reports, requests, commits }) =>      Effect.gen(function* () {        yield* Queue.offer(requests, request("req-late-event"));        yield* settle;        harness.emit("update-available", { version: "1.2.4" });        yield* settle;        harness.emit("update-downloaded", { version: "1.2.4" });        yield* settle;         yield* Queue.offer(commits, {          version: 1,          type: "commitDesktopUpdate",          requestId: "req-late-event",        });        yield* Deferred.await(stopStarted);        harness.emit("download-progress", { percent: 90 });        yield* settle;        yield* Deferred.succeed(releaseStop, undefined);        yield* settle;         assert.deepEqual(          terminalReports(reports)            .filter((report) => report.requestId === "req-late-event")            .map((report) => report.outcome),          ["ready-to-install"],        );      }),    );  });   it.effect("retries a download refused while the check still holds the reservation", () => {    // electron-updater emits update-available from inside checkForUpdates,    // before the check action releases its reservation. The download the    // remote flow forks in response is refused and must be retried once the    // reservation frees up, without burning a download attempt.    const releaseCheck = Deferred.makeUnsafe<void>();    const harness = makeHarness({ checkForUpdates: Deferred.await(releaseCheck) });     return runRemoteUpdatesTest(harness, ({ reports, requests }) =>      Effect.gen(function* () {        yield* Queue.offer(requests, request("req-8"));        yield* settle;        assert.equal(harness.checkCount(), 1);         // Fire "available" while the check reservation is still held.        harness.emit("update-available", { version: "1.2.4" });        yield* settle;        assert.equal(harness.downloadCount(), 0);         yield* Deferred.succeed(releaseCheck, undefined);        yield* settle;        yield* TestClock.adjust(Duration.millis(300));        yield* settle;        assert.equal(harness.downloadCount(), 1);         harness.emit("update-downloaded", { version: "1.2.4" });        yield* settle;        assert.deepEqual(          terminalReports(reports).map((report) => report.outcome),          ["ready-to-install"],        );        assert.equal(harness.quitAndInstalls(), 0);      }),    );  });   it.effect("waits for the download reservation before reporting prepared", () => {    // update-downloaded fires from inside downloadUpdate. If the flow    // reported "installing" right then, install would be refused for the    // held reservation after the irrevocable terminal already went out.    const releaseDownload = Deferred.makeUnsafe<void>();    const harness = makeHarness({ downloadUpdate: Deferred.await(releaseDownload) });     return runRemoteUpdatesTest(harness, ({ reports, requests }) =>      Effect.gen(function* () {        yield* Queue.offer(requests, request("req-10"));        yield* settle;        harness.emit("update-available", { version: "1.2.4" });        yield* settle;        harness.emit("update-downloaded", { version: "1.2.4" });        yield* settle;        assert.deepEqual(terminalReports(reports), []);        assert.equal(harness.quitAndInstalls(), 0);         yield* Deferred.succeed(releaseDownload, undefined);        yield* settle;        yield* TestClock.adjust(Duration.millis(300));        yield* settle;        assert.deepEqual(          terminalReports(reports).map((report) => report.outcome),          ["ready-to-install"],        );        assert.equal(harness.quitAndInstalls(), 0);      }),    );  });   it.effect("joins an install that is already tearing the app down", () => {    const harness = makeHarness();     return runRemoteUpdatesTest(harness, ({ reports, requests }) =>      Effect.gen(function* () {        const desktopState = yield* DesktopState.DesktopState;        harness.emit("update-downloaded", { version: "1.2.4" });        yield* settle;        // Another install already owns the shutdown.        yield* Ref.set(desktopState.quitting, true);         yield* Queue.offer(requests, request("req-9"));        yield* settle;         // Report "installing" once, no "failed" after the refusal: that        // install will relaunch the app and this request rides along.        assert.deepEqual(          terminalReports(reports).map((report) => report.outcome),          ["ready-to-install"],        );        assert.equal(harness.quitAndInstalls(), 0);      }),    );  });   it.effect("does not run the installer twice for a repeated commit", () => {    const harness = makeHarness();     return runRemoteUpdatesTest(harness, ({ requests, commits }) =>      Effect.gen(function* () {        yield* Queue.offer(requests, request("req-repeat"));        yield* settle;        harness.emit("update-available", { version: "1.2.4" });        yield* settle;        harness.emit("update-downloaded", { version: "1.2.4" });        yield* settle;         const commit = {          version: 1,          type: "commitDesktopUpdate",          requestId: "req-repeat",        } as const;        yield* Queue.offer(commits, commit);        yield* settle;        yield* Queue.offer(commits, commit);        yield* settle;         assert.equal(harness.quitAndInstalls(), 1);      }),    );  });   it.effect("waits for a background check before installing a prepared update", () => {    const backgroundCheckStarted = Deferred.makeUnsafe<void>();    const releaseBackgroundCheck = Deferred.makeUnsafe<void>();    let checks = 0;    const harness = makeHarness({      checkForUpdates: Effect.suspend(() => {        checks += 1;        return checks === 2          ? Deferred.succeed(backgroundCheckStarted, undefined).pipe(              Effect.andThen(Deferred.await(releaseBackgroundCheck)),            )          : Effect.void;      }),    });     return runRemoteUpdatesTest(harness, ({ requests, commits }) =>      Effect.gen(function* () {        const updates = yield* DesktopUpdates.DesktopUpdates;        yield* Queue.offer(requests, request("req-background-check"));        yield* settle;        harness.emit("update-available", { version: "1.2.4" });        yield* settle;        harness.emit("update-downloaded", { version: "1.2.4" });        yield* settle;         const backgroundCheck = yield* updates.check("poll").pipe(Effect.forkChild);        yield* Deferred.await(backgroundCheckStarted);        assert.equal((yield* updates.getState).status, "checking");         yield* Queue.offer(commits, {          version: 1,          type: "commitDesktopUpdate",          requestId: "req-background-check",        });        yield* settle;        assert.equal(harness.quitAndInstalls(), 0);         harness.emit("update-downloaded", { version: "1.2.4" });        yield* Deferred.succeed(releaseBackgroundCheck, undefined);        yield* Fiber.join(backgroundCheck);        yield* settle;        assert.equal(harness.quitAndInstalls(), 1);      }),    );

Lines 1–500 of 842.