Files
fluxer/fluxer_desktop/src/main/DesktopUpdateRun.test.mjs
T

300 lines
10 KiB
JavaScript

// SPDX-License-Identifier: AGPL-3.0-or-later
import assert from 'node:assert/strict';
import {describe, test} from 'node:test';
import {installElectronStub} from './LocalAppTestSupport.test.mjs';
installElectronStub();
const {DesktopUpdateRun} = await import('@electron/main/DesktopUpdateRun');
const LAUNCH_ATTEMPT = Object.freeze({committed: {fluxer_renderer: 'b'.repeat(64)}});
function harness({
probes = [{shellLatestVersion: '2026.1008.2', shellNewer: false, modulesChanged: true}],
canSelfUpdateShell = true,
shellResults = [],
installFails = false,
settled = true,
now = () => 0,
} = {}) {
const steps = [];
const published = [];
const failures = [];
let probeIndex = 0;
let shellIndex = 0;
const quiet = () => undefined;
const run = new DesktopUpdateRun({
probe: async () => {
const next = probes[Math.min(probeIndex, probes.length - 1)];
probeIndex += 1;
steps.push('probe');
if (next instanceof Error) throw next;
return next;
},
canSelfUpdateShell,
runShellSelfUpdate: async () => {
steps.push('shell-update');
const result = shellResults[Math.min(shellIndex, shellResults.length - 1)];
shellIndex += 1;
return result;
},
installModules: async () => {
steps.push('install');
if (installFails) throw new Error('disk full');
return LAUNCH_ATTEMPT;
},
takeover: {
begin: async () => {
steps.push('takeover');
},
restore: () => {
steps.push('restore');
},
closeApp: async () => {
steps.push('close');
},
reopen: (launchAttempt) => {
steps.push(launchAttempt == null ? 'reopen' : 'reopen-updated');
},
reloadInPlace: async () => {
steps.push('reload-in-place');
},
},
publish: (check) => {
published.push(check);
},
openDownloadsPage: async () => {
steps.push('downloads-page');
},
reportFailure: (failure) => {
failures.push(failure);
},
logger: {info: quiet, warn: quiet, error: quiet},
now,
});
if (settled) run.markLaunchSettled();
return {run, steps, published, failures};
}
describe('DesktopUpdateRun', () => {
test('the background check only looks, it never downloads, closes or installs anything', async () => {
const {run, steps, published} = harness();
assert.deepEqual(await run.check(), {shellNewer: false, modulesChanged: true});
assert.deepEqual(steps, ['probe']);
assert.deepEqual(published, []);
});
test('a click takes the window over, checks, and only then closes the app and installs', async () => {
const {run, steps, published} = harness();
await run.start();
assert.deepEqual(steps, ['probe', 'takeover', 'close', 'install', 'reopen-updated']);
assert.deepEqual(published, [{shellNewer: false, modulesChanged: false}]);
});
test('a click while offline gives the same windows back instead of tearing the app down', async () => {
const {run, steps, published} = harness({probes: [new Error('offline')]});
await run.start();
assert.deepEqual(steps, ['probe', 'takeover', 'restore']);
assert.deepEqual(published, [], 'the icon stays so the next click can try again');
});
test('a click after the update was withdrawn gives the windows back and hides the icon', async () => {
const {run, steps, published} = harness({
probes: [{shellLatestVersion: '2026.1008.1', shellNewer: false, modulesChanged: false}],
});
await run.start();
assert.deepEqual(steps, ['probe', 'takeover', 'restore']);
assert.deepEqual(published, [{shellNewer: false, modulesChanged: false}]);
});
test('a shell update runs before any module download, and modules install only when the shell did not restart', async () => {
const {run, steps} = harness({
probes: [{shellLatestVersion: '2026.1008.2', shellNewer: true, modulesChanged: true}],
shellResults: [{reason: 'install-failed', detail: 'nope'}],
});
await run.start();
assert.deepEqual(steps, ['probe', 'takeover', 'close', 'shell-update', 'install', 'reopen-updated']);
});
test('a shell feed that lags the manifest stops offering the shell until the manifest moves on', async () => {
const lagging = {shellLatestVersion: '2026.1008.2', shellNewer: true, modulesChanged: false};
const {run, steps, published} = harness({
probes: [lagging, lagging, {...lagging, shellLatestVersion: '2026.1008.3'}],
shellResults: [{reason: 'no-update', detail: null}],
});
await run.start();
assert.deepEqual(published, [{shellNewer: false, modulesChanged: false}]);
assert.deepEqual(
await run.check(),
{shellNewer: false, modulesChanged: false},
'the poll no longer shows the icon',
);
assert.deepEqual(await run.check(), {shellNewer: true, modulesChanged: false}, 'a newer manifest shell does');
assert.deepEqual(steps, ['probe', 'takeover', 'close', 'shell-update', 'reopen', 'probe', 'probe']);
});
test('a manual check offers the shell again even after the platform feed said it had nothing', async () => {
const lagging = {shellLatestVersion: '2026.1012.154032', shellNewer: true, modulesChanged: false};
const {run} = harness({probes: [lagging], shellResults: [{reason: 'no-update', detail: null}]});
await run.start();
assert.deepEqual(await run.check(), {shellNewer: false, modulesChanged: false});
assert.deepEqual(
await run.check({manual: true}),
{shellNewer: true, modulesChanged: false},
'one stale answer from the platform feed must not make Check for updates say up to date for the rest of the session',
);
assert.deepEqual(await run.check(), {shellNewer: true, modulesChanged: false});
});
test('the background check offers a lagging shell again once the platform feed had time to catch up', async () => {
let clock = 0;
const lagging = {shellLatestVersion: '2026.1012.154032', shellNewer: true, modulesChanged: false};
const {run} = harness({
probes: [lagging],
shellResults: [{reason: 'no-update', detail: null}],
now: () => clock,
});
await run.start();
clock = 9 * 60_000;
assert.deepEqual(await run.check(), {shellNewer: false, modulesChanged: false});
clock = 10 * 60_000;
assert.deepEqual(await run.check(), {shellNewer: true, modulesChanged: false});
});
test('a shell update that failed on the network is retried by the next click', async () => {
const shell = {shellLatestVersion: '2026.1008.2', shellNewer: true, modulesChanged: false};
const {run, steps, published} = harness({
probes: [shell],
shellResults: [
{reason: 'timed-out', detail: 'slow link'},
{reason: 'check-failed', detail: 'offline'},
],
});
await run.start();
await run.start();
assert.deepEqual(published, [
{shellNewer: true, modulesChanged: false},
{shellNewer: true, modulesChanged: false},
]);
assert.equal(steps.filter((step) => step === 'shell-update').length, 2);
assert.equal(steps.includes('downloads-page'), false);
});
test('only a shell update that failed to install falls back to the downloads page', async () => {
const shell = {shellLatestVersion: '2026.1008.2', shellNewer: true, modulesChanged: false};
const {run, steps} = harness({probes: [shell], shellResults: [{reason: 'install-failed', detail: 'apply'}]});
await run.start();
steps.length = 0;
await run.start();
assert.deepEqual(steps, ['downloads-page']);
});
test('a shell that cannot update itself never offers the shell part', async () => {
const {run} = harness({
canSelfUpdateShell: false,
probes: [{shellLatestVersion: '2026.1008.2', shellNewer: true, modulesChanged: false}],
});
assert.deepEqual(await run.check(), {shellNewer: false, modulesChanged: false});
});
test('a failed module install reopens the app on the installed set and keeps the icon', async () => {
const {run, steps, published} = harness({installFails: true});
await run.start();
assert.deepEqual(steps, ['probe', 'takeover', 'close', 'install', 'reopen']);
assert.deepEqual(published, [{shellNewer: false, modulesChanged: true}]);
});
test('a click before the renderer confirmed its launch does nothing', async () => {
const {run, steps} = harness({settled: false});
await run.start();
assert.deepEqual(steps, []);
});
});
describe('DesktopUpdateRun installing a renderer only update in place', () => {
const MODULES_ONLY = {shellLatestVersion: '2026.1008.2', shellNewer: false, modulesChanged: true};
const SHELL_TOO = {shellLatestVersion: '2026.1008.3', shellNewer: true, modulesChanged: true};
test('a click after the background check found only modules never shows the splash or closes a window', async () => {
const {run, steps, published, failures} = harness({probes: [MODULES_ONLY]});
await run.check();
await run.start();
assert.deepEqual(steps, ['probe', 'probe', 'install', 'reload-in-place']);
assert.deepEqual(published, [{shellNewer: false, modulesChanged: false}]);
assert.deepEqual(failures, []);
});
test('a shell that turned newer since the background check still updates behind the splash', async () => {
const {run, steps} = harness({
probes: [MODULES_ONLY, SHELL_TOO],
shellResults: [{reason: 'no-update', detail: null}],
});
await run.check();
await run.start();
assert.deepEqual(steps, ['probe', 'probe', 'takeover', 'close', 'shell-update', 'install', 'reopen-updated']);
});
test('a download that fails leaves the running app alone and says so', async () => {
const {run, steps, published, failures} = harness({probes: [MODULES_ONLY], installFails: true});
await run.check();
await run.start();
assert.deepEqual(steps, ['probe', 'probe', 'install']);
assert.deepEqual(published, [{shellNewer: false, modulesChanged: true}]);
assert.deepEqual(failures, [{reason: 'download-failed', detail: 'disk full'}]);
});
test('a click while offline changes nothing on screen', async () => {
const {run, steps, failures} = harness({probes: [MODULES_ONLY, new Error('offline')]});
await run.check();
await run.start();
assert.deepEqual(steps, ['probe', 'probe']);
assert.deepEqual(failures, [{reason: 'check-failed', detail: 'offline'}]);
});
test('a check works from the first window, a click only after the renderer confirmed its launch', async () => {
const {run, steps} = harness({probes: [MODULES_ONLY], settled: false});
assert.deepEqual(await run.check(), {shellNewer: false, modulesChanged: true});
await run.start();
assert.deepEqual(steps, ['probe']);
run.markLaunchSettled();
await run.start();
assert.deepEqual(steps, ['probe', 'probe', 'install', 'reload-in-place']);
});
});