update
This commit is contained in:
127
scripts/check-save-config-rollback.mjs
Normal file
127
scripts/check-save-config-rollback.mjs
Normal 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');
|
||||
Reference in New Issue
Block a user