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

138 lines
5.9 KiB
TypeScript
Raw Permalink 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, ImpactPreview, Provider, ProviderManifest } from './index.js';
export interface FakeProvider extends Provider {
stats: {
calls: Record<string, number>;
/** 每次调用拿到的身份与凭据句柄形态:给 A4 的凭据用例断言「凭据只走句柄、不进参数」 */
contexts: Array<{ executionId: string; accountId?: string; hasCredentials: boolean; params: unknown }>;
};
}
export function createFakeProvider(id = 'fake'): FakeProvider {
const calls: Record<string, number> = {};
const contexts: FakeProvider['stats']['contexts'] = [];
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, contexts },
async describe(): Promise<ProviderManifest> {
// 假 Provider 没有账号概念:不需要身份绑定(真实系统默认需要,见 ProviderManifest.requiresIdentity)
return { id, displayName: '假 Provider', version: '0.0.0', platformVersion: 1, requiresIdentity: false, 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;
contexts.push({
executionId: context.executionId,
...(context.identity === undefined ? {} : { accountId: context.identity.accountId }),
hasCredentials: context.credentials !== undefined,
params: call.params,
});
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() };
},
async preview(_context: ExecutionContext, call: CapabilityCall): Promise<ImpactPreview> {
const params = (call.params ?? {}) as Record<string, unknown>;
if (call.capabilityId !== `${id}.write`) {
throw new ProviderError('invalid_argument', `假 Provider 没有 ${call.capabilityId} 的影响预览`);
}
return {
impactSummary: `会把值写成 ${String(params.value ?? '')}(假 Provider,不碰真实数据)`,
revision: 7,
paramsPreview: { value: params.value ?? null },
};
},
};
}