138 lines
6.3 KiB
JavaScript
138 lines
6.3 KiB
JavaScript
/**
|
||
* 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)
|