fix(stores): 配置文件形状不符先备份再回退;日志尾部同步落盘

- commands/connections/settings 三个 store 原先只对 JSON.parse 抛错做
  .bak 备份,合法 JSON 但形状不符(如 [1,2,3])会静默回退空/默认,下次
  写盘把命令库/书签凭据/自定义主题整体销毁。现统一为形状不符也先备份
  (与 knownHosts 的 fail-closed 策略对齐),三个 store 测试补对应用例
- sanitizeRules 回退分支返回预设副本而非模块级共享数组引用,并在回退时
  记录 warning(原先连警告都没有)
- 会话日志尾部:logStop 触发的最后一轮 flush 改同步落盘(在途异步写未
  落地时由 drain 循环做保序的最终同步轮),before-quit 不再丢日志尾巴
This commit is contained in:
Bill committed 2026-10-09 11:31:06 +08:00
1 parent e23010f950
commit 172cce1962
6 files changed
+253 -19

No files matched your search

+36
View File
@@ -205,6 +205,42 @@ ok(!existsSync(join(freshDir, 'connections.json.bak')), 'a missing file (ENOENT)
ok(existsSync(join(freshDir, 'connections.json')), 'and the first write lands normally')
rmSync(freshDir, { recursive: true, force: true })
// ---- 7b. Valid JSON of the wrong shape is backed up too --------------------------
// `{}` parses fine, so the parse-catch never saw it: the file used to be treated
// exactly like a missing one and the next write replaced every bookmark with an
// empty list. A file that is not the array we wrote is unreadable, not empty —
// same backup + empty-state exit as the corrupt case above.
const shapeDir = mkdtempSync(join(tmpdir(), 'm-conn-shape-'))
const shapeFile = join(shapeDir, 'connections.json')
const shapeStore = new mod.ConnectionsStore(shapeFile)
const wrongShape = '{"connections":[]}'
writeFileSync(shapeFile, wrongShape, 'utf8')
ok(shapeStore.listConnections().length === 0, 'a wrong-shaped connections.json loads as an empty list')
const shapeBak = `${shapeFile}.bak`
ok(existsSync(shapeBak), 'a wrong-shaped connections.json is backed up to .bak')
ok(readFileSync(shapeBak, 'utf8') === wrongShape, '.bak holds the wrong-shaped original byte for byte')
const shapeSaved = shapeStore.saveConnection({
name: 'after-shape-mismatch',
host: 'h',
port: 22,
username: 'u',
auth: 'password',
askPasswordAtConnect: false,
askPassphraseAtConnect: false,
keepaliveIntervalSec: 0
})
const shapeList = JSON.parse(readFileSync(shapeFile, 'utf8'))
ok(
Array.isArray(shapeList) && shapeList.length === 1 && shapeList[0].id === shapeSaved.id,
'the new bookmark is written normally after the backup'
)
// A second wrong-shape episode must not overwrite the first backup.
writeFileSync(shapeFile, '{"nope":true}', 'utf8')
shapeStore.listConnections()
ok(readFileSync(shapeBak, 'utf8') === wrongShape, 'an existing .bak is kept (earliest evidence wins)')
rmSync(shapeDir, { recursive: true, force: true })
// An unreadable path (here: a directory) must still load as empty and never
// throw — the backup is best effort and may itself fail.
const dirCase = mkdtempSync(join(tmpdir(), 'm-conn-dir-'))