aps-agent/server/provider-sdk/testing.ts

115 lines
4.6 KiB
TypeScript
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.

/**
* 假 Provider(任务 A2.3):给契约测试与 Core 单测当靶子,不接任何真实系统。
*
* 能力刻意覆盖几条分支:读、写、慢调用(测超时)、声明错误分类(测映射)、裸异常(测未知结果)。
* 它带调用计数,便于断言「幂等重试只执行一次」。
*/
import type { ExecutionReceipt } from '@aps/contracts';
import { ProviderError } from './errors.js';
import type { Capability, CapabilityCall, ExecutionContext, HealthStatus, Provider, ProviderManifest } from './index.js';
export interface FakeProvider extends Provider {
stats: { calls: Record<string, number> };
}
export function createFakeProvider(id = 'fake'): FakeProvider {
const calls: Record<string, number> = {};
const capabilities: Capability[] = [
{
id: `${id}.echo`,
title: '回显输入',
description: '把输入原样返回,用于联调链路',
effect: 'read',
mode: 'sync',
whenNotToUse: '需要真实业务数据时不要用',
synonyms: ['回显', '测试'],
inputSchema: { type: 'object', properties: { text: { type: 'string' } }, required: ['text'] },
outputSchema: { type: 'object', properties: { echo: { type: 'string' } } },
idempotency: 'none',
errors: ['invalid_argument'],
},
{
id: `${id}.write`,
title: '假写入',
description: '模拟一次写操作,用于验证幂等与确认链路',
effect: 'write',
mode: 'sync',
inputSchema: { type: 'object', properties: { value: { type: 'number' } } },
outputSchema: { type: 'object', properties: { applied: { type: 'boolean' } } },
idempotency: 'key',
errors: ['invalid_argument', 'conflict'],
},
{
id: `${id}.slow`,
title: '慢调用',
description: '按参数等待若干毫秒,用于验证超时与结果未知',
effect: 'read',
mode: 'sync',
inputSchema: { type: 'object', properties: { ms: { type: 'number' } } },
outputSchema: { type: 'object' },
idempotency: 'none',
errors: ['unknown_result'],
},
{
id: `${id}.declare-error`,
title: '声明错误',
description: '按参数抛出指定分类的错误,用于验证错误归一化',
effect: 'read',
mode: 'sync',
inputSchema: { type: 'object', properties: { kind: { type: 'string' } } },
outputSchema: { type: 'object' },
idempotency: 'none',
errors: ['invalid_argument', 'forbidden', 'conflict', 'rate_limited', 'unavailable', 'unknown_result'],
},
{
id: `${id}.crash`,
title: '裸异常',
description: '抛裸异常,验证归一为 unknown_result',
effect: 'read',
mode: 'sync',
inputSchema: { type: 'object' },
outputSchema: { type: 'object' },
idempotency: 'none',
errors: ['unknown_result'],
},
];
return {
stats: { calls },
async describe(): Promise<ProviderManifest> {
return { id, displayName: '假 Provider', version: '0.0.0', platformVersion: 1, capabilities };
},
async invoke(context: ExecutionContext, call: CapabilityCall): Promise<ExecutionReceipt> {
const known = capabilities.find((capability) => capability.id === call.capabilityId);
if (!known) throw new ProviderError('invalid_argument', `假 Provider 没有能力 ${call.capabilityId}`);
calls[call.capabilityId] = (calls[call.capabilityId] ?? 0) + 1;
const params = (call.params ?? {}) as Record<string, unknown>;
switch (known.id) {
case `${id}.echo`:
if (typeof params.text !== 'string' || params.text === '') {
throw new ProviderError('invalid_argument', 'text 不能为空');
}
return { executionId: context.executionId, status: 'succeeded', result: { echo: params.text } };
case `${id}.write`:
return { executionId: context.executionId, status: 'succeeded', result: { applied: true, idempotencyKey: context.idempotencyKey ?? null } };
case `${id}.slow`: {
const ms = typeof params.ms === 'number' ? params.ms : 100;
await new Promise((resolve) => setTimeout(resolve, ms));
return { executionId: context.executionId, status: 'succeeded', result: { waitedMs: ms } };
}
case `${id}.declare-error`: {
const kind = typeof params.kind === 'string' ? params.kind : 'unavailable';
throw new ProviderError(kind as ProviderError['kind'], `按要求抛出 ${kind}`);
}
default:
throw new Error('假 Provider 内部异常:没有兜底分支');
}
},
async health(): Promise<HealthStatus> {
return { status: 'ok', checkedAt: Date.now() };
},
};
}