mirror of
https://github.com/fluxerapp/fluxer
synced 2026-10-18 21:45:31 +09:00
feat(desktop): show download progress and diagnostics on splash (#3273)
This commit is contained in:
1 parent
795e190bda
commit
0ef1cd14c3
12 files changed
+667
-18
No files matched your search
@@ -0,0 +1,163 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
import assert from 'node:assert/strict';
|
||||
import {describe, test} from 'node:test';
|
||||
import './LocalAppTestSupport.test.mjs';
|
||||
|
||||
const {ModuleDownloadRate} = await import('@electron/main/ModuleDownloadRate');
|
||||
const {buildSplashDiagnosticsText, redactDiagnosticText} = await import('@electron/main/SplashDiagnostics');
|
||||
const {isModulePackageStall, ModulePackageFetchError, ModulePackageStallError, MODULE_PACKAGE_STALL_TIMEOUT_MS} =
|
||||
await import('@electron/main/ModulePackageInstaller');
|
||||
const {serializeSplashState, SplashStatus} = await import('@electron/main/SplashWindow');
|
||||
|
||||
function diagnosticsInput(overrides = {}) {
|
||||
return {
|
||||
generatedAt: Date.UTC(2026, 9, 8, 1, 0, 0),
|
||||
splashOpenedAt: Date.UTC(2026, 9, 8, 0, 59, 0),
|
||||
appVersion: '2026.1008.1',
|
||||
channel: 'canary',
|
||||
platform: 'win32',
|
||||
arch: 'x64',
|
||||
osVersion: '10.0.26100',
|
||||
electronVersion: '44.4.1',
|
||||
logsPath: 'C:\\Users\\u\\AppData\\Roaming\\fluxer_desktop_canary\\logs',
|
||||
userDataPath: 'C:\\Users\\u\\AppData\\Roaming\\fluxercanary',
|
||||
packageOrigin: 'https://pkgs.fluxer.com',
|
||||
proxyRoute: 'PROXY 10.0.0.2:3128',
|
||||
splashStatus: 'download-stalled',
|
||||
updaterStatus: 'retry-wait',
|
||||
pendingModule: 'fluxer_renderer',
|
||||
receivedBytes: 42000000,
|
||||
totalBytes: 146684915,
|
||||
bytesPerSecond: 3100000,
|
||||
committed: {fluxer_sourcemaps: 'b'.repeat(64), fluxer_renderer: 'a'.repeat(64)},
|
||||
lastError: 'package download stalled for 30000ms',
|
||||
lastErrorAt: Date.UTC(2026, 9, 8, 0, 59, 40),
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
describe('module download rate', () => {
|
||||
test('reports nothing until a second sample and then the transfer rate', () => {
|
||||
const rate = new ModuleDownloadRate();
|
||||
assert.equal(rate.sample(0, 0), null);
|
||||
assert.equal(rate.sample(1_000_000, 1000), 1_000_000);
|
||||
});
|
||||
|
||||
test('smooths a sudden change instead of jumping to it', () => {
|
||||
const rate = new ModuleDownloadRate();
|
||||
rate.sample(0, 0);
|
||||
rate.sample(1_000_000, 1000);
|
||||
const smoothed = rate.sample(1_000_000 + 4_000_000, 2000);
|
||||
assert.ok(smoothed > 1_000_000 && smoothed < 4_000_000, String(smoothed));
|
||||
});
|
||||
|
||||
test('keeps the last rate for samples closer together than the minimum window', () => {
|
||||
const rate = new ModuleDownloadRate();
|
||||
rate.sample(0, 0);
|
||||
const first = rate.sample(1_000_000, 1000);
|
||||
assert.equal(rate.sample(1_100_000, 1050), first);
|
||||
});
|
||||
|
||||
test('starts over when the byte count goes backwards or after a reset', () => {
|
||||
const rate = new ModuleDownloadRate();
|
||||
rate.sample(0, 0);
|
||||
rate.sample(5_000_000, 1000);
|
||||
assert.equal(rate.sample(100, 1500), null);
|
||||
rate.reset();
|
||||
assert.equal(rate.sample(0, 3000), null);
|
||||
});
|
||||
});
|
||||
|
||||
describe('package download stalls', () => {
|
||||
test('a stall is a transient fetch failure the updater can name', () => {
|
||||
const stall = new ModulePackageStallError('fluxer_renderer', MODULE_PACKAGE_STALL_TIMEOUT_MS);
|
||||
assert.ok(stall instanceof ModulePackageFetchError);
|
||||
assert.equal(stall.status, null);
|
||||
assert.equal(isModulePackageStall(new Error('install failed', {cause: stall})), true);
|
||||
assert.equal(isModulePackageStall(new ModulePackageFetchError('fluxer_renderer', 503, 'busy')), false);
|
||||
});
|
||||
|
||||
test('gives up on a silent transfer after thirty seconds', () => {
|
||||
assert.equal(MODULE_PACKAGE_STALL_TIMEOUT_MS, 30000);
|
||||
});
|
||||
});
|
||||
|
||||
describe('splash download state', () => {
|
||||
test('carries byte counts and speed across the IPC boundary as whole numbers', () => {
|
||||
const serialized = serializeSplashState({
|
||||
status: SplashStatus.DOWNLOADING_UPDATES,
|
||||
receivedBytes: 42_000_000.7,
|
||||
totalBytes: 146_684_915,
|
||||
bytesPerSecond: 3_100_000.4,
|
||||
});
|
||||
assert.equal(serialized.receivedBytes, 42_000_000);
|
||||
assert.equal(serialized.totalBytes, 146_684_915);
|
||||
assert.equal(serialized.bytesPerSecond, 3_100_000);
|
||||
});
|
||||
|
||||
test('keeps the stalled status and drops byte values that are not numbers', () => {
|
||||
const serialized = serializeSplashState({
|
||||
status: SplashStatus.DOWNLOAD_STALLED,
|
||||
seconds: 4,
|
||||
receivedBytes: Number.NaN,
|
||||
totalBytes: '9',
|
||||
bytesPerSecond: Number.POSITIVE_INFINITY,
|
||||
});
|
||||
assert.equal(serialized.status, 'download-stalled');
|
||||
assert.equal(serialized.receivedBytes, null);
|
||||
assert.equal(serialized.totalBytes, null);
|
||||
assert.equal(serialized.bytesPerSecond, null);
|
||||
});
|
||||
});
|
||||
|
||||
describe('splash diagnostics', () => {
|
||||
test('lists what support needs to place a stuck download', () => {
|
||||
const text = buildSplashDiagnosticsText(diagnosticsInput());
|
||||
for (const expected of [
|
||||
'App version: 2026.1008.1 (canary)',
|
||||
'Platform: win32 x64, OS 10.0.26100, Electron 44.4.1',
|
||||
'Package origin: https://pkgs.fluxer.com',
|
||||
'Proxy route: PROXY 10.0.0.2:3128',
|
||||
'Splash status: download-stalled',
|
||||
'Pending module: fluxer_renderer',
|
||||
'Downloaded: 42000000 bytes of 146684915 bytes',
|
||||
'Speed: 3100000 bytes/s',
|
||||
'Last updater error: package download stalled for 30000ms',
|
||||
'Last updater error at: 2026-10-08T00:59:40.000Z',
|
||||
' fluxer_renderer aaaaaaaaaaaa',
|
||||
' fluxer_sourcemaps bbbbbbbbbbbb',
|
||||
]) {
|
||||
assert.ok(text.includes(expected), `missing ${expected}\n${text}`);
|
||||
}
|
||||
assert.ok(text.indexOf('fluxer_renderer aaaa') < text.indexOf('fluxer_sourcemaps bbbb'));
|
||||
});
|
||||
|
||||
test('never carries query values or credential-looking pairs', () => {
|
||||
const text = buildSplashDiagnosticsText(
|
||||
diagnosticsInput({
|
||||
lastError: 'failed to reach https://pkgs.fluxer.com/x?token=abc123&sig=zzz: authorization: Bearer secretvalue',
|
||||
proxyRoute: 'PROXY user:pass@proxy:8080',
|
||||
}),
|
||||
);
|
||||
assert.ok(!text.includes('abc123'));
|
||||
assert.ok(!text.includes('zzz'));
|
||||
assert.ok(!text.includes('secretvalue'));
|
||||
assert.ok(!text.includes('user:pass'));
|
||||
assert.ok(text.includes('?token=…&sig=…'));
|
||||
assert.ok(text.includes('Proxy route: PROXY …@proxy:8080'));
|
||||
});
|
||||
|
||||
test('redaction leaves plain diagnostic text alone', () => {
|
||||
assert.equal(
|
||||
redactDiagnosticText('Package origin: https://pkgs.fluxer.com'),
|
||||
'Package origin: https://pkgs.fluxer.com',
|
||||
);
|
||||
});
|
||||
|
||||
test('says so when nothing is installed yet', () => {
|
||||
const text = buildSplashDiagnosticsText(diagnosticsInput({committed: {}, lastError: null, lastErrorAt: null}));
|
||||
assert.ok(text.includes('Committed modules:\n none'));
|
||||
assert.ok(text.includes('Last updater error: none'));
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user