This commit is contained in:
2026-09-12 13:55:57 +08:00
commit 30f1cedd39
95 changed files with 42057 additions and 0 deletions

View File

@@ -0,0 +1,127 @@
// 回归测试H11 — saveConfig 写盘失败时 appConfig 不被覆盖
//
// 背景:旧 saveConfig 在 writeFileSync 失败时已经更新了内存里的 appConfig
// patch 在 writeFileSync 之前就赋给了 appConfig。结果磁盘保存失败
// 但内存里的配置已经是「新值」—— 用户的设置在重启前看似生效,重启后又被
// 旧值(磁盘上没保存)覆盖,体验「我明明改了,重启就没了」。
//
// 修复v3.7+):先 try 块内 writeFileSync 落盘,成功后再 commit 内存;
// 失败时保持原 appConfig 不变。
//
// 跑法node scripts/check-save-config-rollback.mjs
import { check, summary, printSummary } from './_lib/check.mjs';
// ── 镜像实现:与 main.js 的 saveConfig 一致 ──
console.log('\n[1] saveConfig写盘失败 → appConfig 保持原值');
{
// 镜像 main.js 的新 saveConfig 逻辑(先落盘 → 成功再 commit 内存)
function makeSaveConfig(state = { writeFileThrows: false }) {
let appConfig = { theme: 'auto', fontSize: 14 };
const writeFile = () => {
if (state.writeFileThrows) throw new Error('disk full');
};
function saveConfig(next) {
const candidate = { ...appConfig, ...next };
try {
writeFile();
} catch (e) {
// 写盘失败:保持 appConfig 不变
return appConfig;
}
appConfig = candidate;
return appConfig;
}
return { get appConfig() { return appConfig; }, saveConfig };
}
// 场景 A写盘成功 → 内存 + 返回值都更新
{
const ctx = makeSaveConfig({ writeFileThrows: false });
const before = ctx.appConfig;
const ret = ctx.saveConfig({ theme: 'dark', fontSize: 18 });
check('成功:返回新 appConfig',
ret && ret.theme === 'dark' && ret.fontSize === 18,
`ret=${JSON.stringify(ret)}`);
check('成功appConfig 已是新值',
ctx.appConfig.theme === 'dark' && ctx.appConfig.fontSize === 18);
check('成功:新对象 !== 旧对象(不可变更新)',
ctx.appConfig !== before);
}
// 场景 B写盘失败 → appConfig 保持原值
{
const ctx = makeSaveConfig({ writeFileThrows: true });
const beforeRef = ctx.appConfig;
const ret = ctx.saveConfig({ theme: 'dark', fontSize: 18 });
check('失败:返回的仍是旧 appConfig',
ret && ret.theme === 'auto' && ret.fontSize === 14,
`ret=${JSON.stringify(ret)}`);
check('失败:内存 appConfig 未被覆盖',
ctx.appConfig.theme === 'auto' && ctx.appConfig.fontSize === 14,
`actual=${JSON.stringify(ctx.appConfig)}`);
check('失败appConfig 仍是同一对象引用(无副作用写入)',
ctx.appConfig === beforeRef);
}
// 场景 C写盘失败后再试成功 → 之前的旧值仍然在,下次成功才更新
{
// 用可变状态对象承载 writeFileThrows闭包 getter 在工厂里捕获了 snapshot
const state = { writeFileThrows: true };
const ctx = makeSaveConfig(state);
const beforeRef = ctx.appConfig;
// 第一次:写盘失败
const ret1 = ctx.saveConfig({ theme: 'dark' });
check('C1: 第一次失败 → 返回旧值',
ret1.theme === 'auto');
check('C1: 第一次失败 → appConfig 未变',
ctx.appConfig === beforeRef);
// 写盘恢复
state.writeFileThrows = false;
// 第二次:成功
const ret2 = ctx.saveConfig({ theme: 'dark' });
check('C2: 第二次成功 → 返回新值',
ret2.theme === 'dark');
check('C2: 第二次成功 → appConfig 已是新值',
ctx.appConfig.theme === 'dark');
}
// 场景 D部分字段更新不影响其他字段
{
const ctx = makeSaveConfig();
const ret = ctx.saveConfig({ fontSize: 20 });
check('D: 部分字段更新 → theme 保留',
ret.theme === 'auto' && ret.fontSize === 20);
}
}
// ── [2] 旧实现的回归测试:覆盖式 patch 会污染内存 ──
console.log('\n[2] 回归旧实现patch 在 write 之前)会污染内存');
{
// 镜像 main.js 的旧 saveConfig 行为:先合并、再 try write
let appConfig = { theme: 'auto', fontSize: 14 };
const writeFile = () => { throw new Error('disk full'); };
function oldSaveConfig(next) {
const candidate = { ...appConfig, ...next };
try {
writeFile();
} catch (e) {
// 旧版错误地 commit 了 candidate
appConfig = candidate;
return appConfig;
}
return appConfig;
}
oldSaveConfig({ theme: 'dark' });
// 旧实现下appConfig.theme === 'dark' ← 这就是 bug
check('回归:旧实现下写盘失败但 appConfig.theme 已被改成 "dark"',
appConfig.theme === 'dark',
`actual=${appConfig.theme}`);
}
printSummary('check-save-config-rollback');