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: `
What's Changed
Full Changelog
`,
},
{
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();
const releaseCheck = yield* Deferred.make();
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();
const releaseInstall = yield* Deferred.make();
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> = [];
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();
let disableDifferentialCalls = 0;
const harness = makeHarness({
setDisableDifferentialDownload: Effect.suspend(() => {
disableDifferentialCalls += 1;
if (disableDifferentialCalls === 1) {
return Effect.void;
}
if (disableDifferentialCalls === 2) {
return Deferred.succeed(actionStarted, undefined).pipe(Effect.andThen(Effect.never));
}
return Effect.void;
}),
});
yield* Effect.scoped(
Effect.gen(function* () {
const updates = yield* DesktopUpdates.DesktopUpdates;
yield* updates.configure;
harness.emit("update-available", { version: "1.2.4" });
yield* flushCallbacks;
const downloadFiber = yield* updates.download.pipe(Effect.forkScoped);
yield* Deferred.await(actionStarted);
yield* Fiber.interrupt(downloadFiber);
const interruptedState = yield* updates.getState;
assert.equal(interruptedState.status, "available");
assert.isNull(interruptedState.message);
const retry = yield* updates.download;
assert.isTrue(retry.accepted);
assert.isTrue(retry.completed);
}),
).pipe(Effect.provide(Layer.merge(TestClock.layer(), harness.layer)));
}),
);
it.effect("clears quitting state after an unexpected install setup failure", () => {
const harness = makeHarness({
stopBackend: Effect.die(new Error("backend stop failed")),
});
return Effect.scoped(
Effect.gen(function* () {
const desktopState = yield* DesktopState.DesktopState;
const updates = yield* DesktopUpdates.DesktopUpdates;
yield* updates.configure;
harness.emit("update-downloaded", { version: "1.2.4" });
yield* flushCallbacks;
const result = yield* updates.install;
assert.isTrue(result.accepted);
assert.isFalse(result.completed);
assert.isFalse(yield* Ref.get(desktopState.quitting));
const failedState = yield* updates.getState;
assert.equal(failedState.status, "downloaded");
assert.equal(failedState.errorContext, "install");
assert.equal(failedState.message, "Desktop update install 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("keeps windows and restarts backends when quitAndInstall fails", () => {
const harness = makeHarness({
quitAndInstall: Effect.fail(
new ElectronUpdater.ElectronUpdaterQuitAndInstallError({
channel: "latest",
isSilent: true,
isForceRunAfter: true,
cause: new Error("installer refused"),
}),
),
});
return Effect.scoped(
Effect.gen(function* () {
const desktopState = yield* DesktopState.DesktopState;
const updates = yield* DesktopUpdates.DesktopUpdates;
yield* updates.configure;
harness.emit("update-downloaded", { version: "1.2.4" });
yield* flushCallbacks;
const result = yield* updates.install;
assert.isTrue(result.accepted);
assert.isFalse(yield* Ref.get(desktopState.quitting));
assert.deepEqual(harness.installSteps, ["quitAndInstall", "startBackend"]);
}),
).pipe(Effect.provide(Layer.merge(TestClock.layer(), harness.layer)));
});
it.effect("holds the install reservation until failed-install recovery finishes", () => {
const recoveryStarted = Deferred.makeUnsafe();
const releaseRecovery = Deferred.makeUnsafe();
const harness = makeHarness({
quitAndInstall: Effect.fail(
new ElectronUpdater.ElectronUpdaterQuitAndInstallError({
channel: "latest",
isSilent: true,
isForceRunAfter: true,
cause: new Error("installer refused"),
}),
),
startBackend: Deferred.succeed(recoveryStarted, undefined).pipe(
Effect.andThen(Deferred.await(releaseRecovery)),
),
});
return Effect.scoped(
Effect.gen(function* () {
const updates = yield* DesktopUpdates.DesktopUpdates;
yield* updates.configure;
harness.emit("update-downloaded", { version: "1.2.4" });
yield* flushCallbacks;
const failedInstall = yield* updates.install.pipe(Effect.forkChild);
yield* Deferred.await(recoveryStarted);
assert.isFalse(yield* updates.isInstallActive);
const overlappingInstall = yield* updates.install;
assert.isFalse(overlappingInstall.accepted);
assert.equal(harness.quitAndInstalls(), 1);
harness.emit("error", new Error("duplicate native installer error"));
yield* flushCallbacks;
assert.deepEqual(harness.installSteps, ["quitAndInstall", "startBackend"]);
yield* Deferred.succeed(releaseRecovery, undefined);
const failedResult = yield* Fiber.join(failedInstall);
assert.equal(failedResult.state.errorContext, "install");
const retry = yield* updates.install;
assert.isTrue(retry.accepted);
assert.equal(harness.quitAndInstalls(), 2);
}),
).pipe(Effect.provide(Layer.merge(TestClock.layer(), harness.layer)));
});
it.effect("recovers when quitAndInstall reports failure through an updater event", () => {
const harness = makeHarness();
return Effect.scoped(
Effect.gen(function* () {
const desktopState = yield* DesktopState.DesktopState;
const updates = yield* DesktopUpdates.DesktopUpdates;
yield* updates.configure;
harness.emit("update-downloaded", { version: "1.2.4" });
yield* flushCallbacks;
yield* updates.install;
assert.deepEqual(harness.installSteps, ["quitAndInstall"]);
harness.emit("error", new Error("native installer refused"));
yield* flushCallbacks;
assert.isFalse(yield* Ref.get(desktopState.quitting));
assert.deepEqual(harness.installSteps, ["quitAndInstall", "startBackend"]);
assert.equal((yield* updates.getState).errorContext, "install");
}),
).pipe(Effect.provide(Layer.merge(TestClock.layer(), harness.layer)));
});
it.effect("rejects a prepared install when the downloaded version changed", () => {
const harness = makeHarness();
return Effect.scoped(
Effect.gen(function* () {
const updates = yield* DesktopUpdates.DesktopUpdates;
yield* updates.configure;
harness.emit("update-downloaded", { version: "1.2.5" });
yield* flushCallbacks;
const result = yield* updates.installPrepared("1.2.4");
assert.isFalse(result.accepted);
assert.equal(harness.quitAndInstalls(), 0);
}),
).pipe(Effect.provide(Layer.merge(TestClock.layer(), harness.layer)));
});
it.effect("persists channel changes through the settings service", () => {
const harness = makeHarness();
return Effect.scoped(
Effect.gen(function* () {
const settings = yield* DesktopAppSettings.DesktopAppSettings;
const updates = yield* DesktopUpdates.DesktopUpdates;
yield* updates.configure;
const state = yield* updates.setChannel("nightly");
const persistedSettings = yield* settings.get;
assert.equal(state.channel, "nightly");
assert.equal(persistedSettings.updateChannel, "nightly");
assert.equal(persistedSettings.updateChannelConfiguredByUser, true);
}),
).pipe(Effect.provide(Layer.merge(TestClock.layer(), harness.layer)));
});
it.effect("does not persist an unchanged update channel as a user preference", () => {
const harness = makeHarness();
return Effect.scoped(
Effect.gen(function* () {
const settings = yield* DesktopAppSettings.DesktopAppSettings;
const updates = yield* DesktopUpdates.DesktopUpdates;
yield* updates.configure;
const state = yield* updates.setChannel("latest");
const persistedSettings = yield* settings.get;
assert.equal(state.channel, "latest");
assert.equal(persistedSettings.updateChannel, "latest");
assert.equal(persistedSettings.updateChannelConfiguredByUser, false);
}),
).pipe(Effect.provide(Layer.merge(TestClock.layer(), harness.layer)));
});
it.effect("fails channel changes with a typed error while a check is in progress", () =>
Effect.gen(function* () {
const checkStarted = yield* Deferred.make();
const releaseCheck = yield* Deferred.make();
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;
const checkFiber = yield* updates.check("manual").pipe(Effect.forkScoped);
yield* Deferred.await(checkStarted);
const exit = yield* Effect.exit(updates.setChannel("nightly"));
assert.equal(exit._tag, "Failure");
if (exit._tag === "Failure") {
const error = Cause.squash(exit.cause);
assert.instanceOf(error, DesktopUpdates.DesktopUpdateActionInProgressError);
assert.equal(error.action, "check");
assert.equal(error.requestedChannel, "nightly");
}
yield* Deferred.succeed(releaseCheck, undefined);
yield* Fiber.join(checkFiber);
}),
).pipe(Effect.provide(Layer.merge(TestClock.layer(), harness.layer)));
}),
);
it.effect("rejects checks while an update channel change is being persisted", () =>
Effect.gen(function* () {
const channelChangeStarted = yield* Deferred.make();
const releaseChannelChange = yield* Deferred.make();
const harness = makeHarness({
beforeSetUpdateChannel: Deferred.succeed(channelChangeStarted, undefined).pipe(
Effect.andThen(Deferred.await(releaseChannelChange)),
),
});
yield* Effect.scoped(
Effect.gen(function* () {
const updates = yield* DesktopUpdates.DesktopUpdates;
yield* updates.configure;
const channelFiber = yield* updates.setChannel("nightly").pipe(Effect.forkScoped);
yield* Deferred.await(channelChangeStarted);
const checkResult = yield* updates.check("manual");
assert.isFalse(checkResult.checked);
assert.equal(harness.checkCount(), 0);
yield* Deferred.succeed(releaseChannelChange, undefined);
const state = yield* Fiber.join(channelFiber);
assert.equal(state.channel, "nightly");
assert.equal(harness.checkCount(), 1);
}),
).pipe(Effect.provide(Layer.merge(TestClock.layer(), harness.layer)));
}),
);
it.effect("preserves settings failure context when an update channel cannot be persisted", () => {
const diskFailure = new Error("disk exploded");
const settingsFailure = new DesktopAppSettings.DesktopSettingsWriteError({
operation: "replace-settings-file",
path: "/tmp/settings.json",
cause: diskFailure,
});
const harness = makeHarness({ setUpdateChannelError: settingsFailure });
return Effect.scoped(
Effect.gen(function* () {
const updates = yield* DesktopUpdates.DesktopUpdates;
yield* updates.configure;
const error = yield* updates.setChannel("nightly").pipe(Effect.flip);
assert.instanceOf(error, DesktopUpdates.DesktopUpdateChannelPersistenceError);
assert.equal(error.channel, "nightly");
assert.strictEqual(error.cause, settingsFailure);
assert.strictEqual(error.cause.cause, diskFailure);
assert.equal(error.message, "Failed to persist the nightly desktop update channel.");
assert.notInclude(error.message, diskFailure.message);
const checkResult = yield* updates.check("manual");
assert.isTrue(checkResult.checked);
assert.equal(harness.checkCount(), 1);
}),
).pipe(Effect.provide(Layer.merge(TestClock.layer(), harness.layer)));
});
});