'use strict'; /** * 数据目录降级测试 —— 解决「默认 D:\Clipboard Data 不可用就启动失败」的兼容问题。 * * 被测对象是 config.js 的 isDirUsable / defaultDataDir。三条契约: * 1. isDirUsable 能正确判断目录真的能写入; * 2. defaultDataDir 在默认目录不可用时降级到 home 下的 Clipboard Data; * 3. 探针文件不会残留(每次探测都自清理)。 * * 模拟「D 盘不存在」的方法:临时替换 fs.mkdirSync,让它对 D:\\ 开头的路径 * 抛 ENOENT —— 跟项目里「不引入 mock 框架、靠临时替换内置模块方法」的风格 * 一致。restore 严格用 finally / afterEach 兜住,避免污染其它测试。 */ const assert = require('node:assert'); const fs = require('node:fs'); const os = require('node:os'); const path = require('node:path'); let passed = 0; function check(name, fn) { try { fn(); console.log(' ok -', name); passed++; } catch (e) { console.error(' FAIL -', name, '\n', e.message); process.exitCode = 1; } } // 用临时 HOME 隔离 —— defaultDataDir() 降级时是 %USERPROFILE%\Clipboard Data, // 我们希望探针 / 副作用都落在临时目录,不污染真实 home。 const tmpHome = fs.mkdtempSync(path.join(os.tmpdir(), 'clip-fb-')); process.env.USERPROFILE = tmpHome; process.env.HOME = tmpHome; // 不预设 CLIPBOARD_APP_DATA_DIR —— 本测试就是要验证它没设时的降级行为 delete process.env.CLIPBOARD_APP_DATA_DIR; const cfg = require('../config.js'); // 「D 盘不存在」的 mock:fs.mkdirSync 对 D:\ 前缀抛 ENOENT。 // 不影响其它调用 —— D: 在大多数机器上不一定存在,我们的判断「按前缀」 // 已经足够准;同时不影响 home 下的真实写入。 const originalMkdir = fs.mkdirSync; function mockNoDDrive() { fs.mkdirSync = function patched(p, opts) { if (typeof p === 'string' && /^[dD]:[\\/]/.test(p)) { const e = new Error(`ENOENT: no such file or directory, mkdir '${p}'`); e.code = 'ENOENT'; throw e; } return originalMkdir.call(fs, p, opts); }; } function restoreFs() { fs.mkdirSync = originalMkdir; } process.on('exit', restoreFs); check('isDirUsable: 真实可写目录返回 true', () => { // tmpHome 必然可写 assert.strictEqual(cfg.isDirUsable(tmpHome), true); }); check('isDirUsable: 不存在的盘符(D:\\__no_such__)在 mock 下返回 false', () => { mockNoDDrive(); try { assert.strictEqual(cfg.isDirUsable('D:\\__no_such_drive_xyz__'), false); } finally { restoreFs(); } }); check('isDirUsable: 空字符串返回 false(不打 fs.mkdirSync 空路径)', () => { assert.strictEqual(cfg.isDirUsable(''), false); assert.strictEqual(cfg.isDirUsable(null), false); assert.strictEqual(cfg.isDirUsable(undefined), false); }); check('isDirUsable: 探针文件被清理,不在目录里残留', () => { // 探针名字固定 —— 跑两次探测,第二次还能成功说明第一次 unlink 了。 const probePath = path.join(tmpHome, '.clipboard-write-test'); assert.ok(!fs.existsSync(probePath), '开始前不应有残留'); assert.strictEqual(cfg.isDirUsable(tmpHome), true); assert.ok(!fs.existsSync(probePath), '探测后不应残留探针文件'); assert.strictEqual(cfg.isDirUsable(tmpHome), true, '第二次探测照样过 → 第一次 unlink 干净'); }); check('defaultDataDir: D 盘不可用时降级到 home 下的 Clipboard Data', () => { mockNoDDrive(); try { // 重新加载模块,让 defaultDataDir() 重新走一遍降级逻辑 delete require.cache[require.resolve('../config.js')]; const cfg2 = require('../config.js'); const dir = cfg2.defaultDataDir(); // 降级路径必须等于 home + Clipboard Data assert.strictEqual( path.resolve(dir), path.resolve(tmpHome, 'Clipboard Data'), 'D 盘被 mock 掉时必须降级到 home\\Clipboard Data' ); // 降级目录真的可用 —— 否则还是炸 assert.strictEqual(cfg2.isDirUsable(dir), true); } finally { restoreFs(); delete require.cache[require.resolve('../config.js')]; require('../config.js'); // 重新挂回正常状态给后面的 case } }); check('defaultDataDir: env 覆盖在降级链路里仍最高优先级', () => { mockNoDDrive(); const custom = path.join(tmpHome, 'env-override-dir'); process.env.CLIPBOARD_APP_DATA_DIR = custom; try { delete require.cache[require.resolve('../config.js')]; const cfg2 = require('../config.js'); assert.strictEqual(path.resolve(cfg2.defaultDataDir()), path.resolve(custom), 'env 覆盖必须胜过 D 盘 / home 的降级选择'); } finally { restoreFs(); delete process.env.CLIPBOARD_APP_DATA_DIR; delete require.cache[require.resolve('../config.js')]; require('../config.js'); } }); check('defaultDataDir: D 盘可用时首选 D 盘(契约保留)', () => { // 不 mock —— 用真实环境。仅当 D 盘真的可用时断言,否则跳过。 if (!cfg.isDirUsable('D:\\Clipboard Data')) { console.log(' (D:\\Clipboard Data 在本机不可用,跳过)'); return; } // 不设 env 时必须首选 D 盘 const saved = process.env.CLIPBOARD_APP_DATA_DIR; delete process.env.CLIPBOARD_APP_DATA_DIR; delete require.cache[require.resolve('../config.js')]; try { const cfg2 = require('../config.js'); assert.strictEqual(path.resolve(cfg2.defaultDataDir()), path.resolve('D:\\Clipboard Data')); } finally { process.env.CLIPBOARD_APP_DATA_DIR = saved; delete require.cache[require.resolve('../config.js')]; require('../config.js'); } }); // 清理 fs.rmSync(tmpHome, { recursive: true, force: true }); console.log(`\n${passed} checks passed`);