feat(desktop): bundled renderer, modules and instance accounts (#3254)

This commit is contained in:
Hampus authored and GitHub committed 2026-10-07 16:48:07 +02:00
1 parent c748c8af4e
commit 7c5fa2180a
1275 files changed
+215074 -81246

No files matched your search

@@ -0,0 +1,709 @@
// 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) {
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: SHELL_VERSION,
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} = {},
) {
return Buffer.from(
JSON.stringify({
manifest_version: 1,
release_channel: RELEASE_CHANNEL,
platform: 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: PLATFORM,
arch: ARCH,
packageOrigin: PACKAGE_ORIGIN,
hasOfflineRenderer: false,
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 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.refreshInstalledModules', () => {
test('an activated set that never reaches the renderer strands the poll until the launch is abandoned', async () => {
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});
installModuleOnDisk(storeRoot, 'fluxer_renderer', NEXT_RENDERER_SHA, [['index.js', 'renderer-two']]);
seedCachedPackage(store, NEXT_RENDERER_SHA);
const {updater, requests} = createUpdater(store, {fluxer_renderer: NEXT_RENDERER_SHA});
const activated = await updater.refreshInstalledModules();
assert.equal(activated.status, 'activated');
assert.deepEqual(activated.modules, ['fluxer_renderer']);
assert.equal(requests.length, 1);
assert.equal((await updater.refreshInstalledModules()).status, 'awaiting-renderer');
assert.equal(requests.length, 1, 'a pending launch short circuits the poll before it even reaches the manifest');
updater.abandonPendingLaunch(activated.launchAttempt);
assert.equal((await updater.refreshInstalledModules()).status, 'unchanged');
assert.equal(requests.length, 2, 'abandoning the launch the renderer never took must let the poll run again');
});
test('keeps a module installed on demand while the poll was downloading', async () => {
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});
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 activated = await updater.refreshInstalledModules();
assert.equal(activated.status, 'activated');
assert.deepEqual(activated.launchAttempt.committed, {
fluxer_overlay: OVERLAY_SHA,
fluxer_renderer: NEXT_RENDERER_SHA,
});
assert.deepEqual(store.getCommitted(), {fluxer_overlay: OVERLAY_SHA, fluxer_renderer: NEXT_RENDERER_SHA});
});
test('abandoning an attempt that is not the pending one leaves the pending launch alone', async () => {
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});
installModuleOnDisk(storeRoot, 'fluxer_renderer', NEXT_RENDERER_SHA, [['index.js', 'renderer-two']]);
seedCachedPackage(store, NEXT_RENDERER_SHA);
const {updater} = createUpdater(store, {fluxer_renderer: NEXT_RENDERER_SHA});
const activated = await updater.refreshInstalledModules();
assert.equal(activated.status, 'activated');
updater.abandonPendingLaunch({...activated.launchAttempt});
assert.equal((await updater.refreshInstalledModules()).status, 'awaiting-renderer');
});
test('a module the manifest advertises but the CDN never got does not strand the rest of the poll', 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 activated = await updater.refreshInstalledModules();
assert.equal(activated.status, 'activated');
assert.deepEqual(activated.modules, ['fluxer_renderer']);
assert.deepEqual(store.getCommitted(), {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),
},
);
const activated = await updater.refreshInstalledModules();
assert.equal(activated.status, 'activated');
assert.deepEqual(activated.modules, ['fluxer_renderer']);
assert.deepEqual(store.getCommitted(), {fluxer_renderer: NEXT_RENDERER_SHA});
});
test('a package failure with no deterministic cause still abandons the whole poll', 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.refreshInstalledModules(), /failed to download module fluxer_renderer/u);
assert.deepEqual(store.getCommitted(), {fluxer_overlay: OVERLAY_SHA, fluxer_renderer: RENDERER_SHA});
});
test('a poll 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.refreshInstalledModules(), /no space left on device/u);
assert.deepEqual(store.getCommitted(), {fluxer_overlay: OVERLAY_SHA, fluxer_renderer: RENDERER_SHA});
});
});
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);
});
});