Files
2026-07-13 13:20:22 +08:00

143 lines
5.3 KiB
TypeScript

/**
* @license
* Copyright 2025 AionUi (aionui.com)
* SPDX-License-Identifier: Apache-2.0
*
* Unit tests for process/utils/gpuRecovery — covers the threshold-based GPU
* crash self-healing behavior used to mitigate ELECTRON-9A / ELECTRON-9D.
*/
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import * as fs from 'fs';
import * as path from 'path';
import * as os from 'os';
let userDataDir: string;
const disableHardwareAcceleration = vi.fn();
type ChildProcessGoneListener = (event: unknown, details: { type: string; reason: string; exitCode: number }) => void;
let crashListener: ChildProcessGoneListener | null = null;
vi.mock('electron', () => ({
app: {
getPath: (name: string) => {
if (name === 'userData') return userDataDir;
throw new Error(`unexpected getPath: ${name}`);
},
disableHardwareAcceleration: (...args: unknown[]) => disableHardwareAcceleration(...args),
on: (event: string, listener: ChildProcessGoneListener) => {
if (event === 'child-process-gone') crashListener = listener;
},
},
}));
const configFile = () => path.join(userDataDir, 'gpu.config.json');
describe('gpuRecovery', () => {
beforeEach(() => {
userDataDir = fs.mkdtempSync(path.join(os.tmpdir(), 'gpu-recovery-test-'));
disableHardwareAcceleration.mockReset();
crashListener = null;
vi.resetModules();
});
afterEach(() => {
fs.rmSync(userDataDir, { recursive: true, force: true });
});
it('does nothing on first launch with no config', async () => {
const { applyGpuRecoveryFlags } = await import('@/process/utils/gpuRecovery');
applyGpuRecoveryFlags();
expect(disableHardwareAcceleration).not.toHaveBeenCalled();
});
it('disables hardware acceleration when persisted flag is set', async () => {
fs.writeFileSync(
configFile(),
JSON.stringify({ disableHardwareAcceleration: true, crashCount: 3, lastCrashAt: Date.now() })
);
const { applyGpuRecoveryFlags } = await import('@/process/utils/gpuRecovery');
applyGpuRecoveryFlags();
expect(disableHardwareAcceleration).toHaveBeenCalledOnce();
});
it('user override force-off always disables hardware acceleration', async () => {
fs.writeFileSync(configFile(), JSON.stringify({ userOverride: 'force-off' }));
const { applyGpuRecoveryFlags } = await import('@/process/utils/gpuRecovery');
applyGpuRecoveryFlags();
expect(disableHardwareAcceleration).toHaveBeenCalledOnce();
});
it('user override force-on keeps hardware acceleration even with prior crashes', async () => {
fs.writeFileSync(
configFile(),
JSON.stringify({
userOverride: 'force-on',
disableHardwareAcceleration: true,
crashCount: 5,
lastCrashAt: Date.now(),
})
);
const { applyGpuRecoveryFlags } = await import('@/process/utils/gpuRecovery');
applyGpuRecoveryFlags();
expect(disableHardwareAcceleration).not.toHaveBeenCalled();
});
it('resets crash counter when last crash is older than 24h', async () => {
fs.writeFileSync(
configFile(),
JSON.stringify({
disableHardwareAcceleration: true,
crashCount: 5,
lastCrashAt: Date.now() - 25 * 60 * 60 * 1000,
})
);
const { applyGpuRecoveryFlags } = await import('@/process/utils/gpuRecovery');
applyGpuRecoveryFlags();
expect(disableHardwareAcceleration).not.toHaveBeenCalled();
const cfg = JSON.parse(fs.readFileSync(configFile(), 'utf-8'));
expect(cfg.crashCount).toBe(0);
expect(cfg.disableHardwareAcceleration).toBe(false);
});
it('persists disableHardwareAcceleration after crashes reach the threshold', async () => {
const { installGpuCrashHandler } = await import('@/process/utils/gpuRecovery');
installGpuCrashHandler();
expect(crashListener).not.toBeNull();
const details = { type: 'GPU', reason: 'crashed', exitCode: 1 };
// Below threshold (default = 3): no auto-disable yet.
crashListener!({}, details);
crashListener!({}, details);
let cfg = JSON.parse(fs.readFileSync(configFile(), 'utf-8'));
expect(cfg.crashCount).toBe(2);
expect(cfg.disableHardwareAcceleration).toBeFalsy();
// Threshold reached: persist disable flag for next launch.
crashListener!({}, details);
cfg = JSON.parse(fs.readFileSync(configFile(), 'utf-8'));
expect(cfg.crashCount).toBe(3);
expect(cfg.disableHardwareAcceleration).toBe(true);
});
it('ignores non-GPU child-process-gone events', async () => {
const { installGpuCrashHandler } = await import('@/process/utils/gpuRecovery');
installGpuCrashHandler();
crashListener!({}, { type: 'Utility', reason: 'crashed', exitCode: 1 });
expect(fs.existsSync(configFile())).toBe(false);
});
it('respects user override force-on when handling crashes', async () => {
fs.writeFileSync(configFile(), JSON.stringify({ userOverride: 'force-on' }));
const { installGpuCrashHandler } = await import('@/process/utils/gpuRecovery');
installGpuCrashHandler();
const details = { type: 'GPU', reason: 'crashed', exitCode: 1 };
crashListener!({}, details);
crashListener!({}, details);
crashListener!({}, details);
crashListener!({}, details);
const cfg = JSON.parse(fs.readFileSync(configFile(), 'utf-8'));
expect(cfg.crashCount).toBe(4);
expect(cfg.disableHardwareAcceleration).toBeFalsy();
});
});