Files
dsh-worktable/04_test/server-home.test.mjs
T

228 lines
13 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 服务端数据目录解析回归测试(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