122 lines
4.7 KiB
JavaScript
122 lines
4.7 KiB
JavaScript
|
|
/**
|
|||
|
|
* 依赖方向检查的负例用例(任务 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));
|
|||
|
|
},
|
|||
|
|
);
|
|||
|
|
});
|