desktop/electron/desktopPrefs.test.cjs
Rama main · réplica de lectura del repositorio original.
'use strict'
const test = require('node:test')
const assert = require('node:assert/strict')
const fs = require('fs')
const os = require('os')
const path = require('path')
const {
defaultDesktopPrefs,
getDesktopPrefsPath,
normalizeDesktopPrefs,
readDesktopPrefs,
writeDesktopPrefs,
ensureDesktopDeviceId,
clearDesktopPrefsCache,
DEFAULT_GLOBAL_SHORTCUT,
DEVICE_ID_PATTERN,
} = require('./desktopPrefs.cjs')
/** 共享设备文件路径:与 mao-agent 的 ~/.mao/agent-cli/device.json 同一个文件。 */
function sharedDevicePath() {
return path.join(process.env.MAO_TEST_HOME, '.mao', 'agent-cli', 'device.json')
}
/**
* 隔离共享设备文件的 home:desktopPrefs 用 MAO_TEST_HOME 解析共享路径,
* 用例之间互不影响,也不污染真实 ~/.mao。
*/
function withIsolatedHome(t, body) {
const home = fs.realpathSync(fs.mkdtempSync(path.join(os.tmpdir(), 'mao-shared-device-')))
const previous = process.env.MAO_TEST_HOME
process.env.MAO_TEST_HOME = home
clearDesktopPrefsCache()
t.after(() => {
if (previous == null) delete process.env.MAO_TEST_HOME
else process.env.MAO_TEST_HOME = previous
fs.rmSync(home, { recursive: true, force: true })
clearDesktopPrefsCache()
})
return body(home)
}
function tempUserData(t) {
const dir = fs.realpathSync(fs.mkdtempSync(path.join(os.tmpdir(), 'mao-desktop-prefs-')))
t.after(() => fs.rmSync(dir, { recursive: true, force: true }))
clearDesktopPrefsCache()
return dir
}
test('默认值:关窗托盘化、快捷键 Alt+Shift+M、自启关', () => {
const prefs = defaultDesktopPrefs()
assert.equal(prefs.closeToTray, 'tray')
assert.equal(prefs.trayHintShown, false)
assert.equal(prefs.globalShortcut, DEFAULT_GLOBAL_SHORTCUT)
assert.equal(prefs.globalShortcut, 'Alt+Shift+M')
assert.equal(prefs.autoStart, false)
})
test('文件缺失时读盘返回默认值', (t) => {
const dir = tempUserData(t)
const prefs = readDesktopPrefs(dir)
assert.equal(prefs.closeToTray, 'tray')
assert.equal(prefs.globalShortcut, 'Alt+Shift+M')
})
test('坏文件(非法 JSON)回退默认值,不抛错', (t) => {
const dir = tempUserData(t)
fs.writeFileSync(getDesktopPrefsPath(dir), '{not json')
const prefs = readDesktopPrefs(dir)
assert.equal(prefs.closeToTray, 'tray')
assert.equal(prefs.globalShortcut, 'Alt+Shift+M')
})
test('坏文件(JSON 但类型错误)逐字段回落', (t) => {
const dir = tempUserData(t)
fs.writeFileSync(
getDesktopPrefsPath(dir),
JSON.stringify({ closeToTray: 'nope', globalShortcut: 42, autoStart: 'yes', trayHintShown: 1 })
)
const prefs = readDesktopPrefs(dir)
assert.equal(prefs.closeToTray, 'tray')
assert.equal(prefs.globalShortcut, 'Alt+Shift+M')
assert.equal(prefs.autoStart, false)
assert.equal(prefs.trayHintShown, false)
})
test('合法文件逐字段读入', (t) => {
const dir = tempUserData(t)
fs.writeFileSync(
getDesktopPrefsPath(dir),
JSON.stringify({ closeToTray: 'quit', trayHintShown: true, globalShortcut: null, autoStart: true })
)
const prefs = readDesktopPrefs(dir)
assert.equal(prefs.closeToTray, 'quit')
assert.equal(prefs.trayHintShown, true)
assert.equal(prefs.globalShortcut, null)
assert.equal(prefs.autoStart, true)
})
test('写入后立即读到合并结果(缓存刷新)', (t) => {
const dir = tempUserData(t)
const first = writeDesktopPrefs(dir, { closeToTray: 'quit' })
assert.equal(first.ok, true)
assert.equal(first.prefs.closeToTray, 'quit')
// 未指定字段保持默认
assert.equal(first.prefs.globalShortcut, 'Alt+Shift+M')
const reread = readDesktopPrefs(dir)
assert.equal(reread.closeToTray, 'quit')
const second = writeDesktopPrefs(dir, { globalShortcut: null })
assert.equal(second.prefs.closeToTray, 'quit', '未指定字段保留上次值')
assert.equal(second.prefs.globalShortcut, null)
const third = writeDesktopPrefs(dir, { globalShortcut: 'Ctrl+Alt+K' })
assert.equal(third.prefs.closeToTray, 'quit')
assert.equal(third.prefs.globalShortcut, 'Ctrl+Alt+K')
})
test('写入为原子写:tmp+rename,无残留临时文件,内容完整可解析', (t) => {
const dir = tempUserData(t)
writeDesktopPrefs(dir, { closeToTray: 'quit' })
const files = fs.readdirSync(dir)
assert.deepEqual(files, ['desktop-prefs.json'], `不应残留 tmp 文件: ${files}`)
const raw = fs.readFileSync(getDesktopPrefsPath(dir), 'utf8')
const parsed = JSON.parse(raw)
assert.equal(parsed.closeToTray, 'quit')
})
test('连续多次写入内容始终是最后一次的完整 JSON(无半写文件)', (t) => {
const dir = tempUserData(t)
for (let i = 0; i < 50; i++) {
writeDesktopPrefs(dir, { trayHintShown: i % 2 === 0 })
}
const parsed = JSON.parse(fs.readFileSync(getDesktopPrefsPath(dir), 'utf8'))
// 50 次后 i=49 → false
assert.equal(parsed.trayHintShown, false)
assert.equal(parsed.closeToTray, 'tray')
})
test('normalizeDesktopPrefs:版本演进只加字段,旧文件缺字段走 ?? default', () => {
const normalized = normalizeDesktopPrefs({ closeToTray: 'quit' })
assert.equal(normalized.closeToTray, 'quit')
assert.equal(normalized.trayHintShown, false)
assert.equal(normalized.globalShortcut, 'Alt+Shift+M')
assert.equal(normalized.autoStart, false)
assert.equal(normalized.version, 1)
})
test('normalizeDesktopPrefs:非对象入参回落默认', () => {
assert.equal(normalizeDesktopPrefs(null).closeToTray, 'tray')
assert.equal(normalizeDesktopPrefs('x').closeToTray, 'tray')
assert.equal(normalizeDesktopPrefs(42).closeToTray, 'tray')
})
test('globalShortcut 空字符串回落默认键,null 表示关闭', () => {
assert.equal(normalizeDesktopPrefs({ globalShortcut: '' }).globalShortcut, 'Alt+Shift+M')
assert.equal(normalizeDesktopPrefs({ globalShortcut: ' ' }).globalShortcut, 'Alt+Shift+M')
assert.equal(normalizeDesktopPrefs({ globalShortcut: null }).globalShortcut, null)
assert.equal(normalizeDesktopPrefs({ globalShortcut: ' Ctrl+Shift+A ' }).globalShortcut, 'Ctrl+Shift+A')
})
test('写盘后缓存生效:同 dir 二次读盘不重复解析文件(clearDesktopPrefsCache 后可重读)', (t) => {
const dir = tempUserData(t)
writeDesktopPrefs(dir, { closeToTray: 'quit' })
// 绕过写接口直接改文件,缓存未清时应仍返回缓存值
fs.writeFileSync(getDesktopPrefsPath(dir), JSON.stringify({ closeToTray: 'tray' }))
assert.equal(readDesktopPrefs(dir).closeToTray, 'quit')
clearDesktopPrefsCache()
assert.equal(readDesktopPrefs(dir).closeToTray, 'tray')
// force 读盘
clearDesktopPrefsCache()
writeDesktopPrefs(dir, { closeToTray: 'quit' })
fs.writeFileSync(getDesktopPrefsPath(dir), JSON.stringify({ closeToTray: 'tray' }))
assert.equal(readDesktopPrefs(dir, { force: true }).closeToTray, 'tray')
})
test('设备 ID:ensure 时本机生成并落盘,后续读取稳定不变', (t) => {
const dir = tempUserData(t)
// 读路径不落盘:仅查看偏好不应凭空创建文件
const readOnly = readDesktopPrefs(dir)
assert.match(readOnly.deviceId, DEVICE_ID_PATTERN)
assert.equal(fs.existsSync(getDesktopPrefsPath(dir)), false)
const id = ensureDesktopDeviceId(dir)
assert.match(id, DEVICE_ID_PATTERN)
const persisted = JSON.parse(fs.readFileSync(getDesktopPrefsPath(dir), 'utf8'))
assert.equal(persisted.deviceId, id)
clearDesktopPrefsCache()
assert.equal(readDesktopPrefs(dir).deviceId, id, '设备 ID 必须跨读取稳定')
assert.equal(ensureDesktopDeviceId(dir), id, '重复 ensure 不改写设备 ID')
})
test('设备 ID:patch 不可改写(desktopPrefsSet 无法改绑设备)', (t) => {
const dir = tempUserData(t)
const first = readDesktopPrefs(dir)
const written = writeDesktopPrefs(dir, { deviceId: 'attacker-dev', closeToTray: 'quit' })
assert.equal(written.prefs.deviceId, first.deviceId)
assert.equal(written.prefs.closeToTray, 'quit')
const persisted = JSON.parse(fs.readFileSync(getDesktopPrefsPath(dir), 'utf8'))
assert.equal(persisted.deviceId, first.deviceId)
})
test('设备 ID:旧文件缺字段 / 脏值 → 重新生成本机 ID,其余字段照常读入', (t) => {
const dir = tempUserData(t)
fs.writeFileSync(getDesktopPrefsPath(dir), JSON.stringify({ deviceId: 'bad id!', closeToTray: 'quit' }))
const prefs = readDesktopPrefs(dir)
assert.match(prefs.deviceId, DEVICE_ID_PATTERN)
assert.notEqual(prefs.deviceId, 'bad id!')
assert.equal(prefs.closeToTray, 'quit')
// ensure 后脏值被替换为新 ID 落盘
const id = ensureDesktopDeviceId(dir)
assert.match(id, DEVICE_ID_PATTERN)
const persisted = JSON.parse(fs.readFileSync(getDesktopPrefsPath(dir), 'utf8'))
assert.equal(persisted.deviceId, id)
assert.equal(persisted.closeToTray, 'quit')
})
test('设备 ID:与 mao-agent 共用 ~/.mao/agent-cli/device.json,两端同机同 ID', (t) => {
withIsolatedHome(t, (home) => {
const dir = tempUserData(t)
// CLI 先装过:共享文件里已有 ID,桌面端必须复用它(不再自生成第二个)
const cliId = '11111111-2222-4333-8444-555555555555'
fs.mkdirSync(path.dirname(sharedDevicePath()), { recursive: true })
fs.writeFileSync(sharedDevicePath(), JSON.stringify({ id: cliId }, null, 2) + '\n')
const id = ensureDesktopDeviceId(dir)
assert.equal(id, cliId, '桌面端必须复用 CLI 已登记的设备 ID')
// 桌面偏好同步改绑,避免下次又从旧值出发
clearDesktopPrefsCache()
assert.equal(readDesktopPrefs(dir).deviceId, cliId)
})
})
test('设备 ID:CLI 未装时桌面端创建共享文件,之后 CLI 直接读到同一个 ID', (t) => {
withIsolatedHome(t, () => {
const dir = tempUserData(t)
assert.equal(fs.existsSync(sharedDevicePath()), false)
const id = ensureDesktopDeviceId(dir)
assert.match(id, DEVICE_ID_PATTERN)
assert.equal(fs.existsSync(sharedDevicePath()), true, '桌面端应把设备 ID 写进共享文件')
assert.equal(JSON.parse(fs.readFileSync(sharedDevicePath(), 'utf8')).id, id)
// CLI 侧 resolveDeviceIdentity 读同一文件 → 同机两端同一个 deviceId
clearDesktopPrefsCache()
assert.equal(ensureDesktopDeviceId(dir), id, '重复 ensure 保持稳定')
})
})
test('设备 ID:共享文件损坏时回落桌面自己的 ID,不抛错', (t) => {
withIsolatedHome(t, () => {
const dir = tempUserData(t)
fs.mkdirSync(path.dirname(sharedDevicePath()), { recursive: true })
fs.writeFileSync(sharedDevicePath(), '{not json')
const id = ensureDesktopDeviceId(dir)
assert.match(id, DEVICE_ID_PATTERN)
assert.equal(readDesktopPrefs(dir).deviceId, id)
})
})