mirror of
https://github.com/openswarm-ai/openswarm.git
synced 2026-09-03 00:08:46 +02:00
76 lines
4.4 KiB
JavaScript
76 lines
4.4 KiB
JavaScript
// ENG-400: four crashes from one user, production 1.7.9. The first came after 34 hours; every
|
|
// relaunch afterwards died in under two minutes, so the app was unusable. Safe mode WAS armed for
|
|
// launches 3 and 4 (two dirty exits inside ten minutes) and they crashed anyway, because main
|
|
// computed safeModeInfo and then did nothing with it except answer an IPC. The mitigation was
|
|
// entirely renderer-side, which cannot help a main-process crash.
|
|
//
|
|
// Two halves are pinned here: main enforces safe mode on the one process it controls (the backend's
|
|
// auto-resume, which fires the very turn that was running when the app died), and the offscreen
|
|
// browser stops tearing windows down mid-call, which is the shape of the faulting instruction
|
|
// (a virtual call on an object whose vtable pointer is zero, while walking a list).
|
|
const test = require('node:test');
|
|
const assert = require('node:assert');
|
|
const fs = require('fs');
|
|
const path = require('path');
|
|
|
|
const main = fs.readFileSync(path.join(__dirname, 'main.js'), 'utf8');
|
|
const hidden = fs.readFileSync(path.join(__dirname, 'hiddenBrowser.js'), 'utf8');
|
|
|
|
test('safe mode is known before the backend spawns, not after', () => {
|
|
// It used to be computed inside whenReady, BELOW the backend spawn, so the one process that can
|
|
// act on it had already started with the wrong answer.
|
|
const detect = main.indexOf('\nif (gotLock) detectDirtyExitAndArmSafeMode();');
|
|
const spawn = main.indexOf('await startBackend();');
|
|
assert.ok(detect > 0, 'detection must run at module load');
|
|
assert.ok(detect < spawn, 'detection must precede the backend spawn');
|
|
assert.ok(!main.includes(' detectDirtyExitAndArmSafeMode();\n spawnCrashWatchdog();'),
|
|
'the old whenReady call site must be gone, not duplicated');
|
|
});
|
|
|
|
test('a losing second instance cannot stamp a dirty exit over the real one', () => {
|
|
const i = main.indexOf('detectDirtyExitAndArmSafeMode();\n\nipcMain.handle');
|
|
assert.ok(main.slice(i - 60, i).includes('if (gotLock)'), 'the module-level call is lock-guarded');
|
|
});
|
|
|
|
test('main enforces safe mode instead of only advertising it', () => {
|
|
const i = main.indexOf('OSW_DISABLE_AUTO_RESUME');
|
|
assert.ok(i > 0, 'the backend must be told');
|
|
assert.ok(main.slice(i - 400, i + 60).includes('safeModeInfo.safeMode'),
|
|
'and told on the same condition the renderer reads, so the two cannot disagree');
|
|
});
|
|
|
|
test('the hold is one boot and leaves the user a way back in', () => {
|
|
// Never auto-resuming would be silent work loss, a worse row than the crash loop.
|
|
const i = main.indexOf('OSW_DISABLE_AUTO_RESUME');
|
|
const why = main.slice(i - 400, i);
|
|
assert.ok(/Resume chip/.test(why), 'the comment must record that the turn stays recoverable');
|
|
});
|
|
|
|
test('every offscreen page call goes through the one guarded door', () => {
|
|
assert.ok(!/win\.webContents\.executeJavaScript/.test(hidden),
|
|
'a direct call can fire after the killer already tore the window down');
|
|
assert.ok(hidden.includes('async function evalInPage'));
|
|
const body = hidden.slice(hidden.indexOf('async function evalInPage'), hidden.indexOf('async function withWindow'));
|
|
assert.ok(body.includes('win.isDestroyed()'), 'the window must be checked');
|
|
assert.ok(body.includes('wc.isDestroyed()'), 'and so must its contents; the window can outlive them');
|
|
assert.ok(body.includes('return null'), 'a dead window yields nothing, it does not throw');
|
|
});
|
|
|
|
test('one idempotent disposer, graceful before violent', () => {
|
|
const body = hidden.slice(hidden.indexOf('function disposeWindow'), hidden.indexOf('// The ONLY door'));
|
|
assert.ok(body.includes('if (!win || win.isDestroyed()) return;'), 'calling it twice must be safe');
|
|
assert.ok(body.indexOf('win.close()') < body.indexOf('win.destroy()'),
|
|
'close() runs the teardown that lets observers unregister; destroy() skips it');
|
|
});
|
|
|
|
test('the timeout killer routes through the disposer, not a bare destroy', () => {
|
|
const body = hidden.slice(hidden.indexOf('async function withWindow'), hidden.indexOf('const HARVEST_GRACE_MS'));
|
|
assert.ok(body.includes('setTimeout(() => disposeWindow(win)'));
|
|
assert.ok(!/killer = setTimeout\(\(\) => \{ try \{ win\.destroy/.test(body));
|
|
});
|
|
|
|
test('loading refuses to start on a window that is already gone', () => {
|
|
const body = hidden.slice(hidden.indexOf('async function loadAndSettle'), hidden.indexOf('// Fetch a URL'));
|
|
assert.ok(body.includes('if (win.isDestroyed()) return;'));
|
|
});
|