'use strict' const assert = require('node:assert/strict') const fs = require('node:fs') const os = require('node:os') const path = require('node:path') const test = require('node:test') const { resolveDataDir, migrateLegacyData, isInside } = require('./datadir') // 用绝对路径字符串拼测试目录,避免依赖运行平台的盘符写法 const INSTALL = path.resolve('/scoreboard-install') const LOCAL = path.resolve('/localappdata') function makeRoot(t) { const root = fs.mkdtempSync(path.join(os.tmpdir(), 'scoreboard-datadir-')) t.after(() => fs.rmSync(root, { recursive: true, force: true })) return root } function writeFile(file, text) { fs.mkdirSync(path.dirname(file), { recursive: true }) fs.writeFileSync(file, text) } test('打包态默认数据目录落在 %LOCALAPPDATA%\\ScoreboardConsole\\data', () => { const info = resolveDataDir({ values: { server: { dataDir: '' } }, installDir: INSTALL, env: { LOCALAPPDATA: LOCAL }, isPackaged: true }) assert.equal(info.dir, path.join(LOCAL, 'ScoreboardConsole', 'data')) assert.equal(info.source, 'localappdata') assert.equal(info.passToServer, true) assert.equal(isInside(INSTALL, info.dir), false) }) test('开发态不接管数据目录,也不给服务端加 -data', () => { const info = resolveDataDir({ values: { server: { dataDir: '' } }, installDir: INSTALL, env: { LOCALAPPDATA: LOCAL }, isPackaged: false }) assert.equal(info.dir, path.join(INSTALL, 'data')) assert.equal(info.passToServer, false) }) test('配置了绝对路径就按配置值走', () => { const custom = path.resolve('/scoreboard-data') const info = resolveDataDir({ values: { server: { dataDir: custom } }, installDir: INSTALL, env: { LOCALAPPDATA: LOCAL }, isPackaged: true }) assert.equal(info.dir, custom) assert.equal(info.source, 'config') assert.equal(info.passToServer, true) }) test('配置了相对路径按安装目录解析', () => { const info = resolveDataDir({ values: { server: { dataDir: 'data' } }, installDir: INSTALL, env: { LOCALAPPDATA: LOCAL }, isPackaged: true }) assert.equal(info.dir, path.join(INSTALL, 'data')) assert.equal(info.passToServer, true) assert.equal(isInside(INSTALL, info.dir), true) }) test('缺 %LOCALAPPDATA% 时退回安装目录,不下发 -data', () => { const info = resolveDataDir({ values: { server: {} }, installDir: INSTALL, env: {}, isPackaged: true }) assert.equal(info.dir, path.join(INSTALL, 'data')) assert.equal(info.source, 'fallback') assert.equal(info.passToServer, false) }) test('目标为空且旧目录有数据 → 整体迁移', (t) => { const root = makeRoot(t) const legacyDir = path.join(root, 'install', 'data') const targetDir = path.join(root, 'localappdata', 'ScoreboardConsole', 'data') writeFile(path.join(legacyDir, 'system.db'), 'db') writeFile(path.join(legacyDir, 'event_projs', 'default', 'event.db'), 'event') const lines = [] const result = migrateLegacyData({ targetDir, legacyDir, log: (line) => lines.push(line) }) assert.equal(result.migrated, true) assert.equal(result.reason, 'moved') assert.equal(fs.existsSync(legacyDir), false) assert.equal(fs.readFileSync(path.join(targetDir, 'system.db'), 'utf8'), 'db') assert.equal(fs.existsSync(path.join(targetDir, 'event_projs', 'default', 'event.db')), true) assert.match(lines.join('\n'), /已把旧数据从 .* 迁移到 /) }) test('目标目录已存在但是空目录 → 照样迁移', (t) => { const root = makeRoot(t) const legacyDir = path.join(root, 'install', 'data') const targetDir = path.join(root, 'localappdata', 'ScoreboardConsole', 'data') writeFile(path.join(legacyDir, 'system.db'), 'db') fs.mkdirSync(targetDir, { recursive: true }) const result = migrateLegacyData({ targetDir, legacyDir, log: () => {} }) assert.equal(result.migrated, true) assert.equal(fs.readFileSync(path.join(targetDir, 'system.db'), 'utf8'), 'db') assert.equal(fs.existsSync(legacyDir), false) }) test('两边都有数据 → 不动任何一边,只记日志', (t) => { const root = makeRoot(t) const legacyDir = path.join(root, 'install', 'data') const targetDir = path.join(root, 'localappdata', 'ScoreboardConsole', 'data') writeFile(path.join(legacyDir, 'system.db'), 'old') writeFile(path.join(targetDir, 'system.db'), 'new') const lines = [] const result = migrateLegacyData({ targetDir, legacyDir, log: (line) => lines.push(line) }) assert.equal(result.migrated, false) assert.equal(result.reason, 'both') assert.equal(fs.readFileSync(path.join(legacyDir, 'system.db'), 'utf8'), 'old') assert.equal(fs.readFileSync(path.join(targetDir, 'system.db'), 'utf8'), 'new') assert.match(lines.join('\n'), /都有数据/) }) test('旧目录没有数据 / 目标已有数据 / 两边同路径 → 都不动作', (t) => { const root = makeRoot(t) const emptyLegacy = path.join(root, 'a', 'data') fs.mkdirSync(emptyLegacy, { recursive: true }) assert.equal( migrateLegacyData({ targetDir: path.join(root, 'b', 'data'), legacyDir: emptyLegacy, log: () => {} }).reason, 'no-legacy' ) assert.equal( migrateLegacyData({ targetDir: path.join(root, 'c', 'data'), legacyDir: path.join(root, 'd', 'data'), log: () => {} }).reason, 'no-legacy' ) const legacyDir = path.join(root, 'e', 'data') const targetDir = path.join(root, 'f', 'data') writeFile(path.join(targetDir, 'system.db'), 'new') assert.equal(migrateLegacyData({ targetDir, legacyDir, log: () => {} }).reason, 'target-has-data') assert.equal(migrateLegacyData({ targetDir, legacyDir: targetDir, log: () => {} }).reason, 'same-dir') }) test('迁移失败时旧数据原地不动', (t) => { const root = makeRoot(t) // 目标目录嵌在旧目录内部,rename 必然失败(EINVAL) const legacyDir = path.join(root, 'install', 'data') const targetDir = path.join(legacyDir, 'nested') writeFile(path.join(legacyDir, 'system.db'), 'db') const lines = [] const result = migrateLegacyData({ targetDir, legacyDir, log: (line) => lines.push(line) }) assert.equal(result.migrated, false) assert.equal(result.reason, 'failed') assert.equal(fs.readFileSync(path.join(legacyDir, 'system.db'), 'utf8'), 'db') assert.match(lines.join('\n'), /旧数据仍保留在/) }) test('目标路径读不出来时按「可能有数据」处理,不做迁移', (t) => { const root = makeRoot(t) const legacyDir = path.join(root, 'install', 'data') writeFile(path.join(legacyDir, 'system.db'), 'db') // 目标目录的上级是个文件,readdir 会报 ENOTDIR const blocker = path.join(root, 'blocker') writeFile(blocker, 'not a dir') const lines = [] const result = migrateLegacyData({ targetDir: path.join(blocker, 'data'), legacyDir, log: (line) => lines.push(line) }) assert.equal(result.migrated, false) assert.equal(result.reason, 'target-unreadable') assert.equal(fs.readFileSync(path.join(legacyDir, 'system.db'), 'utf8'), 'db') assert.match(lines.join('\n'), /跳过旧数据迁移/) }) test('旧路径读不出来时不当成「没有旧数据」,要记一条日志', (t) => { const root = makeRoot(t) const targetDir = path.join(root, 'localappdata', 'ScoreboardConsole', 'data') fs.mkdirSync(targetDir, { recursive: true }) // 旧目录的上级是个文件:readdir 报 ENOTDIR,等同于现场遇到 EACCES 这类「读不到」 const blocker = path.join(root, 'blocker') writeFile(blocker, 'not a dir') const lines = [] const result = migrateLegacyData({ targetDir, legacyDir: path.join(blocker, 'data'), log: (line) => lines.push(line) }) assert.equal(result.migrated, false) assert.equal(result.reason, 'legacy-unreadable') assert.match(lines.join('\n'), /读不到旧数据目录/) }) test('确实没有旧数据时只返回 no-legacy,不写警告日志', (t) => { const root = makeRoot(t) const targetDir = path.join(root, 'localappdata', 'ScoreboardConsole', 'data') fs.mkdirSync(targetDir, { recursive: true }) const legacyDir = path.join(root, 'install', 'data') fs.mkdirSync(legacyDir, { recursive: true }) const lines = [] const result = migrateLegacyData({ targetDir, legacyDir, log: (line) => lines.push(line) }) assert.equal(result.migrated, false) assert.equal(result.reason, 'no-legacy') assert.deepEqual(lines, []) }) // —— 以下是 Linux 分支:默认位置换成 XDG 的数据目录,理由与 Windows 一致—— // 必须落在安装目录之外,否则覆盖安装时会被卸载器连目录一起删掉。 test('Linux 打包态默认数据目录落在 $XDG_DATA_HOME/ScoreboardConsole/data', () => { const xdg = path.resolve('/xdg/data') const info = resolveDataDir({ values: { server: { dataDir: '' } }, installDir: INSTALL, env: { XDG_DATA_HOME: xdg, HOME: path.resolve('/home/operator') }, isPackaged: true, platform: 'linux' }) assert.equal(info.dir, path.join(xdg, 'ScoreboardConsole', 'data')) assert.equal(info.source, 'xdg') assert.equal(info.passToServer, true) assert.equal(isInside(INSTALL, info.dir), false) }) test('Linux 没设 $XDG_DATA_HOME 时用 ~/.local/share', () => { const home = path.resolve('/home/operator') const info = resolveDataDir({ values: { server: { dataDir: '' } }, installDir: INSTALL, env: { HOME: home }, isPackaged: true, platform: 'linux' }) assert.equal(info.dir, path.join(home, '.local', 'share', 'ScoreboardConsole', 'data')) assert.equal(info.passToServer, true) }) test('Linux 拿不到 $XDG_DATA_HOME 与 $HOME 时退回安装目录,不下发 -data', () => { const info = resolveDataDir({ values: { server: {} }, installDir: INSTALL, env: {}, isPackaged: true, platform: 'linux' }) assert.equal(info.dir, path.join(INSTALL, 'data')) assert.equal(info.source, 'fallback') assert.equal(info.passToServer, false) }) test('Linux 开发态同样不接管,也不给服务端加 -data', () => { const home = path.resolve('/home/operator') const info = resolveDataDir({ values: { server: { dataDir: '' } }, installDir: INSTALL, env: { HOME: home }, isPackaged: false, platform: 'linux' }) assert.equal(info.dir, path.join(INSTALL, 'data')) assert.equal(info.passToServer, false) }) test('Linux 上配置了 dataDir 一样按配置走(绝对路径 / 相对安装目录)', () => { const home = path.resolve('/home/operator') const absolute = path.resolve('/scoreboard-data') assert.equal( resolveDataDir({ values: { server: { dataDir: absolute } }, installDir: INSTALL, env: { HOME: home }, isPackaged: true, platform: 'linux' }).dir, absolute ) assert.equal( resolveDataDir({ values: { server: { dataDir: 'data' } }, installDir: INSTALL, env: { HOME: home }, isPackaged: true, platform: 'linux' }).dir, path.join(INSTALL, 'data') ) })