aps-agent/scripts/check-dependency-direction....

122 lines
4.7 KiB
JavaScript
Raw Normal View History

/**
* 依赖方向检查的负例用例(任务 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(
{ '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));
},
);
});