2026-09-24 15:48:45 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* 假 Provider(任务 A2.3):给契约测试与 Core 单测当靶子,不接任何真实系统。
|
|
|
|
|
|
*
|
|
|
|
|
|
* 能力刻意覆盖几条分支:读、写、慢调用(测超时)、声明错误分类(测映射)、裸异常(测未知结果)。
|
|
|
|
|
|
* 它带调用计数,便于断言「幂等重试只执行一次」。
|
|
|
|
|
|
*/
|
|
|
|
|
|
import type { ExecutionReceipt } from '@aps/contracts';
|
|
|
|
|
|
|
|
|
|
|
|
import { ProviderError } from './errors.js';
|
2026-09-29 10:59:34 +08:00
|
|
|
|
import type { Capability, CapabilityCall, ExecutionContext, HealthStatus, ImpactPreview, Provider, ProviderManifest } from './index.js';
|
2026-09-24 15:48:45 +08:00
|
|
|
|
|
|
|
|
|
|
export interface FakeProvider extends Provider {
|
2026-09-29 10:59:34 +08:00
|
|
|
|
stats: {
|
|
|
|
|
|
calls: Record<string, number>;
|
|
|
|
|
|
/** 每次调用拿到的身份与凭据句柄形态:给 A4 的凭据用例断言「凭据只走句柄、不进参数」 */
|
|
|
|
|
|
contexts: Array<{ executionId: string; accountId?: string; hasCredentials: boolean; params: unknown }>;
|
|
|
|
|
|
};
|
2026-09-24 15:48:45 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
export function createFakeProvider(id = 'fake'): FakeProvider {
|
|
|
|
|
|
const calls: Record<string, number> = {};
|
2026-09-29 10:59:34 +08:00
|
|
|
|
const contexts: FakeProvider['stats']['contexts'] = [];
|
2026-09-24 15:48:45 +08:00
|
|
|
|
|
|
|
|
|
|
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 {
|
2026-09-29 10:59:34 +08:00
|
|
|
|
stats: { calls, contexts },
|
2026-09-24 15:48:45 +08:00
|
|
|
|
async describe(): Promise<ProviderManifest> {
|
2026-09-29 10:59:34 +08:00
|
|
|
|
// 假 Provider 没有账号概念:不需要身份绑定(真实系统默认需要,见 ProviderManifest.requiresIdentity)
|
|
|
|
|
|
return { id, displayName: '假 Provider', version: '0.0.0', platformVersion: 1, requiresIdentity: false, capabilities };
|
2026-09-24 15:48:45 +08:00
|
|
|
|
},
|
|
|
|
|
|
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;
|
2026-09-29 10:59:34 +08:00
|
|
|
|
contexts.push({
|
|
|
|
|
|
executionId: context.executionId,
|
|
|
|
|
|
...(context.identity === undefined ? {} : { accountId: context.identity.accountId }),
|
|
|
|
|
|
hasCredentials: context.credentials !== undefined,
|
|
|
|
|
|
params: call.params,
|
|
|
|
|
|
});
|
2026-09-24 15:48:45 +08:00
|
|
|
|
|
|
|
|
|
|
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() };
|
|
|
|
|
|
},
|
2026-09-29 10:59:34 +08:00
|
|
|
|
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 },
|
|
|
|
|
|
};
|
|
|
|
|
|
},
|
2026-09-24 15:48:45 +08:00
|
|
|
|
};
|
|
|
|
|
|
}
|