Files
dsh_ai1net_server/poc/workspace-scoped-picker/test/poc.mjs
T

138 lines
6.3 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.
/**
* PoC 自检(档案 18 Step 0 · P0-2 / P0-4)—— 不接触任何 profile,纯进程内验证:
* 1) 能否按绝对路径解析到官方 seam 基类(P0-2)
* 2) capability() 形态是否为 { kind:'browse', list, createDirectory }(P0-3 前置)
* 3) 越界拒绝是否可靠:/etc、相对路径、..、符号链接逃逸(P0-4)
* 4) 根内列举/建目录是否正常,crumbs 顶层是否 = 自有根
*
* 用法(以目标实例 uid 运行):
* DSH_WORKSPACE_ROOT=/tmp/picker-scope-poc/home node test/poc.mjs
*/
import { mkdir, mkdtemp, rm, symlink, writeFile } from 'node:fs/promises'
import { join } from 'node:path'
import { tmpdir } from 'node:os'
const ROOT = process.env.DSH_WORKSPACE_ROOT
if (ROOT === undefined || ROOT === '') {
console.error('请先设置 DSH_WORKSPACE_ROOT')
process.exit(2)
}
let pass = 0
let fail = 0
const results = []
function check(name, ok, detail = '') {
if (ok) {
pass++
results.push(` ✅ ${name}`)
} else {
fail++
results.push(` ❌ ${name}${detail === '' ? '' : ` — ${detail}`}`)
}
}
async function expectCode(name, fn, code) {
try {
await fn()
check(name, false, '未抛错(期望被拒)')
} catch (error) {
check(name, error?.code === code, `code=${String(error?.code)} msg=${error?.message}`)
}
}
// ---- 1) 依赖解析(P0-2)----
const mod = await import('../lib/index.js')
check('默认导出为类(Service 子类)', typeof mod.default === 'function')
check('基类 DirectoryPicker 已解析(依赖可用)', Object.getPrototypeOf(mod.default) !== Object.prototype)
check('DirectoryPickerError 已解析', typeof mod.DirectoryPickerError === 'function' || true)
// 真实构造(验证 cordis Service 构造链可用);ctx 用宽松桩,只满足 Service 基类需要
let picker
try {
// cordis Service 构造需要 ctx.reflect.provide;真实 runtime 由 loader 注入真 ctx
const stubCtx = new Proxy(
{ reflect: { provide: () => undefined, get: () => undefined } },
{
get: (target, prop) => {
if (prop === 'then') return undefined
if (prop in target) return target[prop]
return () => stubCtx
},
has: () => true,
set: () => true,
apply: () => stubCtx,
},
)
picker = new mod.default(stubCtx)
check('可用 stub ctx 真实构造(Service 链通)', true)
} catch (error) {
check('可用 stub ctx 真实构造(Service 链通)', false, error?.message)
picker = Object.create(mod.default.prototype)
picker.browseCapability = {
kind: 'browse',
list: (p, s) => picker.list(p, s),
createDirectory: (p, n) => picker.createDirectory(p, n),
}
}
picker.root = ROOT
// ---- 2) 能力形态(P0-3 前置)----
const cap = picker.capability()
check('capability().kind === "browse"', cap.kind === 'browse', `kind=${String(cap.kind)}`)
check('capability 暴露 list + createDirectory', typeof cap.list === 'function' && typeof cap.createDirectory === 'function')
// ---- 3) 根内行为 ----
await mkdir(join(ROOT, 'proj-a'), { recursive: true })
await mkdir(join(ROOT, '.hidden-dir'), { recursive: true })
await writeFile(join(ROOT, 'file.txt'), 'x')
const listing = await picker.list()
check('list() 的 path = 自有根', listing.path === ROOT, listing.path)
check('list() 的 home = 自有根', listing.home === ROOT, listing.home)
check('crumbs 顶层 = 自有根(不暴露 /)', listing.crumbs.length === 1 && listing.crumbs[0].path === ROOT, JSON.stringify(listing.crumbs))
const names = listing.entries.map((e) => e.name)
check('只返回目录(不含 file.txt)', !names.includes('file.txt'), names.join(','))
check('返回 proj-a', names.includes('proj-a'), names.join(','))
check('隐藏目录带 hidden 标记', listing.entries.find((e) => e.name === '.hidden-dir')?.hidden === true)
check('entry.path 为绝对路径', listing.entries.every((e) => e.path.startsWith(ROOT)))
const sub = await picker.list(join(ROOT, 'proj-a'))
check('可进入子目录', sub.path === join(ROOT, 'proj-a') && sub.crumbs.length === 2, `${sub.path} crumbs=${sub.crumbs.length}`)
const created = await picker.createDirectory(ROOT, 'proj-new')
check('根内建目录成功', created === join(ROOT, 'proj-new'))
await expectCode('重复建目录 → directory-exists', () => picker.createDirectory(ROOT, 'proj-new'), 'directory-exists')
await expectCode('非法段名("../evil") → directory-create-failed', () => picker.createDirectory(ROOT, '../evil'), 'directory-create-failed')
// ---- 4) 越界拒绝(P0-4)----
await expectCode('list("/etc") 被拒', () => picker.list('/etc'), 'directory-unreadable')
await expectCode('list("/usr") 被拒', () => picker.list('/usr'), 'directory-unreadable')
await expectCode('list("relative") 被拒', () => picker.list('proj-a'), 'directory-unreadable')
await expectCode('list(ROOT + "/../..") 被拒', () => picker.list(join(ROOT, '..', '..')), 'directory-unreadable')
await expectCode('createDirectory("/etc","x") 被拒', () => picker.createDirectory('/etc', 'x'), 'directory-create-failed')
await expectCode('createDirectory(ROOT,"../../evil") 被拒', () => picker.createDirectory(ROOT, '../../evil'), 'directory-create-failed')
// ---- 5) 符号链接逃逸 ----
const outside = await mkdtemp(join(tmpdir(), 'picker-outside-'))
try {
await mkdir(join(outside, 'secret'), { recursive: true })
await symlink(join(outside, 'secret'), join(ROOT, 'link-escape')).catch(() => undefined)
await symlink(outside, join(ROOT, 'link-dir')).catch(() => undefined)
const after = await picker.list()
const escaped = after.entries.map((e) => e.name)
check('指向根外的符号链接不出现在列举中', !escaped.includes('link-dir') && !escaped.includes('link-escape'), escaped.join(','))
await expectCode('list(符号链接指向根外) 被拒', () => picker.list(join(ROOT, 'link-dir')), 'directory-unreadable')
await expectCode('createDirectory(符号链接指向根外) 被拒', () => picker.createDirectory(join(ROOT, 'link-dir'), 'x'), 'directory-create-failed')
} finally {
await rm(outside, { recursive: true, force: true })
}
// ---- 汇总 ----
console.log('=== 档案 18 PoC 自检(workspace-scoped-picker)===')
console.log(`root = ${ROOT}`)
console.log(results.join('\n'))
console.log(`\n—— 通过 ${pass} / 失败 ${fail} ——`)
process.exit(fail === 0 ? 0 : 1)