update
This commit is contained in:
120
tests/unit/config-store-quit-gate.test.js
Normal file
120
tests/unit/config-store-quit-gate.test.js
Normal file
@@ -0,0 +1,120 @@
|
||||
// main/config-store.js —— saveConfig isQuitting 闸(audit P1-2)
|
||||
//
|
||||
// 背景:renderer 错误风暴(如 settingsStore.update 触发循环)会建无界
|
||||
// Promise 链;进程退出时 IPC handler `await configStore.saveConfig(...)`
|
||||
// 被卡在最末一个未完成 task,before-quit 等不到 save 全部完成 → quit 阻塞。
|
||||
//
|
||||
// 修复:加 isQuitting 闸。before-quit 触发时把闸置 true,新 saveConfig
|
||||
// 立即返回 { ok: false, error: 'APP_QUITTING' }(同步值,不是 Promise,
|
||||
// 避免 IPC await 永不 resolve)。in-flight 任务照常完成(已 fsync 的
|
||||
// 文件 rename 完才返回 ok:true,强制 abort 会丢正在写的 tmp)—— 「
|
||||
// 进程退出」语意下「最新一个 save 完成」即可接受。
|
||||
//
|
||||
// 测试策略:直接 import config-store 模块,用 _setIsQuitting 控制闸。
|
||||
// saveConfig 内部写 tmp + rename 到真文件 → 测试用真 fs + tmpdir 隔离。
|
||||
// 注意:每次测试前 _reset 清状态 + _setIsQuitting(false),避免 case 串扰。
|
||||
|
||||
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
||||
import { mkdtempSync, writeFileSync, readFileSync, existsSync, rmSync } from 'node:fs';
|
||||
import { tmpdir } from 'node:os';
|
||||
import { join } from 'node:path';
|
||||
import * as configStore from '../../main/config-store.js';
|
||||
|
||||
let mockHomeDir;
|
||||
let mockUserDataDir;
|
||||
let home;
|
||||
|
||||
function makeIsolatedHome() {
|
||||
const dir = mkdtempSync(join(tmpdir(), 'notes-config-quit-'));
|
||||
// 写入一个最小可用的 config.json(init 需要它存在)
|
||||
writeFileSync(join(dir, 'config.json'), JSON.stringify({ alwaysOnTop: false }, null, 2), 'utf-8');
|
||||
return dir;
|
||||
}
|
||||
|
||||
describe('config-store saveConfig isQuitting 闸(audit P1-2)', () => {
|
||||
beforeEach(async () => {
|
||||
mockHomeDir = mkdtempSync(join(tmpdir(), 'notes-cq-home-'));
|
||||
mockUserDataDir = mkdtempSync(join(tmpdir(), 'notes-cq-ud-'));
|
||||
configStore._setApp({
|
||||
getPath: (name) => {
|
||||
if (name === 'home') return mockHomeDir;
|
||||
if (name === 'userData') return mockUserDataDir;
|
||||
throw new Error(`unexpected getPath(${name})`);
|
||||
},
|
||||
});
|
||||
configStore._reset();
|
||||
configStore._setIsQuitting(false);
|
||||
home = makeIsolatedHome();
|
||||
await configStore.init({ homeDir: home });
|
||||
});
|
||||
|
||||
it('默认状态:isQuitting = false', () => {
|
||||
expect(configStore._isQuitting()).toBe(false);
|
||||
});
|
||||
|
||||
it('_setIsQuitting(true) 后 isQuitting 反映为 true', () => {
|
||||
configStore._setIsQuitting(true);
|
||||
expect(configStore._isQuitting()).toBe(true);
|
||||
});
|
||||
|
||||
it('isQuitting=true 时 saveConfig 同步返 { ok:false, error:APP_QUITTING },不写盘', async () => {
|
||||
configStore._setIsQuitting(true);
|
||||
const before = readFileSync(join(home, 'config.json'), 'utf-8');
|
||||
|
||||
const result = await configStore.saveConfig({ alwaysOnTop: true });
|
||||
|
||||
expect(result).toEqual({ ok: false, error: 'APP_QUITTING' });
|
||||
|
||||
// 文件未被改写(before vs after 内容一致)
|
||||
const after = readFileSync(join(home, 'config.json'), 'utf-8');
|
||||
expect(after).toBe(before);
|
||||
});
|
||||
|
||||
it('isQuitting=true 时 saveConfig 返的 Promise 已 resolve(不等链尾)', async () => {
|
||||
// 关键:闸开时返回的必须是「已 resolve 的值」,否则 IPC handler
|
||||
// `await saveConfig(...)` 永远挂在链尾 → before-quit 阻塞。
|
||||
configStore._setIsQuitting(true);
|
||||
|
||||
// 10 次并发调用 → 全部应立即 resolve
|
||||
const results = await Promise.all(
|
||||
Array.from({ length: 10 }, () => configStore.saveConfig({ alwaysOnTop: true }))
|
||||
);
|
||||
|
||||
expect(results).toHaveLength(10);
|
||||
for (const r of results) {
|
||||
expect(r).toEqual({ ok: false, error: 'APP_QUITTING' });
|
||||
}
|
||||
});
|
||||
|
||||
it('isQuitting=true 时 in-flight 之前的 save 仍正常完成(不强制 abort)', async () => {
|
||||
// 第一个 save 在闸开启前排进 queue,应正常完成
|
||||
const p1 = configStore.saveConfig({ alwaysOnTop: true });
|
||||
configStore._setIsQuitting(true);
|
||||
const r1 = await p1;
|
||||
|
||||
expect(r1.ok).toBe(true);
|
||||
expect(r1.value.alwaysOnTop).toBe(true);
|
||||
|
||||
// 闸开后后续 save 被拒
|
||||
const r2 = await configStore.saveConfig({ alwaysOnTop: false });
|
||||
expect(r2).toEqual({ ok: false, error: 'APP_QUITTING' });
|
||||
});
|
||||
|
||||
it('isQuitting=true → _setIsQuitting(false) 后 saveConfig 恢复工作', async () => {
|
||||
configStore._setIsQuitting(true);
|
||||
const r1 = await configStore.saveConfig({ alwaysOnTop: true });
|
||||
expect(r1).toEqual({ ok: false, error: 'APP_QUITTING' });
|
||||
|
||||
configStore._setIsQuitting(false);
|
||||
const r2 = await configStore.saveConfig({ alwaysOnTop: true });
|
||||
expect(r2.ok).toBe(true);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
if (home && existsSync(home)) {
|
||||
rmSync(home, { recursive: true, force: true });
|
||||
}
|
||||
if (mockHomeDir) rmSync(mockHomeDir, { recursive: true, force: true });
|
||||
if (mockUserDataDir) rmSync(mockUserDataDir, { recursive: true, force: true });
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user