228 lines
13 KiB
JavaScript
228 lines
13 KiB
JavaScript
/**
|
||
* 服务端数据目录解析回归测试(Codex 对抗审查:loadPkg ↔ resolveDshHomeSafe 循环调用 + 降级规则对齐官方)。
|
||
* 用法:node server-home.test.mjs [lib/index.js 路径](缺省 = 工作目录构建产物)
|
||
*
|
||
* 场景(全部在子进程隔离目录中运行,父进程不改环境变量、不残留临时目录):
|
||
* A. 官方分支:隔离目录里「明确准备官方工具包夹具」(与 @deepseek-ai/dsh-home-paths 同布局、同实现语义),
|
||
* 断言 homeSource=official(证明代码确实走了官方分支并调用其 resolveDshHome)+ 探测次数有界 + 内容正确
|
||
* B. BOM 容忍:状态码 + 解析后内容双断言
|
||
* C. 兜底分支:官方包不可解析 → homeSource=fallback + 探测次数有界 + 内容正确(旧代码循环重入会产生上万次探测)
|
||
* D/E/F. ~ / ~/ / ~\ 前缀展开与相对路径:断言错误信息包含「JSON 转义后的完整预期绝对路径」
|
||
* 说明:DSH_HOME 空/纯空白 → 默认 ~/.dsh(真实目录)——测试刻意不触发,避免读取真实用户数据。
|
||
* 失败路径不调用 process.exit(避免跳过 finally),统一收口到 process.exitCode。
|
||
*/
|
||
import { spawnSync } from 'node:child_process'
|
||
import { copyFileSync, existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs'
|
||
import { tmpdir } from 'node:os'
|
||
import { dirname, join, resolve } from 'node:path'
|
||
import { fileURLToPath, pathToFileURL } from 'node:url'
|
||
|
||
const HERE = dirname(fileURLToPath(import.meta.url))
|
||
// 构建产物路径参数化:发布脚本传入「已安装包内」的 lib/index.js;缺省回退工作目录构建产物
|
||
const BUNDLE_SRC = resolve(process.argv[2] ?? join(HERE, '..', '01_content', 'lib', 'index.js'))
|
||
|
||
// 探测次数上界:正常解析(祖先链 + profiles 枚举)远低于此;旧代码循环重入会达到数千次
|
||
const PROBE_BOUND = 100
|
||
|
||
let pass = 0
|
||
const failures = []
|
||
function fail(name, detail) {
|
||
failures.push(name + ': ' + detail)
|
||
console.error('FAIL(' + name + '): ' + detail)
|
||
}
|
||
const ok = (name) => { console.log('ok ' + name); pass++ }
|
||
|
||
/** 临时 DSH 数据目录(含 storages/workspace.json 标记内容;无 BOM) */
|
||
function makeHome(marker) {
|
||
const home = mkdtempSync(join(tmpdir(), 'wt-home-test-'))
|
||
mkdirSync(join(home, 'storages'), { recursive: true })
|
||
writeFileSync(join(home, 'storages', 'workspace.json'), JSON.stringify({
|
||
unit: { name: 'workspace', version: 2 },
|
||
global: { initialized: true, workspaceIds: ['fake-' + marker], archivedSessionIds: [] },
|
||
tables: { workspaces: { ['fake-' + marker]: { title: marker, sessionIds: ['s-' + marker] } } },
|
||
}), 'utf8')
|
||
return home
|
||
}
|
||
|
||
/**
|
||
* 子进程跑一个隔离场景:
|
||
* - withOfficial=true 时,在隔离目录 node_modules 下「明确准备官方工具包夹具」
|
||
* (与 @deepseek-ai/dsh-home-paths 同布局:lib/index.js 导出 resolveDshHome/expandHomePath/defaultDshHome,
|
||
* 实现为其公开文档语义的忠实镜像)——验证我们调用官方分支与返回值处理的集成,
|
||
* 上游包自身语义由官方维护;夹具不引入生产依赖。
|
||
*/
|
||
function runChild(envHome, name, withOfficial = false) {
|
||
const isoDir = mkdtempSync(join(tmpdir(), 'wt-home-iso-'))
|
||
let home = null
|
||
try {
|
||
mkdirSync(join(isoDir, 'lib'), { recursive: true })
|
||
copyFileSync(BUNDLE_SRC, join(isoDir, 'lib', 'index.js'))
|
||
if (withOfficial) {
|
||
const pkgDir = join(isoDir, 'node_modules', '@deepseek-ai', 'dsh-home-paths')
|
||
mkdirSync(join(pkgDir, 'lib'), { recursive: true })
|
||
writeFileSync(join(pkgDir, 'package.json'), JSON.stringify({ name: '@deepseek-ai/dsh-home-paths', version: '0.1.2-rc.1', type: 'module', main: 'lib/index.js' }), 'utf8')
|
||
writeFileSync(join(pkgDir, 'lib', 'index.js'), [
|
||
"import { homedir } from 'node:os'",
|
||
"import { join, resolve } from 'node:path'",
|
||
"export const DSH_HOME_DIR_NAME = '.dsh'",
|
||
"export function defaultDshHome() { return join(homedir(), DSH_HOME_DIR_NAME) }",
|
||
"export function expandHomePath(path) {",
|
||
" if (path === '~') return homedir()",
|
||
" if (path.startsWith('~/') || path.startsWith('~' + String.fromCharCode(92))) return join(homedir(), path.slice(2))",
|
||
" return path",
|
||
"}",
|
||
"export function resolveDshHome(configured, env = process.env) {",
|
||
" const fromEnv = env['DSH_HOME']",
|
||
" return resolve(expandHomePath(configured ?? (fromEnv !== undefined && fromEnv.trim().length > 0 ? fromEnv : defaultDshHome())))",
|
||
"}",
|
||
"export function dshHomePath(...segments) { return join(resolveDshHome(), ...segments) }",
|
||
"export function dshHomeDisplay(resolvedHome) {",
|
||
" return resolvedHome === resolve(defaultDshHome()) ? '~/.dsh' : '$DSH_HOME'",
|
||
"}",
|
||
"export const DSH_HOME_ENV = 'DSH_HOME'",
|
||
"export const DEFAULT_DSH_HOME_DISPLAY = '~/.dsh'",
|
||
].join('\n'), 'utf8')
|
||
}
|
||
const homeVal = envHome === undefined ? (home = makeHome('MARKER_' + name)) : envHome
|
||
const script = [
|
||
"import { pathToFileURL } from 'node:url'",
|
||
"import { resolve as pResolve, join as pJoin } from 'node:path'",
|
||
"import { homedir } from 'node:os'",
|
||
"const mod = await import(pathToFileURL(process.argv[1]).href)",
|
||
"const routes = []",
|
||
"const fakeCtx = { webServer: { register: (r) => { routes.push(r) } }, get: () => ({ admit: () => ({ peer: { principal: { kind: 'owner' } } }) }), effect: (fn) => fn(), logger: { warn() {}, info() {} } }",
|
||
"mod.apply(fakeCtx)",
|
||
"const route = routes.find((r) => r.path === '/api/worktable/workspaces')",
|
||
"let status = 0; let body = ''",
|
||
"const res = { writeHead(s) { status = s }, end(b) { body = String(b) } }",
|
||
"await route.handler({}, res)",
|
||
"const stats = mod.__wtLoadProbeStats ? mod.__wtLoadProbeStats() : null",
|
||
"const raw = process.env.DSH_HOME || ''",
|
||
"const BS = String.fromCharCode(92)",
|
||
"const expanded = raw === '~' ? homedir() : (raw.startsWith('~/') || raw.startsWith('~' + BS)) ? pJoin(homedir(), raw.slice(2)) : raw",
|
||
"const expectedFull = pResolve(expanded, 'storages', 'workspace.json')",
|
||
"const expectedEncoded = JSON.stringify(expectedFull).slice(1, -1)",
|
||
"console.log(JSON.stringify({ status, body: body.slice(0, 500), stats, expectedFull, fullMatch: body.includes(expectedEncoded) }))",
|
||
].join('\n')
|
||
const r = spawnSync(process.execPath, ['--input-type=module', '-e', script, join(isoDir, 'lib', 'index.js')], {
|
||
env: { ...process.env, DSH_HOME: homeVal ?? '' },
|
||
encoding: 'utf8',
|
||
timeout: 15000,
|
||
})
|
||
if (r.error) { fail(name, 'spawn error: ' + r.error.message); return null }
|
||
if (r.status !== 0) { fail(name, 'exit=' + r.status + ' stderr=' + (r.stderr || '').slice(0, 400)); return null }
|
||
return JSON.parse(r.stdout.trim().split('\n').pop())
|
||
} finally {
|
||
rmSync(isoDir, { recursive: true, force: true })
|
||
if (home) rmSync(home, { recursive: true, force: true })
|
||
}
|
||
}
|
||
|
||
// —— A+B. 官方分支(测试环境明确准备官方夹具)+ BOM 容忍 ——
|
||
{
|
||
const out = runChild(undefined, 'A', true)
|
||
if (!out) fail('A.官方路径', 'child did not return')
|
||
else {
|
||
if (out.status !== 200) fail('A.官方路径', 'status=' + out.status + ' body=' + out.body.slice(0, 120))
|
||
else {
|
||
try {
|
||
const parsed = JSON.parse(out.body)
|
||
if (parsed.tables?.workspaces?.['fake-MARKER_A']?.title !== 'MARKER_A') fail('A.官方路径', 'content mismatch: ' + out.body.slice(0, 160))
|
||
} catch { fail('A.官方路径', 'body not JSON: ' + out.body.slice(0, 160)) }
|
||
}
|
||
if (!out.stats) fail('A.官方路径', '__wtLoadProbeStats missing')
|
||
else {
|
||
if (out.stats.homeSource !== 'official') fail('A.官方路径', 'homeSource=' + out.stats.homeSource + ' (expect official; 场景 A 必须证明官方分支被实际调用)')
|
||
if (out.stats.attempts > PROBE_BOUND) fail('A.官方路径', 'probe attempts=' + out.stats.attempts + ' > ' + PROBE_BOUND)
|
||
}
|
||
// BOM 容忍:在子进程里没法重写(缓存单次解析),这里再跑一次带 BOM 夹具文件的场景 B
|
||
}
|
||
// B. BOM:单独子进程,写入带 BOM 的 workspace.json
|
||
const outB = (() => {
|
||
const isoDir = mkdtempSync(join(tmpdir(), 'wt-home-iso-'))
|
||
let home = null
|
||
try {
|
||
mkdirSync(join(isoDir, 'lib'), { recursive: true })
|
||
copyFileSync(BUNDLE_SRC, join(isoDir, 'lib', 'index.js'))
|
||
const pkgDir = join(isoDir, 'node_modules', '@deepseek-ai', 'dsh-home-paths')
|
||
mkdirSync(join(pkgDir, 'lib'), { recursive: true })
|
||
writeFileSync(join(pkgDir, 'package.json'), JSON.stringify({ name: '@deepseek-ai/dsh-home-paths', version: '0.1.2-rc.1', type: 'module', main: 'lib/index.js' }), 'utf8')
|
||
writeFileSync(join(pkgDir, 'lib', 'index.js'), [
|
||
"import { homedir } from 'node:os'",
|
||
"import { join, resolve } from 'node:path'",
|
||
"export function resolveDshHome(configured, env = process.env) {",
|
||
" const fromEnv = env['DSH_HOME']",
|
||
" return resolve(configured ?? (fromEnv !== undefined && fromEnv.trim().length > 0 ? fromEnv : join(homedir(), '.dsh')))",
|
||
"}",
|
||
].join('\n'), 'utf8')
|
||
home = makeHome('MARKER_B')
|
||
const file = join(home, 'storages', 'workspace.json')
|
||
writeFileSync(file, '\uFEFF' + readFileSync(file, 'utf8'), 'utf8')
|
||
const script = [
|
||
"import { pathToFileURL } from 'node:url'",
|
||
"const mod = await import(pathToFileURL(process.argv[1]).href)",
|
||
"const routes = []",
|
||
"const fakeCtx = { webServer: { register: (r) => { routes.push(r) } }, get: () => ({ admit: () => ({ peer: { principal: { kind: 'owner' } } }) }), effect: (fn) => fn(), logger: { warn() {}, info() {} } }",
|
||
"mod.apply(fakeCtx)",
|
||
"const route = routes.find((r) => r.path === '/api/worktable/workspaces')",
|
||
"let status = 0; let body = ''",
|
||
"const res = { writeHead(s) { status = s }, end(b) { body = String(b) } }",
|
||
"await route.handler({}, res)",
|
||
"console.log(JSON.stringify({ status, body: body.slice(0, 500) }))",
|
||
].join('\n')
|
||
const r = spawnSync(process.execPath, ['--input-type=module', '-e', script, join(isoDir, 'lib', 'index.js')], {
|
||
env: { ...process.env, DSH_HOME: home },
|
||
encoding: 'utf8',
|
||
timeout: 15000,
|
||
})
|
||
if (r.error) { fail('B.BOM', 'spawn error: ' + r.error.message); return null }
|
||
if (r.status !== 0) { fail('B.BOM', 'exit=' + r.status + ' stderr=' + (r.stderr || '').slice(0, 400)); return null }
|
||
return JSON.parse(r.stdout.trim().split('\n').pop())
|
||
} finally {
|
||
rmSync(isoDir, { recursive: true, force: true })
|
||
if (home) rmSync(home, { recursive: true, force: true })
|
||
}
|
||
})()
|
||
if (outB) {
|
||
let parsed = null
|
||
try { parsed = JSON.parse(outB.body) } catch {}
|
||
if (outB.status !== 200 || !parsed || parsed.tables?.workspaces?.['fake-MARKER_B']?.title !== 'MARKER_B') fail('B.BOM', 'status=' + outB.status + ' body=' + outB.body.slice(0, 160))
|
||
}
|
||
if (!failures.some((f) => f.startsWith('A.')) && !failures.some((f) => f.startsWith('B.'))) ok('A+B. 官方分支(夹具验证 homeSource=official + 有界探测)+ BOM 容忍')
|
||
}
|
||
|
||
// —— C. 兜底分支:官方包不可解析 → baseDshHome,且探测次数有界(循环回归) ——
|
||
{
|
||
const out = runChild(undefined, 'C')
|
||
if (!out) fail('C.兜底路径', 'child did not return')
|
||
else {
|
||
if (out.status !== 200) fail('C.兜底路径', 'status=' + out.status + ' body=' + out.body.slice(0, 200))
|
||
if (!out.body.includes('MARKER_C')) fail('C.兜底路径', 'content mismatch: ' + out.body.slice(0, 200))
|
||
if (!out.stats) fail('C.兜底路径', '__wtLoadProbeStats missing')
|
||
else {
|
||
if (out.stats.homeSource !== 'fallback') fail('C.兜底路径', 'homeSource=' + out.stats.homeSource + ' (expect fallback)')
|
||
if (out.stats.attempts > PROBE_BOUND) fail('C.兜底路径', 'probe attempts=' + out.stats.attempts + ' > ' + PROBE_BOUND + '(疑似循环重入)')
|
||
}
|
||
}
|
||
if (!failures.some((f) => f.startsWith('C.'))) ok('C. 官方包不可解析 → baseDshHome 兜底(homeSource=fallback + 探测有界 + 内容正确)')
|
||
}
|
||
|
||
// —— D/E/F. ~ / ~/ / ~\ 前缀展开 + 相对路径:完整绝对路径断言 ——
|
||
{
|
||
const cases = [
|
||
['D.波浪号', '~/__wt_probe_tilde__'],
|
||
['E.相对路径', '__wt_probe_rel__'],
|
||
['F.反斜杠波浪号', '~' + String.fromCharCode(92) + '__wt_probe_bs__'],
|
||
]
|
||
for (const [label, envVal] of cases) {
|
||
const out = runChild(envVal, label.replace(/^\w\./, ''))
|
||
if (!out) { fail(label, 'child did not return'); continue }
|
||
if (out.status !== 404) { fail(label, 'expect 404, got ' + out.status + ' ' + out.body.slice(0, 120)); continue }
|
||
if (out.fullMatch !== true) fail(label, 'error missing FULL path: expected ' + out.expectedFull + ' body=' + out.body.slice(0, 200))
|
||
}
|
||
if (!failures.some((f) => /^[DEF]\./.test(f))) ok('D+E+F. ~ 与 ~/ 与 ~\\ 前缀展开 + 相对路径按 cwd 解析(完整绝对路径断言)')
|
||
}
|
||
|
||
console.log('all server-home tests passed: ' + pass + ' scenarios, ' + failures.length + ' failures')
|
||
process.exitCode = failures.length > 0 ? 1 : 0
|