'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) }) })