chore: import upstream snapshot with attribution
CI / Shell Format Check (push) Has been cancelled
CI / Check Ruby (3.4) (push) Has been cancelled
CI / CI Config (push) Has been cancelled
CI / Test on Node ${{ matrix.node }} and ${{ matrix.os }}${{ matrix.shard && format(' (shard {0}/3)', matrix.shard) || '' }} (push) Has been cancelled
CI / Build on Node ${{ matrix.node }} (push) Has been cancelled
CI / Style Check (push) Has been cancelled
CI / Generate Assets (push) Has been cancelled
CI / Check Python (3.14) (push) Has been cancelled
CI / Check Python (3.9) (push) Has been cancelled
CI / Build Docs (push) Has been cancelled
CI / Code Scan Action (push) Has been cancelled
CI / Site tests (push) Has been cancelled
CI / webui tests (push) Has been cancelled
CI / Run Integration Tests (push) Has been cancelled
CI / Run Smoke Tests (push) Has been cancelled
CI / Go Tests (push) Has been cancelled
CI / Share Test (push) Has been cancelled
CI / Redteam (Production API) (push) Has been cancelled
CI / Redteam (Staging API) (push) Has been cancelled
CI / GitHub Actions Lint (push) Has been cancelled
CI / Check Ruby (3.0) (push) Has been cancelled
release-please / release-please (push) Has been cancelled
release-please / build (push) Has been cancelled
release-please / publish-npm (push) Has been cancelled
release-please / publish-npm-backfill (push) Has been cancelled
release-please / docker (push) Has been cancelled
release-please / publish-code-scan-action (push) Has been cancelled
release-please / attest-code-scan-action (push) Has been cancelled
Deploy local.promptfoo.app / Deploy to Cloudflare Pages (push) Has been cancelled
Test and Publish Multi-arch Docker Image / test (push) Has been cancelled
Test and Publish Multi-arch Docker Image / build-docker-and-push-digests (map[digest-suffix:linux-amd64 platform:linux/amd64 runner:ubuntu-latest]) (push) Has been cancelled
Test and Publish Multi-arch Docker Image / build-docker-and-push-digests (map[digest-suffix:linux-arm64 platform:linux/arm64 runner:ubuntu-24.04-arm]) (push) Has been cancelled
Test and Publish Multi-arch Docker Image / merge-docker-digests (push) Has been cancelled
Test and Publish Multi-arch Docker Image / Attest Multi-arch Image (push) Has been cancelled
Validate Renovate Config / Validate Renovate Configuration (push) Has been cancelled
CI / Shell Format Check (push) Has been cancelled
CI / Check Ruby (3.4) (push) Has been cancelled
CI / CI Config (push) Has been cancelled
CI / Test on Node ${{ matrix.node }} and ${{ matrix.os }}${{ matrix.shard && format(' (shard {0}/3)', matrix.shard) || '' }} (push) Has been cancelled
CI / Build on Node ${{ matrix.node }} (push) Has been cancelled
CI / Style Check (push) Has been cancelled
CI / Generate Assets (push) Has been cancelled
CI / Check Python (3.14) (push) Has been cancelled
CI / Check Python (3.9) (push) Has been cancelled
CI / Build Docs (push) Has been cancelled
CI / Code Scan Action (push) Has been cancelled
CI / Site tests (push) Has been cancelled
CI / webui tests (push) Has been cancelled
CI / Run Integration Tests (push) Has been cancelled
CI / Run Smoke Tests (push) Has been cancelled
CI / Go Tests (push) Has been cancelled
CI / Share Test (push) Has been cancelled
CI / Redteam (Production API) (push) Has been cancelled
CI / Redteam (Staging API) (push) Has been cancelled
CI / GitHub Actions Lint (push) Has been cancelled
CI / Check Ruby (3.0) (push) Has been cancelled
release-please / release-please (push) Has been cancelled
release-please / build (push) Has been cancelled
release-please / publish-npm (push) Has been cancelled
release-please / publish-npm-backfill (push) Has been cancelled
release-please / docker (push) Has been cancelled
release-please / publish-code-scan-action (push) Has been cancelled
release-please / attest-code-scan-action (push) Has been cancelled
Deploy local.promptfoo.app / Deploy to Cloudflare Pages (push) Has been cancelled
Test and Publish Multi-arch Docker Image / test (push) Has been cancelled
Test and Publish Multi-arch Docker Image / build-docker-and-push-digests (map[digest-suffix:linux-amd64 platform:linux/amd64 runner:ubuntu-latest]) (push) Has been cancelled
Test and Publish Multi-arch Docker Image / build-docker-and-push-digests (map[digest-suffix:linux-arm64 platform:linux/arm64 runner:ubuntu-24.04-arm]) (push) Has been cancelled
Test and Publish Multi-arch Docker Image / merge-docker-digests (push) Has been cancelled
Test and Publish Multi-arch Docker Image / Attest Multi-arch Image (push) Has been cancelled
Validate Renovate Config / Validate Renovate Configuration (push) Has been cancelled
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,122 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { createTransformResponse } from '../../../src/providers/a2a/transforms';
|
||||
|
||||
describe('A2A createTransformResponse', () => {
|
||||
const finalResponse = {
|
||||
events: [{ statusUpdate: { status: { state: 'TASK_STATE_COMPLETED' } } }],
|
||||
message: {
|
||||
parts: [{ text: 'hello from message' }],
|
||||
role: 'ROLE_AGENT',
|
||||
},
|
||||
raw: {
|
||||
message: {
|
||||
parts: [{ text: 'hello from message' }],
|
||||
role: 'ROLE_AGENT',
|
||||
},
|
||||
},
|
||||
task: {
|
||||
id: 'task-1',
|
||||
status: { state: 'TASK_STATE_COMPLETED' },
|
||||
},
|
||||
};
|
||||
|
||||
const context = {
|
||||
events: finalResponse.events,
|
||||
message: finalResponse.message,
|
||||
mode: 'stream' as const,
|
||||
raw: finalResponse.raw,
|
||||
task: finalResponse.task,
|
||||
};
|
||||
|
||||
it('returns extracted text when no transform is configured', async () => {
|
||||
const transform = createTransformResponse(undefined);
|
||||
|
||||
await expect(transform(finalResponse, 'fallback text', context)).resolves.toEqual({
|
||||
output: 'fallback text',
|
||||
});
|
||||
});
|
||||
|
||||
it('passes json, text, and context to function transforms', async () => {
|
||||
const transform = createTransformResponse((json: any, text: string, transformContext: any) => ({
|
||||
metadata: {
|
||||
eventCount: transformContext.events.length,
|
||||
mode: transformContext.mode,
|
||||
taskId: json.task.id,
|
||||
},
|
||||
output: text.toUpperCase(),
|
||||
}));
|
||||
|
||||
await expect(transform(finalResponse, 'fallback text', context)).resolves.toEqual({
|
||||
metadata: {
|
||||
eventCount: 1,
|
||||
mode: 'stream',
|
||||
taskId: 'task-1',
|
||||
},
|
||||
output: 'FALLBACK TEXT',
|
||||
});
|
||||
});
|
||||
|
||||
it('wraps primitive function transform results as provider output', async () => {
|
||||
const transform = createTransformResponse((json: any) => json.task.id);
|
||||
|
||||
await expect(transform(finalResponse, 'fallback text', context)).resolves.toEqual({
|
||||
output: 'task-1',
|
||||
});
|
||||
});
|
||||
|
||||
it('supports string expressions with json and result aliases', async () => {
|
||||
const transform = createTransformResponse(
|
||||
'({ output: `${json.task.id}:${result.message.role}:${text}:${context.mode}` })',
|
||||
);
|
||||
|
||||
await expect(transform(finalResponse, 'fallback text', context)).resolves.toEqual({
|
||||
output: 'task-1:ROLE_AGENT:fallback text:stream',
|
||||
});
|
||||
});
|
||||
|
||||
it('supports async string function expressions', async () => {
|
||||
const transform = createTransformResponse('async (json, text) => `${json.task.id}:${text}`');
|
||||
|
||||
await expect(transform(finalResponse, 'fallback text', context)).resolves.toEqual({
|
||||
output: 'task-1:fallback text',
|
||||
});
|
||||
});
|
||||
|
||||
it('wraps object string-expression results without provider response fields', async () => {
|
||||
const transform = createTransformResponse('({ taskId: result.task.id })');
|
||||
|
||||
await expect(transform(finalResponse, 'fallback text', context)).resolves.toEqual({
|
||||
output: { taskId: 'task-1' },
|
||||
});
|
||||
});
|
||||
|
||||
it('throws if file transforms are not pre-loaded', () => {
|
||||
expect(() => createTransformResponse('file://parser.js')).toThrow(
|
||||
/should be pre-loaded before calling createTransformResponse/,
|
||||
);
|
||||
});
|
||||
|
||||
it('rejects unsupported transform types', () => {
|
||||
expect(() => createTransformResponse(123 as any)).toThrow(
|
||||
/Unsupported response transform type: number/,
|
||||
);
|
||||
});
|
||||
|
||||
it('preserves errors from function transforms', async () => {
|
||||
const transform = createTransformResponse(() => {
|
||||
throw new Error('function transform failed');
|
||||
});
|
||||
|
||||
await expect(transform(finalResponse, 'fallback text', context)).rejects.toThrow(
|
||||
'function transform failed',
|
||||
);
|
||||
});
|
||||
|
||||
it('wraps errors from string transforms', async () => {
|
||||
const transform = createTransformResponse('result.missing.value');
|
||||
|
||||
await expect(transform(finalResponse, 'fallback text', context)).rejects.toThrow(
|
||||
/Failed to transform A2A response/,
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,207 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import {
|
||||
A2AAgentCardSchema,
|
||||
A2AAuthSchema,
|
||||
A2AProviderConfigSchema,
|
||||
A2AStreamResponseSchema,
|
||||
A2ATaskSchema,
|
||||
} from '../../../src/providers/a2a/types';
|
||||
|
||||
describe('A2A type schemas', () => {
|
||||
it('applies provider config defaults', () => {
|
||||
const config = A2AProviderConfigSchema.parse({});
|
||||
|
||||
expect(config.mode).toBe('auto');
|
||||
expect(config.polling).toEqual({
|
||||
enabled: true,
|
||||
intervalMs: 1000,
|
||||
timeoutMs: 300000,
|
||||
});
|
||||
});
|
||||
|
||||
it.each(['', 'none', 'no_auth'])('normalizes %s auth to undefined', (type) => {
|
||||
expect(A2AAuthSchema.parse({ type })).toBeUndefined();
|
||||
});
|
||||
|
||||
it('parses bearer and basic auth configs', () => {
|
||||
expect(A2AAuthSchema.parse({ token: 'token-1', type: 'bearer' })).toEqual({
|
||||
token: 'token-1',
|
||||
type: 'bearer',
|
||||
});
|
||||
expect(
|
||||
A2AAuthSchema.parse({
|
||||
password: 'pass-1',
|
||||
type: 'basic',
|
||||
username: 'user-1',
|
||||
}),
|
||||
).toEqual({
|
||||
password: 'pass-1',
|
||||
type: 'basic',
|
||||
username: 'user-1',
|
||||
});
|
||||
});
|
||||
|
||||
it('accepts api key auth with either value field', () => {
|
||||
expect(
|
||||
A2AAuthSchema.parse({
|
||||
keyName: 'X-API-Key',
|
||||
placement: 'header',
|
||||
type: 'api_key',
|
||||
value: 'secret-1',
|
||||
}),
|
||||
).toEqual({
|
||||
keyName: 'X-API-Key',
|
||||
placement: 'header',
|
||||
type: 'api_key',
|
||||
value: 'secret-1',
|
||||
});
|
||||
expect(
|
||||
A2AAuthSchema.parse({
|
||||
api_key: 'secret-2',
|
||||
placement: 'query',
|
||||
type: 'api_key',
|
||||
}),
|
||||
).toEqual({
|
||||
api_key: 'secret-2',
|
||||
placement: 'query',
|
||||
type: 'api_key',
|
||||
});
|
||||
});
|
||||
|
||||
it('rejects api key auth without a value', () => {
|
||||
expect(() => A2AAuthSchema.parse({ type: 'api_key' })).toThrow(
|
||||
/api_key auth requires value or api_key/,
|
||||
);
|
||||
});
|
||||
|
||||
it('preserves oauth client credentials string scopes until render time', () => {
|
||||
expect(
|
||||
A2AAuthSchema.parse({
|
||||
clientId: 'client-1',
|
||||
clientSecret: 'secret-1',
|
||||
scopes: 'agent:read agent:write,profile',
|
||||
tokenUrl: 'https://agent.example.com/oauth/token',
|
||||
type: 'oauth',
|
||||
}),
|
||||
).toEqual({
|
||||
clientId: 'client-1',
|
||||
clientSecret: 'secret-1',
|
||||
grantType: 'client_credentials',
|
||||
scopes: 'agent:read agent:write,profile',
|
||||
tokenUrl: 'https://agent.example.com/oauth/token',
|
||||
type: 'oauth',
|
||||
});
|
||||
});
|
||||
|
||||
it('preserves templated oauth scope strings with spaced Nunjucks delimiters', () => {
|
||||
expect(
|
||||
A2AAuthSchema.parse({
|
||||
clientId: 'client-1',
|
||||
clientSecret: 'secret-1',
|
||||
scopes: '{{ env.A2A_SCOPES }}',
|
||||
type: 'oauth',
|
||||
}),
|
||||
).toMatchObject({
|
||||
scopes: '{{ env.A2A_SCOPES }}',
|
||||
});
|
||||
});
|
||||
|
||||
it('parses oauth password auth with array scopes', () => {
|
||||
expect(
|
||||
A2AAuthSchema.parse({
|
||||
grantType: 'password',
|
||||
password: 'pass-1',
|
||||
scopes: ['agent:read', 'agent:write'],
|
||||
type: 'oauth',
|
||||
username: 'user-1',
|
||||
}),
|
||||
).toEqual({
|
||||
grantType: 'password',
|
||||
password: 'pass-1',
|
||||
scopes: ['agent:read', 'agent:write'],
|
||||
type: 'oauth',
|
||||
username: 'user-1',
|
||||
});
|
||||
});
|
||||
|
||||
it('parses task, stream, and agent card data models with loose extension fields', () => {
|
||||
expect(
|
||||
A2ATaskSchema.parse({
|
||||
artifacts: [{ custom: true, parts: [{ text: { text: 'artifact text' } }] }],
|
||||
history: [{ parts: [{ text: 'history text' }], role: 'ROLE_USER' }],
|
||||
id: 'task-1',
|
||||
status: {
|
||||
message: { parts: [{ text: 'done' }], role: 'ROLE_AGENT' },
|
||||
state: 'TASK_STATE_COMPLETED',
|
||||
},
|
||||
}),
|
||||
).toMatchObject({
|
||||
artifacts: [{ custom: true, parts: [{ text: { text: 'artifact text' } }] }],
|
||||
id: 'task-1',
|
||||
status: { state: 'TASK_STATE_COMPLETED' },
|
||||
});
|
||||
|
||||
expect(
|
||||
A2AStreamResponseSchema.parse({
|
||||
artifactUpdate: {
|
||||
artifact: { artifactId: 'artifact-1', parts: [{ data: { ok: true } }] },
|
||||
taskId: 'task-1',
|
||||
},
|
||||
statusUpdate: {
|
||||
status: { state: 'TASK_STATE_WORKING' },
|
||||
taskId: 'task-1',
|
||||
},
|
||||
}),
|
||||
).toMatchObject({
|
||||
artifactUpdate: { artifact: { artifactId: 'artifact-1' }, taskId: 'task-1' },
|
||||
statusUpdate: { status: { state: 'TASK_STATE_WORKING' }, taskId: 'task-1' },
|
||||
});
|
||||
|
||||
expect(
|
||||
A2AAgentCardSchema.parse({
|
||||
capabilities: {
|
||||
pushNotifications: true,
|
||||
stateTransitionHistory: true,
|
||||
streaming: true,
|
||||
},
|
||||
documentationUrl: 'https://agent.example.com/docs',
|
||||
name: 'Travel Agent',
|
||||
protocolVersion: '0.3.0',
|
||||
additionalInterfaces: [
|
||||
{
|
||||
tenant: 'tenant-b',
|
||||
transport: 'HTTP+JSON',
|
||||
url: 'https://agent.example.com/a2a/http',
|
||||
},
|
||||
],
|
||||
preferredTransport: 'JSONRPC',
|
||||
skills: [
|
||||
{
|
||||
description: 'Books flights',
|
||||
examples: ['Book SFO to JFK'],
|
||||
id: 'book_flight',
|
||||
input_modes: ['text'],
|
||||
outputModes: ['text'],
|
||||
tags: ['travel'],
|
||||
},
|
||||
],
|
||||
supportedInterfaces: [
|
||||
{
|
||||
protocolBinding: 'HTTP+JSON',
|
||||
protocolVersion: '1.0',
|
||||
tenant: 'tenant-a',
|
||||
url: 'https://agent.example.com/a2a/v1',
|
||||
},
|
||||
],
|
||||
}),
|
||||
).toMatchObject({
|
||||
capabilities: { streaming: true },
|
||||
name: 'Travel Agent',
|
||||
protocolVersion: '0.3.0',
|
||||
additionalInterfaces: [{ transport: 'HTTP+JSON' }],
|
||||
preferredTransport: 'JSONRPC',
|
||||
skills: [{ id: 'book_flight' }],
|
||||
supportedInterfaces: [{ protocolBinding: 'HTTP+JSON' }],
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user