/** * 依赖方向检查的负例用例(任务 A1.2 的验证方式)。 * 每个负例都要求检查器真的报错——只测正例等于没测。 */ import assert from 'node:assert/strict'; import { mkdtemp, mkdir, rm, writeFile } from 'node:fs/promises'; import { tmpdir } from 'node:os'; import { dirname, join } from 'node:path'; import { test } from 'node:test'; import { fileURLToPath } from 'node:url'; import { findViolations } from './check-dependency-direction.mjs'; const REPO_ROOT = fileURLToPath(new URL('..', import.meta.url)); const WORKSPACE = ['contracts', 'server', 'server/provider-sdk', 'providers/*', 'client/*']; /** 造一个最小工作区;extra 覆盖或新增文件(键为相对路径)。 */ async function makeWorkspace(extra = {}) { const root = await mkdtemp(join(tmpdir(), 'depcheck-')); const files = { 'pnpm-workspace.yaml': `packages:\n${WORKSPACE.map((p) => ` - ${p}\n`).join('')}`, 'contracts/package.json': JSON.stringify({ name: '@aps/contracts' }), 'server/package.json': JSON.stringify({ name: '@aps/server', dependencies: { '@aps/contracts': 'workspace:*', '@aps/provider-sdk': 'workspace:*' } }), 'server/provider-sdk/package.json': JSON.stringify({ name: '@aps/provider-sdk', dependencies: { '@aps/contracts': 'workspace:*' } }), 'providers/aps/package.json': JSON.stringify({ name: '@aps/provider-aps', dependencies: { '@aps/provider-sdk': 'workspace:*' } }), 'client/app/package.json': JSON.stringify({ name: '@aps/client-app', dependencies: { '@aps/contracts': 'workspace:*' } }), ...extra, }; for (const [rel, content] of Object.entries(files)) { const file = join(root, rel); await mkdir(dirname(file), { recursive: true }); await writeFile(file, content, 'utf8'); } return root; } async function withWorkspace(extra, run) { const root = await makeWorkspace(extra); try { return await run(root); } finally { await rm(root, { recursive: true, force: true }); } } const reasons = (violations) => violations.map((v) => `${v.file} → ${v.specifier}:${v.reason}`).join('\n'); test('正例:当前仓库无违规', async () => { assert.deepEqual(await findViolations(REPO_ROOT), []); }); test('负例:core 引用具体 Provider 被拦下', async () => { await withWorkspace( { 'server/core/router.ts': "import type { CapabilityProvider } from '@aps/provider-aps';\nexport type X = CapabilityProvider;\n", }, async (root) => { const violations = await findViolations(root); assert.ok(violations.some((v) => v.file.endsWith('server/core/router.ts') && v.specifier === '@aps/provider-aps'), reasons(violations)); }, ); }); test('负例:业务包直连 Provider 被拦下', async () => { await withWorkspace( { 'client/app/direct.ts': "import '@aps/provider-aps';\n" }, async (root) => { const violations = await findViolations(root); assert.ok(violations.some((v) => v.package === '@aps/client-app' && v.specifier === '@aps/provider-aps'), reasons(violations)); }, ); }); test('负例:客户端引用服务端被拦下', async () => { await withWorkspace( { 'client/app/server-call.ts': "import type { ExecutionReceipt } from '@aps/server';\nexport type R = ExecutionReceipt;\n" }, async (root) => { const violations = await findViolations(root); assert.ok(violations.some((v) => v.specifier === '@aps/server'), reasons(violations)); }, ); }); test('负例:非 agent 模块引用引擎包被拦下', async () => { await withWorkspace( { 'server/entry/boot.ts': "import '@earendil-works/pi-agent-core';\n" }, async (root) => { const violations = await findViolations(root); assert.ok(violations.some((v) => v.file.endsWith('server/entry/boot.ts')), reasons(violations)); }, ); }); test('正例:server/agent 引用引擎包放行', async () => { await withWorkspace( { 'server/agent/runtime.ts': "import '@earendil-works/pi-agent-core';\n" }, async (root) => { assert.deepEqual(await findViolations(root), []); }, ); }); test('正例:允许包的子路径导入不算违规', async () => { await withWorkspace({ 'server/agent/importer.ts': "import { createFakeProvider } from '@aps/provider-sdk/testing';\nexport const p = createFakeProvider;\n" }, async (root) => { assert.deepEqual(await findViolations(root), []); }); }); test('负例:相对路径逃出包边界被拦下', async () => { await withWorkspace( { 'client/app/escape.ts': "import '../../../providers/aps/index.js';\n" }, async (root) => { const violations = await findViolations(root); assert.ok(violations.some((v) => v.reason === '相对路径逃出包边界'), reasons(violations)); }, ); }); test('负例:未登记的包名被拦下', async () => { await withWorkspace( { 'providers/erp/package.json': JSON.stringify({ name: '@aps/provider-erp' }) }, async (root) => { const violations = await findViolations(root); assert.ok(violations.some((v) => v.reason.includes('未登记的包名')), reasons(violations)); }, ); });