mirror of
https://github.com/fluxerapp/fluxer
synced 2026-10-17 13:05:32 +09:00
1072 lines
40 KiB
JavaScript
1072 lines
40 KiB
JavaScript
// SPDX-License-Identifier: AGPL-3.0-or-later
|
|
|
|
import assert from 'node:assert/strict';
|
|
import {Buffer} from 'node:buffer';
|
|
import {createHash} from 'node:crypto';
|
|
import {mkdirSync, mkdtempSync, writeFileSync} from 'node:fs';
|
|
import {rm} from 'node:fs/promises';
|
|
import os from 'node:os';
|
|
import path from 'node:path';
|
|
import {after, describe, test} from 'node:test';
|
|
import './LocalAppTestSupport.test.mjs';
|
|
|
|
const {ModuleStore, getModuleStoreRoot} = await import('@electron/main/ModuleStore');
|
|
const {ModuleUpdater} = await import('@electron/main/ModuleUpdater');
|
|
|
|
const SHELL_VERSION = '2026.823.1';
|
|
const RELEASE_CHANNEL = 'canary';
|
|
const PLATFORM = 'darwin';
|
|
const ARCH = 'arm64';
|
|
const PACKAGE_ORIGIN = 'https://pkgs.invalid';
|
|
const SOURCE_SHA = 'a'.repeat(40);
|
|
const NOW = Date.parse('2026-08-23T12:00:00.000Z');
|
|
|
|
const temporaryRoots = [];
|
|
|
|
after(async () => {
|
|
for (const root of temporaryRoots) {
|
|
await rm(root, {recursive: true, force: true});
|
|
}
|
|
});
|
|
|
|
function sha256(data) {
|
|
return createHash('sha256').update(data).digest('hex');
|
|
}
|
|
|
|
function createUserData() {
|
|
const root = mkdtempSync(path.join(os.tmpdir(), 'module-updater-'));
|
|
temporaryRoots.push(root);
|
|
return root;
|
|
}
|
|
|
|
function installModuleOnDisk(storeRoot, moduleName, digest, entries, buildVersion = SHELL_VERSION) {
|
|
const directory = path.join(storeRoot, 'store', moduleName, digest);
|
|
mkdirSync(directory, {recursive: true});
|
|
const files = entries.map(([relative, body]) => {
|
|
writeFileSync(path.join(directory, relative), body);
|
|
return {path: relative, sha256: sha256(Buffer.from(body, 'utf8')), bytes: Buffer.byteLength(body, 'utf8')};
|
|
});
|
|
writeFileSync(
|
|
path.join(directory, 'module.json'),
|
|
`${JSON.stringify(
|
|
{
|
|
module: moduleName,
|
|
build_version: buildVersion,
|
|
release_channel: RELEASE_CHANNEL,
|
|
source_sha: SOURCE_SHA,
|
|
files,
|
|
},
|
|
null,
|
|
'\t',
|
|
)}\n`,
|
|
);
|
|
return directory;
|
|
}
|
|
|
|
function manifestBytes(
|
|
modules,
|
|
{
|
|
shellLatest = SHELL_VERSION,
|
|
shellMinimum = SHELL_VERSION,
|
|
required = null,
|
|
bounds = {},
|
|
metadataVersion = 1,
|
|
platform = PLATFORM,
|
|
} = {},
|
|
) {
|
|
return Buffer.from(
|
|
JSON.stringify({
|
|
manifest_version: 1,
|
|
release_channel: RELEASE_CHANNEL,
|
|
platform,
|
|
arch: ARCH,
|
|
build_version: SHELL_VERSION,
|
|
pub_date: '2026-08-23T00:00:00.000Z',
|
|
metadata_version: metadataVersion,
|
|
shell: {latest_version: shellLatest, minimum_version: shellMinimum},
|
|
modules: Object.fromEntries(
|
|
Object.entries(modules).map(([moduleName, digest]) => [
|
|
moduleName,
|
|
{
|
|
sha256: digest,
|
|
bytes: 1024,
|
|
url: `${PACKAGE_ORIGIN}/desktop/${RELEASE_CHANNEL}/modules/${moduleName}/${digest}/package.br`,
|
|
...(bounds[moduleName] ?? {}),
|
|
},
|
|
]),
|
|
),
|
|
required_modules: required ?? Object.keys(modules),
|
|
}),
|
|
'utf8',
|
|
);
|
|
}
|
|
|
|
async function openStore(userDataPath) {
|
|
return await ModuleStore.open({
|
|
root: getModuleStoreRoot(userDataPath),
|
|
shellVersion: SHELL_VERSION,
|
|
releaseChannel: RELEASE_CHANNEL,
|
|
});
|
|
}
|
|
|
|
function createUpdater(store, modules, options = {}) {
|
|
const bytes = manifestBytes(modules, options);
|
|
const requests = [];
|
|
const sleeps = [];
|
|
const reports = [];
|
|
const updater = new ModuleUpdater({
|
|
store,
|
|
shellVersion: SHELL_VERSION,
|
|
releaseChannel: RELEASE_CHANNEL,
|
|
platform: options.platform ?? PLATFORM,
|
|
arch: ARCH,
|
|
packageOrigin: PACKAGE_ORIGIN,
|
|
hasOfflineRenderer: options.bundledRendererVersion != null,
|
|
bundledRendererVersion: options.bundledRendererVersion ?? null,
|
|
forceStartupUpdate: options.forceStartupUpdate ?? false,
|
|
selfUpdateShellFirst: options.selfUpdateShellFirst,
|
|
onState: options.onState,
|
|
fetch: async (url, init) => {
|
|
requests.push(url);
|
|
const response = options.respond == null ? null : await options.respond(url, init);
|
|
return response ?? new Response(bytes, {status: 200, headers: {etag: '"manifest-1"'}});
|
|
},
|
|
sleep: async (ms) => {
|
|
sleeps.push(ms);
|
|
},
|
|
random: () => 0,
|
|
now: () => NOW,
|
|
report: (report) => {
|
|
reports.push(report);
|
|
},
|
|
});
|
|
return {updater, requests, sleeps, reports};
|
|
}
|
|
|
|
function failNthInstallationCheck(store, nth, failure) {
|
|
let checks = 0;
|
|
return new Proxy(store, {
|
|
get(target, property) {
|
|
const value = Reflect.get(target, property, target);
|
|
if (typeof value !== 'function') {
|
|
return value;
|
|
}
|
|
if (property !== 'isInstalled') {
|
|
return value.bind(target);
|
|
}
|
|
return async (moduleName, digest) => {
|
|
checks += 1;
|
|
if (checks === nth) {
|
|
throw failure;
|
|
}
|
|
return await value.call(target, moduleName, digest);
|
|
};
|
|
},
|
|
});
|
|
}
|
|
|
|
const RENDERER_SHA = sha256('renderer-one');
|
|
const NEXT_RENDERER_SHA = sha256('renderer-two');
|
|
const OVERLAY_SHA = sha256('overlay-one');
|
|
const NEXT_OVERLAY_SHA = sha256('overlay-two');
|
|
|
|
function failInstallModule(store, failure) {
|
|
return new Proxy(store, {
|
|
get(target, property) {
|
|
const value = Reflect.get(target, property, target);
|
|
if (typeof value !== 'function') {
|
|
return value;
|
|
}
|
|
if (property !== 'installModule') {
|
|
return value.bind(target);
|
|
}
|
|
return async () => {
|
|
throw failure;
|
|
};
|
|
},
|
|
});
|
|
}
|
|
|
|
function failInstallModuleFor(store, moduleName, failure) {
|
|
return new Proxy(store, {
|
|
get(target, property) {
|
|
const value = Reflect.get(target, property, target);
|
|
if (typeof value !== 'function') {
|
|
return value;
|
|
}
|
|
if (property !== 'installModule') {
|
|
return value.bind(target);
|
|
}
|
|
return async (request) => {
|
|
if (request.module === moduleName) {
|
|
throw failure;
|
|
}
|
|
return await value.call(target, request);
|
|
};
|
|
},
|
|
});
|
|
}
|
|
|
|
function mergeDuringInstall(store, entries) {
|
|
let merged = false;
|
|
return new Proxy(store, {
|
|
get(target, property) {
|
|
const value = Reflect.get(target, property, target);
|
|
if (typeof value !== 'function') {
|
|
return value;
|
|
}
|
|
if (property !== 'installModule') {
|
|
return value.bind(target);
|
|
}
|
|
return async (request) => {
|
|
if (!merged) {
|
|
merged = true;
|
|
await target.mergeCommitted(entries);
|
|
}
|
|
return await value.call(target, request);
|
|
};
|
|
},
|
|
});
|
|
}
|
|
|
|
function recordInstalls(store, installs) {
|
|
return new Proxy(store, {
|
|
get(target, property) {
|
|
const value = Reflect.get(target, property, target);
|
|
if (typeof value !== 'function') {
|
|
return value;
|
|
}
|
|
if (property !== 'installModule') {
|
|
return value.bind(target);
|
|
}
|
|
return async (request) => {
|
|
installs.push(request.module);
|
|
return await value.call(target, request);
|
|
};
|
|
},
|
|
});
|
|
}
|
|
|
|
function unreachableFeed() {
|
|
return async () => {
|
|
throw new Error('module feed is unreachable');
|
|
};
|
|
}
|
|
|
|
function missingPackages() {
|
|
return async (url) => (url.endsWith('.br') ? new Response('', {status: 404}) : null);
|
|
}
|
|
|
|
function seedCachedPackage(store, digest) {
|
|
const packagePath = store.getPackagePath(digest);
|
|
mkdirSync(path.dirname(packagePath), {recursive: true});
|
|
writeFileSync(packagePath, 'cached');
|
|
}
|
|
|
|
describe('ModuleUpdater.run', () => {
|
|
test('launches the committed set when the manifest asks for nothing new', async () => {
|
|
const userDataPath = createUserData();
|
|
const store = await openStore(userDataPath);
|
|
installModuleOnDisk(getModuleStoreRoot(userDataPath), 'fluxer_renderer', RENDERER_SHA, [['index.js', 'renderer']]);
|
|
await store.commit({fluxer_renderer: RENDERER_SHA});
|
|
const {updater, requests, sleeps} = createUpdater(store, {fluxer_renderer: RENDERER_SHA});
|
|
|
|
const outcome = await updater.run();
|
|
|
|
assert.equal(outcome.status, 'launching');
|
|
assert.deepEqual(outcome.committed, {fluxer_renderer: RENDERER_SHA});
|
|
assert.equal(outcome.rolledBack, false);
|
|
assert.deepEqual(outcome.belowFloor, []);
|
|
assert.equal(Object.hasOwn(outcome, 'installed'), false);
|
|
assert.equal(requests.length, 1);
|
|
assert.deepEqual(sleeps, []);
|
|
assert.deepEqual(store.getState().floor, {fluxer_renderer: RENDERER_SHA});
|
|
assert.equal(store.getState().boot_attempt, 1);
|
|
});
|
|
|
|
test('a store failure at the floor check is retried instead of escaping run', async () => {
|
|
const userDataPath = createUserData();
|
|
const store = await openStore(userDataPath);
|
|
installModuleOnDisk(getModuleStoreRoot(userDataPath), 'fluxer_renderer', RENDERER_SHA, [['index.js', 'renderer']]);
|
|
await store.commit({fluxer_renderer: RENDERER_SHA});
|
|
const failure = Object.assign(new Error('failed to inspect installed module path'), {
|
|
name: 'ModuleStoreIOError',
|
|
});
|
|
const {updater, sleeps, reports} = createUpdater(failNthInstallationCheck(store, 2, failure), {
|
|
fluxer_renderer: RENDERER_SHA,
|
|
});
|
|
|
|
const outcome = await updater.run();
|
|
|
|
assert.equal(outcome.status, 'launching');
|
|
assert.deepEqual(outcome.committed, {fluxer_renderer: RENDERER_SHA});
|
|
assert.deepEqual(sleeps, [500]);
|
|
assert.deepEqual(
|
|
reports.map((report) => [report.type, report.message]),
|
|
[['network-error', failure.message]],
|
|
);
|
|
});
|
|
|
|
test('a manifest that no longer carries the committed module blocks instead of launching nothing', async () => {
|
|
const userDataPath = createUserData();
|
|
const store = await openStore(userDataPath);
|
|
installModuleOnDisk(getModuleStoreRoot(userDataPath), 'fluxer_renderer', RENDERER_SHA, [['index.js', 'renderer']]);
|
|
await store.commit({fluxer_renderer: RENDERER_SHA});
|
|
const {updater} = createUpdater(store, {});
|
|
|
|
const outcome = await updater.run();
|
|
|
|
assert.equal(outcome.status, 'blocked-update-required');
|
|
assert.equal(outcome.reason, 'nothing-installed');
|
|
});
|
|
|
|
test('a shell floor above this shell blocks on the shell update instead of installing modules', async () => {
|
|
const userDataPath = createUserData();
|
|
const store = await openStore(userDataPath);
|
|
installModuleOnDisk(getModuleStoreRoot(userDataPath), 'fluxer_renderer', RENDERER_SHA, [['index.js', 'renderer']]);
|
|
await store.commit({fluxer_renderer: RENDERER_SHA});
|
|
const {updater, requests} = createUpdater(
|
|
store,
|
|
{fluxer_renderer: NEXT_RENDERER_SHA},
|
|
{shellLatest: '2026.824.0', shellMinimum: '2026.824.0'},
|
|
);
|
|
|
|
const outcome = await updater.run();
|
|
|
|
assert.equal(outcome.status, 'blocked-shell-update');
|
|
assert.equal(outcome.latestVersion, '2026.824.0');
|
|
assert.equal(outcome.minimumVersion, '2026.824.0');
|
|
assert.equal(outcome.requiredSecurityUpdate, false);
|
|
assert.deepEqual(requests, [`${PACKAGE_ORIGIN}/desktop/${RELEASE_CHANNEL}/${PLATFORM}/${ARCH}/modules.json`]);
|
|
assert.deepEqual(store.getCommitted(), {fluxer_renderer: RENDERER_SHA});
|
|
});
|
|
|
|
test('a required module that outruns this shell blocks on the shell update', async () => {
|
|
const userDataPath = createUserData();
|
|
const store = await openStore(userDataPath);
|
|
installModuleOnDisk(getModuleStoreRoot(userDataPath), 'fluxer_renderer', RENDERER_SHA, [['index.js', 'renderer']]);
|
|
await store.commit({fluxer_renderer: RENDERER_SHA});
|
|
const {updater, requests} = createUpdater(
|
|
store,
|
|
{fluxer_renderer: NEXT_RENDERER_SHA},
|
|
{bounds: {fluxer_renderer: {minimum_shell_version: '2026.824.0'}}},
|
|
);
|
|
|
|
const outcome = await updater.run();
|
|
|
|
assert.equal(outcome.status, 'blocked-shell-update');
|
|
assert.equal(outcome.latestVersion, SHELL_VERSION);
|
|
assert.equal(outcome.minimumVersion, SHELL_VERSION);
|
|
assert.equal(requests.length, 1, 'a shell block must not start downloading packages');
|
|
});
|
|
|
|
test('an optional module this shell has outgrown keeps its committed copy and still launches', async () => {
|
|
const userDataPath = createUserData();
|
|
const storeRoot = getModuleStoreRoot(userDataPath);
|
|
const store = await openStore(userDataPath);
|
|
installModuleOnDisk(storeRoot, 'fluxer_renderer', RENDERER_SHA, [['index.js', 'renderer']]);
|
|
installModuleOnDisk(storeRoot, 'fluxer_overlay', OVERLAY_SHA, [['index.js', 'overlay']]);
|
|
await store.commit({fluxer_overlay: OVERLAY_SHA, fluxer_renderer: RENDERER_SHA});
|
|
const {updater, requests} = createUpdater(
|
|
store,
|
|
{fluxer_overlay: NEXT_OVERLAY_SHA, fluxer_renderer: RENDERER_SHA},
|
|
{required: ['fluxer_renderer'], bounds: {fluxer_overlay: {minimum_shell_version: '2026.824.0'}}},
|
|
);
|
|
|
|
const outcome = await updater.run();
|
|
|
|
assert.equal(outcome.status, 'launching');
|
|
assert.deepEqual(outcome.committed, {fluxer_overlay: OVERLAY_SHA, fluxer_renderer: RENDERER_SHA});
|
|
assert.equal(requests.length, 1, 'a module this shell cannot run must never be downloaded');
|
|
});
|
|
|
|
test('an unreachable feed with no recorded fetch blocks instead of launching whatever is on disk', async () => {
|
|
const userDataPath = createUserData();
|
|
const store = await openStore(userDataPath);
|
|
installModuleOnDisk(getModuleStoreRoot(userDataPath), 'fluxer_renderer', RENDERER_SHA, [['index.js', 'renderer']]);
|
|
await store.commit({fluxer_renderer: RENDERER_SHA});
|
|
const {updater, sleeps} = createUpdater(store, {fluxer_renderer: RENDERER_SHA}, {respond: unreachableFeed()});
|
|
|
|
const outcome = await updater.run();
|
|
|
|
assert.equal(outcome.status, 'blocked-update-required');
|
|
assert.equal(outcome.reason, 'nothing-installed');
|
|
assert.deepEqual(sleeps, [500, 1000, 2000, 4000, 8000]);
|
|
assert.equal(store.getState().boot_attempt, 0);
|
|
});
|
|
|
|
test('an unreachable feed launches the set a previous boot already verified without waiting out the backoff', async () => {
|
|
const userDataPath = createUserData();
|
|
const store = await openStore(userDataPath);
|
|
installModuleOnDisk(getModuleStoreRoot(userDataPath), 'fluxer_renderer', RENDERER_SHA, [['index.js', 'renderer']]);
|
|
await store.commit({fluxer_renderer: RENDERER_SHA});
|
|
assert.equal((await createUpdater(store, {fluxer_renderer: RENDERER_SHA}).updater.run()).status, 'launching');
|
|
const {updater, sleeps} = createUpdater(store, {fluxer_renderer: RENDERER_SHA}, {respond: unreachableFeed()});
|
|
|
|
const outcome = await updater.run();
|
|
|
|
assert.equal(outcome.status, 'unreachable-launch');
|
|
assert.deepEqual(sleeps, []);
|
|
assert.deepEqual(outcome.committed, {fluxer_renderer: RENDERER_SHA});
|
|
assert.equal(outcome.launchAttempt.committed.fluxer_renderer, RENDERER_SHA);
|
|
assert.equal(store.getState().boot_attempt, 2);
|
|
});
|
|
|
|
test('a manifest older than the one already seen launches the verified set without retrying', async () => {
|
|
const userDataPath = createUserData();
|
|
const store = await openStore(userDataPath);
|
|
installModuleOnDisk(getModuleStoreRoot(userDataPath), 'fluxer_renderer', RENDERER_SHA, [['index.js', 'renderer']]);
|
|
await store.commit({fluxer_renderer: RENDERER_SHA});
|
|
const seen = createUpdater(store, {fluxer_renderer: RENDERER_SHA}, {metadataVersion: 5});
|
|
assert.equal((await seen.updater.run()).status, 'launching');
|
|
const {updater, sleeps, reports} = createUpdater(store, {fluxer_renderer: NEXT_RENDERER_SHA}, {metadataVersion: 4});
|
|
|
|
const outcome = await updater.run();
|
|
|
|
assert.equal(outcome.status, 'unreachable-launch');
|
|
assert.deepEqual(outcome.committed, {fluxer_renderer: RENDERER_SHA});
|
|
assert.deepEqual(sleeps, []);
|
|
assert.deepEqual(
|
|
reports.map((report) => report.type),
|
|
['manifest-rollback'],
|
|
);
|
|
});
|
|
|
|
test('a package the server no longer has demotes an optional module to its committed digest', async () => {
|
|
const userDataPath = createUserData();
|
|
const storeRoot = getModuleStoreRoot(userDataPath);
|
|
const store = await openStore(userDataPath);
|
|
installModuleOnDisk(storeRoot, 'fluxer_renderer', RENDERER_SHA, [['index.js', 'renderer']]);
|
|
installModuleOnDisk(storeRoot, 'fluxer_overlay', OVERLAY_SHA, [['index.js', 'overlay']]);
|
|
await store.commit({fluxer_overlay: OVERLAY_SHA, fluxer_renderer: RENDERER_SHA});
|
|
const {updater, reports} = createUpdater(
|
|
store,
|
|
{fluxer_overlay: NEXT_OVERLAY_SHA, fluxer_renderer: RENDERER_SHA},
|
|
{required: ['fluxer_renderer'], respond: missingPackages()},
|
|
);
|
|
|
|
const outcome = await updater.run();
|
|
|
|
assert.equal(outcome.status, 'launching');
|
|
assert.deepEqual(outcome.committed, {fluxer_overlay: OVERLAY_SHA, fluxer_renderer: RENDERER_SHA});
|
|
assert.deepEqual(
|
|
reports.filter((report) => report.type !== 'network-error').map((report) => [report.type, report.module]),
|
|
[['package-missing', 'fluxer_overlay']],
|
|
);
|
|
});
|
|
|
|
test('an install that runs out of disk space reports the storage error and commits nothing', async () => {
|
|
const userDataPath = createUserData();
|
|
const store = await openStore(userDataPath);
|
|
installModuleOnDisk(getModuleStoreRoot(userDataPath), 'fluxer_renderer', RENDERER_SHA, [['index.js', 'renderer']]);
|
|
await store.commit({fluxer_renderer: RENDERER_SHA});
|
|
const failure = Object.assign(new Error('no space left on device'), {code: 'ENOSPC'});
|
|
const {updater, reports} = createUpdater(failInstallModule(store, failure), {
|
|
fluxer_renderer: NEXT_RENDERER_SHA,
|
|
});
|
|
|
|
const outcome = await updater.run();
|
|
|
|
assert.equal(outcome.status, 'blocked-update-required');
|
|
assert.equal(outcome.reason, 'unreachable-below-floor');
|
|
assert.equal(outcome.message, 'no space left to install fluxer_renderer: no space left on device');
|
|
assert.equal(outcome.updateServerUnreachable, false);
|
|
assert.deepEqual(store.getCommitted(), {fluxer_renderer: RENDERER_SHA});
|
|
assert.deepEqual(
|
|
[...new Set(reports.filter((report) => report.type === 'storage-error').map((report) => report.module))],
|
|
['fluxer_renderer', null],
|
|
);
|
|
assert.equal(updater.getLastState().detail, 'Not enough disk space to install the update');
|
|
});
|
|
});
|
|
|
|
describe('ModuleUpdater checks without downloading and installs only when asked', () => {
|
|
async function storeOnRenderer() {
|
|
const userDataPath = createUserData();
|
|
const storeRoot = getModuleStoreRoot(userDataPath);
|
|
const store = await openStore(userDataPath);
|
|
installModuleOnDisk(storeRoot, 'fluxer_renderer', RENDERER_SHA, [['index.js', 'renderer']]);
|
|
await store.commit({fluxer_renderer: RENDERER_SHA});
|
|
return {store, storeRoot};
|
|
}
|
|
|
|
test('a check reads only the manifest and leaves the committed set, the floor and the boot attempt alone', async () => {
|
|
const {store} = await storeOnRenderer();
|
|
const before = store.getState();
|
|
const installs = [];
|
|
const {updater, requests} = createUpdater(recordInstalls(store, installs), {fluxer_renderer: NEXT_RENDERER_SHA});
|
|
|
|
const check = await updater.checkForUpdate();
|
|
|
|
assert.deepEqual(check, {shellLatestVersion: SHELL_VERSION, shellNewer: false, modulesChanged: true});
|
|
assert.deepEqual(
|
|
requests.map((url) => String(url).split('/').pop()),
|
|
['modules.json'],
|
|
);
|
|
assert.deepEqual(installs, []);
|
|
assert.deepEqual(store.getCommitted(), {fluxer_renderer: RENDERER_SHA});
|
|
assert.equal(store.getState().boot_attempt, before.boot_attempt);
|
|
assert.deepEqual(store.getState().floor, before.floor);
|
|
assert.equal(store.getState().last_manifest_fetch, before.last_manifest_fetch);
|
|
});
|
|
|
|
test('a check reports a newer shell and an unchanged module set', async () => {
|
|
const {store} = await storeOnRenderer();
|
|
const {updater} = createUpdater(store, {fluxer_renderer: RENDERER_SHA}, {shellLatest: '2026.824.1'});
|
|
|
|
assert.deepEqual(await updater.checkForUpdate(), {
|
|
shellLatestVersion: '2026.824.1',
|
|
shellNewer: true,
|
|
modulesChanged: false,
|
|
});
|
|
});
|
|
|
|
test('a module that needs the next shell reads as a shell update, not as a module update', async () => {
|
|
const {store} = await storeOnRenderer();
|
|
const {updater} = createUpdater(
|
|
store,
|
|
{fluxer_renderer: NEXT_RENDERER_SHA},
|
|
{shellLatest: '2026.824.1', bounds: {fluxer_renderer: {minimum_shell_version: '2026.824.1'}}},
|
|
);
|
|
|
|
assert.deepEqual(await updater.checkForUpdate(), {
|
|
shellLatestVersion: '2026.824.1',
|
|
shellNewer: true,
|
|
modulesChanged: false,
|
|
});
|
|
});
|
|
|
|
test('the click installs the new set, reports progress and arms a launch attempt for the next window', async () => {
|
|
const {store, storeRoot} = await storeOnRenderer();
|
|
installModuleOnDisk(storeRoot, 'fluxer_renderer', NEXT_RENDERER_SHA, [['index.js', 'renderer-two']]);
|
|
seedCachedPackage(store, NEXT_RENDERER_SHA);
|
|
const statuses = [];
|
|
const {updater} = createUpdater(
|
|
store,
|
|
{fluxer_renderer: NEXT_RENDERER_SHA},
|
|
{onState: (state) => statuses.push(state.status)},
|
|
);
|
|
|
|
const attempt = await updater.installPending();
|
|
|
|
assert.deepEqual(attempt?.committed, {fluxer_renderer: NEXT_RENDERER_SHA});
|
|
assert.deepEqual(store.getCommitted(), {fluxer_renderer: NEXT_RENDERER_SHA});
|
|
assert.equal(store.getState().boot_attempt, 1);
|
|
assert.equal(statuses[0], 'checking');
|
|
assert.equal(statuses.at(-1), 'verifying');
|
|
await updater.markLaunchSucceeded(attempt);
|
|
assert.equal(store.getState().boot_attempt, 0);
|
|
});
|
|
|
|
test('the click keeps a module installed on demand while it was downloading', async () => {
|
|
const {store, storeRoot} = await storeOnRenderer();
|
|
installModuleOnDisk(storeRoot, 'fluxer_renderer', NEXT_RENDERER_SHA, [['index.js', 'renderer-two']]);
|
|
installModuleOnDisk(storeRoot, 'fluxer_overlay', OVERLAY_SHA, [['index.js', 'overlay']]);
|
|
seedCachedPackage(store, NEXT_RENDERER_SHA);
|
|
const {updater} = createUpdater(mergeDuringInstall(store, {fluxer_overlay: OVERLAY_SHA}), {
|
|
fluxer_renderer: NEXT_RENDERER_SHA,
|
|
});
|
|
|
|
const attempt = await updater.installPending();
|
|
|
|
assert.deepEqual(attempt?.committed, {fluxer_overlay: OVERLAY_SHA, fluxer_renderer: NEXT_RENDERER_SHA});
|
|
assert.deepEqual(store.getCommitted(), {fluxer_overlay: OVERLAY_SHA, fluxer_renderer: NEXT_RENDERER_SHA});
|
|
});
|
|
|
|
test('the click installs nothing when the manifest now needs a newer shell', async () => {
|
|
const {store} = await storeOnRenderer();
|
|
const installs = [];
|
|
const {updater} = createUpdater(
|
|
recordInstalls(store, installs),
|
|
{fluxer_renderer: NEXT_RENDERER_SHA},
|
|
{shellLatest: '2026.824.1', shellMinimum: '2026.824.1'},
|
|
);
|
|
|
|
assert.equal(await updater.installPending(), null);
|
|
assert.deepEqual(installs, []);
|
|
assert.deepEqual(store.getCommitted(), {fluxer_renderer: RENDERER_SHA});
|
|
});
|
|
|
|
test('a module the manifest advertises but the CDN never got does not strand the rest of the update', async () => {
|
|
const userDataPath = createUserData();
|
|
const storeRoot = getModuleStoreRoot(userDataPath);
|
|
const store = await openStore(userDataPath);
|
|
installModuleOnDisk(storeRoot, 'fluxer_overlay', OVERLAY_SHA, [['index.js', 'overlay']]);
|
|
installModuleOnDisk(storeRoot, 'fluxer_renderer', RENDERER_SHA, [['index.js', 'renderer']]);
|
|
await store.commit({fluxer_overlay: OVERLAY_SHA, fluxer_renderer: RENDERER_SHA});
|
|
installModuleOnDisk(storeRoot, 'fluxer_renderer', NEXT_RENDERER_SHA, [['index.js', 'renderer-two']]);
|
|
seedCachedPackage(store, NEXT_RENDERER_SHA);
|
|
const {updater, reports} = createUpdater(
|
|
store,
|
|
{fluxer_overlay: NEXT_OVERLAY_SHA, fluxer_renderer: NEXT_RENDERER_SHA},
|
|
{
|
|
required: ['fluxer_renderer'],
|
|
respond: async (url) =>
|
|
url.endsWith(`${NEXT_OVERLAY_SHA}/package.br`) ? new Response('', {status: 404}) : null,
|
|
},
|
|
);
|
|
|
|
const attempt = await updater.installPending();
|
|
|
|
assert.deepEqual(attempt?.committed, {fluxer_overlay: OVERLAY_SHA, fluxer_renderer: NEXT_RENDERER_SHA});
|
|
assert.deepEqual(
|
|
[...new Set(reports.filter((report) => report.type === 'package-missing').map((report) => report.module))],
|
|
['fluxer_overlay'],
|
|
);
|
|
});
|
|
|
|
test('a committed optional module whose tree is gone and whose package 404s still lets the renderer activate', async () => {
|
|
const userDataPath = createUserData();
|
|
const storeRoot = getModuleStoreRoot(userDataPath);
|
|
const store = await openStore(userDataPath);
|
|
installModuleOnDisk(storeRoot, 'fluxer_overlay', OVERLAY_SHA, [['index.js', 'overlay']]);
|
|
installModuleOnDisk(storeRoot, 'fluxer_renderer', RENDERER_SHA, [['index.js', 'renderer']]);
|
|
await store.commit({fluxer_overlay: OVERLAY_SHA, fluxer_renderer: RENDERER_SHA});
|
|
await rm(path.join(storeRoot, 'store', 'fluxer_overlay', OVERLAY_SHA), {recursive: true, force: true});
|
|
installModuleOnDisk(storeRoot, 'fluxer_renderer', NEXT_RENDERER_SHA, [['index.js', 'renderer-two']]);
|
|
seedCachedPackage(store, NEXT_RENDERER_SHA);
|
|
const {updater} = createUpdater(
|
|
store,
|
|
{fluxer_overlay: OVERLAY_SHA, fluxer_renderer: NEXT_RENDERER_SHA},
|
|
{
|
|
required: ['fluxer_renderer'],
|
|
respond: async (url) => (url.endsWith(`${OVERLAY_SHA}/package.br`) ? new Response('', {status: 404}) : null),
|
|
},
|
|
);
|
|
|
|
await updater.installPending();
|
|
|
|
assert.deepEqual(store.getCommitted(), {fluxer_renderer: NEXT_RENDERER_SHA});
|
|
});
|
|
|
|
test('a package failure with no deterministic cause rejects the click and keeps the installed set', async () => {
|
|
const userDataPath = createUserData();
|
|
const storeRoot = getModuleStoreRoot(userDataPath);
|
|
const store = await openStore(userDataPath);
|
|
installModuleOnDisk(storeRoot, 'fluxer_overlay', OVERLAY_SHA, [['index.js', 'overlay']]);
|
|
installModuleOnDisk(storeRoot, 'fluxer_renderer', RENDERER_SHA, [['index.js', 'renderer']]);
|
|
await store.commit({fluxer_overlay: OVERLAY_SHA, fluxer_renderer: RENDERER_SHA});
|
|
installModuleOnDisk(storeRoot, 'fluxer_overlay', NEXT_OVERLAY_SHA, [['index.js', 'overlay-two']]);
|
|
seedCachedPackage(store, NEXT_OVERLAY_SHA);
|
|
const {updater} = createUpdater(
|
|
store,
|
|
{fluxer_overlay: NEXT_OVERLAY_SHA, fluxer_renderer: NEXT_RENDERER_SHA},
|
|
{
|
|
required: ['fluxer_renderer'],
|
|
respond: async (url) =>
|
|
url.endsWith(`${NEXT_RENDERER_SHA}/package.br`) ? new Response('', {status: 503}) : null,
|
|
},
|
|
);
|
|
|
|
await assert.rejects(updater.installPending(), /failed to download module fluxer_renderer/u);
|
|
|
|
assert.deepEqual(store.getCommitted(), {fluxer_overlay: OVERLAY_SHA, fluxer_renderer: RENDERER_SHA});
|
|
});
|
|
|
|
test('a click that runs out of disk space aborts instead of activating the part that fit', async () => {
|
|
const userDataPath = createUserData();
|
|
const storeRoot = getModuleStoreRoot(userDataPath);
|
|
const store = await openStore(userDataPath);
|
|
installModuleOnDisk(storeRoot, 'fluxer_overlay', OVERLAY_SHA, [['index.js', 'overlay']]);
|
|
installModuleOnDisk(storeRoot, 'fluxer_renderer', RENDERER_SHA, [['index.js', 'renderer']]);
|
|
await store.commit({fluxer_overlay: OVERLAY_SHA, fluxer_renderer: RENDERER_SHA});
|
|
installModuleOnDisk(storeRoot, 'fluxer_overlay', NEXT_OVERLAY_SHA, [['index.js', 'overlay-two']]);
|
|
seedCachedPackage(store, NEXT_OVERLAY_SHA);
|
|
const failure = Object.assign(new Error('no space left on device'), {code: 'ENOSPC'});
|
|
const {updater} = createUpdater(
|
|
failInstallModuleFor(store, 'fluxer_renderer', failure),
|
|
{fluxer_overlay: NEXT_OVERLAY_SHA, fluxer_renderer: NEXT_RENDERER_SHA},
|
|
{required: ['fluxer_renderer']},
|
|
);
|
|
|
|
await assert.rejects(updater.installPending(), /no space left on device/u);
|
|
|
|
assert.deepEqual(store.getCommitted(), {fluxer_overlay: OVERLAY_SHA, fluxer_renderer: RENDERER_SHA});
|
|
});
|
|
|
|
test('a boot with a newer shell updates the shell before downloading any module', async () => {
|
|
const {store, storeRoot} = await storeOnRenderer();
|
|
installModuleOnDisk(storeRoot, 'fluxer_renderer', NEXT_RENDERER_SHA, [['index.js', 'renderer-two']]);
|
|
seedCachedPackage(store, NEXT_RENDERER_SHA);
|
|
const steps = [];
|
|
const {updater} = createUpdater(
|
|
recordInstalls(store, steps),
|
|
{fluxer_renderer: NEXT_RENDERER_SHA},
|
|
{
|
|
shellLatest: '2026.824.1',
|
|
forceStartupUpdate: true,
|
|
selfUpdateShellFirst: async (latestVersion) => {
|
|
steps.push(`shell ${latestVersion}`);
|
|
},
|
|
},
|
|
);
|
|
|
|
const outcome = await updater.run();
|
|
|
|
assert.equal(outcome.status, 'launching');
|
|
assert.deepEqual(steps, ['shell 2026.824.1', 'fluxer_renderer']);
|
|
});
|
|
|
|
test('a boot with the newest shell never runs the shell update', async () => {
|
|
const {store, storeRoot} = await storeOnRenderer();
|
|
installModuleOnDisk(storeRoot, 'fluxer_renderer', NEXT_RENDERER_SHA, [['index.js', 'renderer-two']]);
|
|
seedCachedPackage(store, NEXT_RENDERER_SHA);
|
|
const steps = [];
|
|
const {updater} = createUpdater(
|
|
recordInstalls(store, steps),
|
|
{fluxer_renderer: NEXT_RENDERER_SHA},
|
|
{
|
|
forceStartupUpdate: true,
|
|
selfUpdateShellFirst: async (latestVersion) => {
|
|
steps.push(`shell ${latestVersion}`);
|
|
},
|
|
},
|
|
);
|
|
|
|
await updater.run();
|
|
|
|
assert.deepEqual(steps, ['fluxer_renderer']);
|
|
});
|
|
|
|
test('a Linux boot right after a shell update fetches the modules instead of launching the old set', async () => {
|
|
const optional = await storeOnRenderer();
|
|
const quiet = createUpdater(optional.store, {fluxer_renderer: NEXT_RENDERER_SHA}, {platform: 'linux'});
|
|
const skipped = await quiet.updater.run();
|
|
assert.equal(skipped.status, 'launching');
|
|
assert.deepEqual(skipped.committed, {fluxer_renderer: RENDERER_SHA});
|
|
assert.deepEqual(quiet.requests, []);
|
|
|
|
const forced = await storeOnRenderer();
|
|
installModuleOnDisk(forced.storeRoot, 'fluxer_renderer', NEXT_RENDERER_SHA, [['index.js', 'renderer-two']]);
|
|
seedCachedPackage(forced.store, NEXT_RENDERER_SHA);
|
|
const {updater, requests} = createUpdater(
|
|
forced.store,
|
|
{fluxer_renderer: NEXT_RENDERER_SHA},
|
|
{platform: 'linux', forceStartupUpdate: true},
|
|
);
|
|
const outcome = await updater.run();
|
|
|
|
assert.equal(outcome.status, 'launching');
|
|
assert.deepEqual(outcome.committed, {fluxer_renderer: NEXT_RENDERER_SHA});
|
|
assert.ok(requests.some((url) => String(url).endsWith('/modules.json')));
|
|
});
|
|
|
|
test('a Linux boot forced only by a shell change launches the installed set at once when the download fails', async () => {
|
|
const {store} = await storeOnRenderer();
|
|
const {updater, sleeps} = createUpdater(
|
|
store,
|
|
{fluxer_renderer: NEXT_RENDERER_SHA},
|
|
{
|
|
platform: 'linux',
|
|
forceStartupUpdate: true,
|
|
respond: async (url) => (String(url).endsWith('package.br') ? new Response('', {status: 503}) : null),
|
|
},
|
|
);
|
|
|
|
const outcome = await updater.run();
|
|
|
|
assert.equal(outcome.status, 'unreachable-launch');
|
|
assert.deepEqual(outcome.committed, {fluxer_renderer: RENDERER_SHA});
|
|
assert.deepEqual(
|
|
sleeps,
|
|
[],
|
|
'a package or deb upgrade never waits through the backoff for a module it can live without',
|
|
);
|
|
});
|
|
});
|
|
|
|
describe('ModuleUpdater explains why a launch is blocked', () => {
|
|
test('a first launch that cannot reach the feed blocks as unreachable rather than as a missing update', async () => {
|
|
const userDataPath = createUserData();
|
|
const store = await openStore(userDataPath);
|
|
const {updater} = createUpdater(store, {fluxer_renderer: RENDERER_SHA}, {respond: unreachableFeed()});
|
|
|
|
const outcome = await updater.run();
|
|
|
|
assert.equal(outcome.status, 'blocked-update-required');
|
|
assert.equal(outcome.reason, 'nothing-installed');
|
|
assert.equal(outcome.updateServerUnreachable, true);
|
|
});
|
|
|
|
test('a package url the policy refuses names its cause in the block and in a report', async () => {
|
|
const userDataPath = createUserData();
|
|
const store = await openStore(userDataPath);
|
|
const url = `http://192.168.139.107:48780/desktop/canary/modules/fluxer_renderer/${RENDERER_SHA}/package.br`;
|
|
const {updater, reports} = createUpdater(
|
|
store,
|
|
{fluxer_renderer: RENDERER_SHA},
|
|
{bounds: {fluxer_renderer: {url}}},
|
|
);
|
|
|
|
const outcome = await updater.run();
|
|
|
|
assert.equal(outcome.status, 'blocked-update-required');
|
|
assert.equal(outcome.reason, 'required-module-unavailable');
|
|
assert.equal(outcome.updateServerUnreachable, false);
|
|
assert.match(outcome.message, /not https: http:\/\/192\.168\.139\.107/u);
|
|
const rejected = reports.find((report) => report.type === 'package-rejected');
|
|
assert.equal(rejected?.module, 'fluxer_renderer');
|
|
assert.match(rejected?.message ?? '', /192\.168\.139\.107/u);
|
|
const blocked = reports.find((report) => report.type === 'blocked');
|
|
assert.notEqual(blocked?.error, null);
|
|
});
|
|
});
|
|
|
|
describe('ModuleUpdater after a crash-loop rollback', () => {
|
|
async function rollBackFrom(store, modules) {
|
|
for (let launch = 0; launch < 2; launch += 1) {
|
|
assert.equal((await createUpdater(store, modules).updater.run()).status, 'launching');
|
|
}
|
|
const reverted = await createUpdater(store, modules).updater.run();
|
|
assert.equal(reverted.rolledBack, true);
|
|
return reverted;
|
|
}
|
|
|
|
test('the next boot keeps the reverted set instead of reinstalling the build that crashed', async () => {
|
|
const userDataPath = createUserData();
|
|
const storeRoot = getModuleStoreRoot(userDataPath);
|
|
const store = await openStore(userDataPath);
|
|
installModuleOnDisk(storeRoot, 'fluxer_renderer', RENDERER_SHA, [['index.js', 'renderer']]);
|
|
installModuleOnDisk(storeRoot, 'fluxer_renderer', NEXT_RENDERER_SHA, [['index.js', 'renderer two']]);
|
|
await store.commit({fluxer_renderer: RENDERER_SHA});
|
|
await store.commit({fluxer_renderer: NEXT_RENDERER_SHA});
|
|
const modules = {fluxer_renderer: NEXT_RENDERER_SHA};
|
|
await rollBackFrom(store, modules);
|
|
|
|
const {updater, requests} = createUpdater(store, modules);
|
|
const outcome = await updater.run();
|
|
|
|
assert.equal(outcome.status, 'launching');
|
|
assert.equal(outcome.rolledBack, false);
|
|
assert.deepEqual(outcome.committed, {fluxer_renderer: RENDERER_SHA});
|
|
assert.deepEqual(
|
|
requests.filter((url) => String(url).endsWith('.br')),
|
|
[],
|
|
);
|
|
});
|
|
|
|
test('a manifest that moves past the rejected build clears the rejection', async () => {
|
|
const userDataPath = createUserData();
|
|
const storeRoot = getModuleStoreRoot(userDataPath);
|
|
const store = await openStore(userDataPath);
|
|
installModuleOnDisk(storeRoot, 'fluxer_renderer', RENDERER_SHA, [['index.js', 'renderer']]);
|
|
installModuleOnDisk(storeRoot, 'fluxer_renderer', NEXT_RENDERER_SHA, [['index.js', 'renderer two']]);
|
|
await store.commit({fluxer_renderer: RENDERER_SHA});
|
|
await store.commit({fluxer_renderer: NEXT_RENDERER_SHA});
|
|
await rollBackFrom(store, {fluxer_renderer: NEXT_RENDERER_SHA});
|
|
assert.equal(store.isRejected('fluxer_renderer', NEXT_RENDERER_SHA), true);
|
|
|
|
await createUpdater(store, {fluxer_renderer: OVERLAY_SHA}, {metadataVersion: 2, respond: missingPackages()})
|
|
.updater.run()
|
|
.catch(() => undefined);
|
|
|
|
assert.equal(store.isRejected('fluxer_renderer', NEXT_RENDERER_SHA), false);
|
|
});
|
|
});
|
|
|
|
describe('ModuleUpdater with a renderer bundled in the shell', () => {
|
|
const OLDER_VERSION = '2026.822.9';
|
|
|
|
async function storeWithOlderRenderer() {
|
|
const store = await openStore(createUserData());
|
|
installModuleOnDisk(store.root, 'fluxer_renderer', RENDERER_SHA, [['index.html', 'renderer one']], OLDER_VERSION);
|
|
await store.commit({fluxer_renderer: RENDERER_SHA});
|
|
await store.recordManifestFetch({
|
|
etag: null,
|
|
fetchedAt: new Date(NOW).toISOString(),
|
|
manifest: {
|
|
feed: {releaseChannel: RELEASE_CHANNEL, platform: PLATFORM, arch: ARCH},
|
|
metadataVersion: 1,
|
|
manifestSha256: 'c'.repeat(64),
|
|
},
|
|
floor: {fluxer_renderer: NEXT_RENDERER_SHA},
|
|
});
|
|
return store;
|
|
}
|
|
|
|
test('a new shell over an older installed renderer launches the bundle with the feed down', async () => {
|
|
const store = await storeWithOlderRenderer();
|
|
const {updater, reports} = createUpdater(
|
|
store,
|
|
{},
|
|
{bundledRendererVersion: SHELL_VERSION, forceStartupUpdate: true, respond: unreachableFeed()},
|
|
);
|
|
|
|
const outcome = await updater.run();
|
|
|
|
assert.equal(outcome.status, 'unreachable-launch');
|
|
assert.deepEqual(outcome.committed, {});
|
|
assert.deepEqual(store.getCommitted(), {});
|
|
assert.deepEqual(store.getState().previous, {});
|
|
assert.equal(
|
|
reports.some((report) => report.type === 'bundled-renderer-preferred'),
|
|
true,
|
|
);
|
|
assert.deepEqual((await updater.selectServedModules(outcome.committed)).renderer, {
|
|
source: 'bundled',
|
|
version: SHELL_VERSION,
|
|
bundledVersion: SHELL_VERSION,
|
|
});
|
|
});
|
|
|
|
test('a feed renderer the bundle already covers is never downloaded', async () => {
|
|
const store = await storeWithOlderRenderer();
|
|
const {updater, requests} = createUpdater(
|
|
store,
|
|
{fluxer_renderer: NEXT_RENDERER_SHA},
|
|
{bundledRendererVersion: SHELL_VERSION, forceStartupUpdate: true, metadataVersion: 2},
|
|
);
|
|
|
|
const outcome = await updater.run();
|
|
|
|
assert.equal(outcome.status, 'launching');
|
|
assert.deepEqual(outcome.committed, {});
|
|
assert.equal(
|
|
requests.some((url) => String(url).endsWith('package.br')),
|
|
false,
|
|
);
|
|
assert.deepEqual(store.getState().floor, {});
|
|
assert.deepEqual(await updater.checkForUpdate(), {
|
|
shellLatestVersion: SHELL_VERSION,
|
|
shellNewer: false,
|
|
modulesChanged: false,
|
|
});
|
|
});
|
|
|
|
test('a feed renderer newer than the bundle is still fetched', async () => {
|
|
const store = await openStore(createUserData());
|
|
const {updater, requests} = createUpdater(
|
|
store,
|
|
{fluxer_renderer: NEXT_RENDERER_SHA},
|
|
{bundledRendererVersion: OLDER_VERSION, respond: missingPackages()},
|
|
);
|
|
|
|
await updater.run();
|
|
|
|
assert.equal(
|
|
requests.some((url) => String(url).endsWith(`${NEXT_RENDERER_SHA}/package.br`)),
|
|
true,
|
|
);
|
|
});
|
|
|
|
test('a renderer module that keeps failing to boot is dropped for the bundle, never relaunched forever', async () => {
|
|
const store = await openStore(createUserData());
|
|
installModuleOnDisk(store.root, 'fluxer_renderer', NEXT_RENDERER_SHA, [['index.html', 'broken']], '2026.900.1');
|
|
await store.commit({fluxer_renderer: NEXT_RENDERER_SHA});
|
|
const served = [];
|
|
for (let launch = 0; launch < 4; launch += 1) {
|
|
const {updater} = createUpdater(
|
|
store,
|
|
{},
|
|
{bundledRendererVersion: SHELL_VERSION, platform: 'linux', respond: unreachableFeed()},
|
|
);
|
|
const outcome = await updater.run();
|
|
served.push((await updater.selectServedModules(outcome.committed)).renderer.source);
|
|
}
|
|
|
|
assert.deepEqual(served, ['module', 'module', 'bundled', 'bundled']);
|
|
assert.equal(store.isRejected('fluxer_renderer', NEXT_RENDERER_SHA), true);
|
|
});
|
|
|
|
test('a committed on-demand module whose files are gone never blocks an offline launch', async () => {
|
|
for (const platform of ['darwin', 'linux']) {
|
|
const store = await openStore(createUserData());
|
|
const directory = installModuleOnDisk(store.root, 'fluxer_fonts_jp', OVERLAY_SHA, [['a.woff2', 'font']], '0.0.0');
|
|
await store.commit({fluxer_fonts_jp: OVERLAY_SHA});
|
|
await rm(directory, {recursive: true, force: true});
|
|
const {updater} = createUpdater(
|
|
store,
|
|
{},
|
|
{bundledRendererVersion: SHELL_VERSION, platform, respond: unreachableFeed()},
|
|
);
|
|
|
|
const outcome = await updater.run();
|
|
|
|
assert.equal(outcome.status, platform === 'linux' ? 'launching' : 'unreachable-launch', platform);
|
|
assert.deepEqual(outcome.committed, {}, platform);
|
|
}
|
|
});
|
|
|
|
test('a newer feed renderer whose package is missing launches the bundle instead of blocking', async () => {
|
|
const store = await openStore(createUserData());
|
|
const {updater} = createUpdater(
|
|
store,
|
|
{fluxer_renderer: NEXT_RENDERER_SHA},
|
|
{bundledRendererVersion: OLDER_VERSION, respond: missingPackages()},
|
|
);
|
|
|
|
const outcome = await updater.run();
|
|
|
|
assert.equal(outcome.status, 'launching');
|
|
assert.deepEqual(outcome.committed, {});
|
|
assert.deepEqual(await updater.checkForUpdate(), {
|
|
shellLatestVersion: SHELL_VERSION,
|
|
shellNewer: false,
|
|
modulesChanged: true,
|
|
});
|
|
});
|
|
|
|
test('a packaged bundle with no readable version still outranks an installed module', async () => {
|
|
const store = await openStore(createUserData());
|
|
installModuleOnDisk(store.root, 'fluxer_renderer', RENDERER_SHA, [['index.html', 'old']], OLDER_VERSION);
|
|
await store.commit({fluxer_renderer: RENDERER_SHA});
|
|
const updater = new ModuleUpdater({
|
|
store,
|
|
shellVersion: SHELL_VERSION,
|
|
releaseChannel: RELEASE_CHANNEL,
|
|
platform: 'linux',
|
|
arch: ARCH,
|
|
packageOrigin: PACKAGE_ORIGIN,
|
|
hasOfflineRenderer: true,
|
|
bundledRendererVersion: 'dev',
|
|
preferUnversionedBundle: true,
|
|
fetch: unreachableFeed(),
|
|
sleep: async () => {},
|
|
random: () => 0,
|
|
now: () => NOW,
|
|
});
|
|
|
|
const outcome = await updater.run();
|
|
|
|
assert.equal((await updater.selectServedModules(outcome.committed)).renderer.source, 'bundled');
|
|
assert.deepEqual(store.getCommitted(), {});
|
|
});
|
|
|
|
test('a Linux launch never waits on a shell download, a required security update still does', async () => {
|
|
const attempts = [];
|
|
const selfUpdateShellFirst = async (latestVersion) => {
|
|
attempts.push(latestVersion);
|
|
};
|
|
const store = await openStore(createUserData());
|
|
const {updater} = createUpdater(
|
|
store,
|
|
{},
|
|
{
|
|
bundledRendererVersion: SHELL_VERSION,
|
|
platform: 'linux',
|
|
forceStartupUpdate: true,
|
|
shellLatest: '2026.900.1',
|
|
shellMinimum: '0.0.0',
|
|
selfUpdateShellFirst,
|
|
},
|
|
);
|
|
|
|
assert.equal((await updater.run()).status, 'launching');
|
|
assert.deepEqual(attempts, []);
|
|
|
|
const mac = createUpdater(
|
|
await openStore(createUserData()),
|
|
{},
|
|
{
|
|
bundledRendererVersion: SHELL_VERSION,
|
|
forceStartupUpdate: true,
|
|
shellLatest: '2026.900.1',
|
|
shellMinimum: '0.0.0',
|
|
selfUpdateShellFirst,
|
|
},
|
|
);
|
|
await mac.updater.run();
|
|
assert.deepEqual(attempts, ['2026.900.1']);
|
|
});
|
|
});
|