mirror of
https://github.com/fluxerapp/fluxer
synced 2026-10-18 05:25:40 +09:00
feat(desktop): bundled renderer, modules and instance accounts (#3254)
This commit is contained in:
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);
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user