@renseiai/agentfactory 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +125 -0
- package/dist/src/config/index.d.ts +3 -0
- package/dist/src/config/index.d.ts.map +1 -0
- package/dist/src/config/index.js +1 -0
- package/dist/src/config/repository-config.d.ts +44 -0
- package/dist/src/config/repository-config.d.ts.map +1 -0
- package/dist/src/config/repository-config.js +88 -0
- package/dist/src/config/repository-config.test.d.ts +2 -0
- package/dist/src/config/repository-config.test.d.ts.map +1 -0
- package/dist/src/config/repository-config.test.js +249 -0
- package/dist/src/deployment/deployment-checker.d.ts +110 -0
- package/dist/src/deployment/deployment-checker.d.ts.map +1 -0
- package/dist/src/deployment/deployment-checker.js +242 -0
- package/dist/src/deployment/index.d.ts +3 -0
- package/dist/src/deployment/index.d.ts.map +1 -0
- package/dist/src/deployment/index.js +2 -0
- package/dist/src/frontend/index.d.ts +2 -0
- package/dist/src/frontend/index.d.ts.map +1 -0
- package/dist/src/frontend/index.js +1 -0
- package/dist/src/frontend/types.d.ts +106 -0
- package/dist/src/frontend/types.d.ts.map +1 -0
- package/dist/src/frontend/types.js +11 -0
- package/dist/src/governor/decision-engine.d.ts +52 -0
- package/dist/src/governor/decision-engine.d.ts.map +1 -0
- package/dist/src/governor/decision-engine.js +220 -0
- package/dist/src/governor/decision-engine.test.d.ts +2 -0
- package/dist/src/governor/decision-engine.test.d.ts.map +1 -0
- package/dist/src/governor/decision-engine.test.js +629 -0
- package/dist/src/governor/event-bus.d.ts +43 -0
- package/dist/src/governor/event-bus.d.ts.map +1 -0
- package/dist/src/governor/event-bus.js +8 -0
- package/dist/src/governor/event-deduplicator.d.ts +43 -0
- package/dist/src/governor/event-deduplicator.d.ts.map +1 -0
- package/dist/src/governor/event-deduplicator.js +53 -0
- package/dist/src/governor/event-driven-governor.d.ts +131 -0
- package/dist/src/governor/event-driven-governor.d.ts.map +1 -0
- package/dist/src/governor/event-driven-governor.js +379 -0
- package/dist/src/governor/event-driven-governor.test.d.ts +2 -0
- package/dist/src/governor/event-driven-governor.test.d.ts.map +1 -0
- package/dist/src/governor/event-driven-governor.test.js +673 -0
- package/dist/src/governor/event-types.d.ts +78 -0
- package/dist/src/governor/event-types.d.ts.map +1 -0
- package/dist/src/governor/event-types.js +32 -0
- package/dist/src/governor/governor-types.d.ts +82 -0
- package/dist/src/governor/governor-types.d.ts.map +1 -0
- package/dist/src/governor/governor-types.js +21 -0
- package/dist/src/governor/governor.d.ts +100 -0
- package/dist/src/governor/governor.d.ts.map +1 -0
- package/dist/src/governor/governor.js +262 -0
- package/dist/src/governor/governor.test.d.ts +2 -0
- package/dist/src/governor/governor.test.d.ts.map +1 -0
- package/dist/src/governor/governor.test.js +514 -0
- package/dist/src/governor/human-touchpoints.d.ts +131 -0
- package/dist/src/governor/human-touchpoints.d.ts.map +1 -0
- package/dist/src/governor/human-touchpoints.js +251 -0
- package/dist/src/governor/human-touchpoints.test.d.ts +2 -0
- package/dist/src/governor/human-touchpoints.test.d.ts.map +1 -0
- package/dist/src/governor/human-touchpoints.test.js +366 -0
- package/dist/src/governor/in-memory-event-bus.d.ts +29 -0
- package/dist/src/governor/in-memory-event-bus.d.ts.map +1 -0
- package/dist/src/governor/in-memory-event-bus.js +79 -0
- package/dist/src/governor/index.d.ts +14 -0
- package/dist/src/governor/index.d.ts.map +1 -0
- package/dist/src/governor/index.js +13 -0
- package/dist/src/governor/override-parser.d.ts +60 -0
- package/dist/src/governor/override-parser.d.ts.map +1 -0
- package/dist/src/governor/override-parser.js +98 -0
- package/dist/src/governor/override-parser.test.d.ts +2 -0
- package/dist/src/governor/override-parser.test.d.ts.map +1 -0
- package/dist/src/governor/override-parser.test.js +312 -0
- package/dist/src/governor/platform-adapter.d.ts +69 -0
- package/dist/src/governor/platform-adapter.d.ts.map +1 -0
- package/dist/src/governor/platform-adapter.js +11 -0
- package/dist/src/governor/processing-state.d.ts +66 -0
- package/dist/src/governor/processing-state.d.ts.map +1 -0
- package/dist/src/governor/processing-state.js +43 -0
- package/dist/src/governor/processing-state.test.d.ts +2 -0
- package/dist/src/governor/processing-state.test.d.ts.map +1 -0
- package/dist/src/governor/processing-state.test.js +96 -0
- package/dist/src/governor/top-of-funnel.d.ts +118 -0
- package/dist/src/governor/top-of-funnel.d.ts.map +1 -0
- package/dist/src/governor/top-of-funnel.js +168 -0
- package/dist/src/governor/top-of-funnel.test.d.ts +2 -0
- package/dist/src/governor/top-of-funnel.test.d.ts.map +1 -0
- package/dist/src/governor/top-of-funnel.test.js +331 -0
- package/dist/src/index.d.ts +11 -0
- package/dist/src/index.d.ts.map +1 -0
- package/dist/src/index.js +10 -0
- package/dist/src/linear-cli.d.ts +38 -0
- package/dist/src/linear-cli.d.ts.map +1 -0
- package/dist/src/linear-cli.js +674 -0
- package/dist/src/logger.d.ts +117 -0
- package/dist/src/logger.d.ts.map +1 -0
- package/dist/src/logger.js +430 -0
- package/dist/src/manifest/generate.d.ts +20 -0
- package/dist/src/manifest/generate.d.ts.map +1 -0
- package/dist/src/manifest/generate.js +65 -0
- package/dist/src/manifest/index.d.ts +4 -0
- package/dist/src/manifest/index.d.ts.map +1 -0
- package/dist/src/manifest/index.js +2 -0
- package/dist/src/manifest/route-manifest.d.ts +34 -0
- package/dist/src/manifest/route-manifest.d.ts.map +1 -0
- package/dist/src/manifest/route-manifest.js +148 -0
- package/dist/src/orchestrator/activity-emitter.d.ts +119 -0
- package/dist/src/orchestrator/activity-emitter.d.ts.map +1 -0
- package/dist/src/orchestrator/activity-emitter.js +306 -0
- package/dist/src/orchestrator/api-activity-emitter.d.ts +167 -0
- package/dist/src/orchestrator/api-activity-emitter.d.ts.map +1 -0
- package/dist/src/orchestrator/api-activity-emitter.js +417 -0
- package/dist/src/orchestrator/heartbeat-writer.d.ts +57 -0
- package/dist/src/orchestrator/heartbeat-writer.d.ts.map +1 -0
- package/dist/src/orchestrator/heartbeat-writer.js +137 -0
- package/dist/src/orchestrator/index.d.ts +20 -0
- package/dist/src/orchestrator/index.d.ts.map +1 -0
- package/dist/src/orchestrator/index.js +22 -0
- package/dist/src/orchestrator/log-analyzer.d.ts +160 -0
- package/dist/src/orchestrator/log-analyzer.d.ts.map +1 -0
- package/dist/src/orchestrator/log-analyzer.js +572 -0
- package/dist/src/orchestrator/log-config.d.ts +39 -0
- package/dist/src/orchestrator/log-config.d.ts.map +1 -0
- package/dist/src/orchestrator/log-config.js +45 -0
- package/dist/src/orchestrator/orchestrator.d.ts +316 -0
- package/dist/src/orchestrator/orchestrator.d.ts.map +1 -0
- package/dist/src/orchestrator/orchestrator.js +3290 -0
- package/dist/src/orchestrator/parse-work-result.d.ts +16 -0
- package/dist/src/orchestrator/parse-work-result.d.ts.map +1 -0
- package/dist/src/orchestrator/parse-work-result.js +135 -0
- package/dist/src/orchestrator/parse-work-result.test.d.ts +2 -0
- package/dist/src/orchestrator/parse-work-result.test.d.ts.map +1 -0
- package/dist/src/orchestrator/parse-work-result.test.js +234 -0
- package/dist/src/orchestrator/progress-logger.d.ts +72 -0
- package/dist/src/orchestrator/progress-logger.d.ts.map +1 -0
- package/dist/src/orchestrator/progress-logger.js +135 -0
- package/dist/src/orchestrator/session-logger.d.ts +159 -0
- package/dist/src/orchestrator/session-logger.d.ts.map +1 -0
- package/dist/src/orchestrator/session-logger.js +275 -0
- package/dist/src/orchestrator/state-recovery.d.ts +96 -0
- package/dist/src/orchestrator/state-recovery.d.ts.map +1 -0
- package/dist/src/orchestrator/state-recovery.js +302 -0
- package/dist/src/orchestrator/state-types.d.ts +165 -0
- package/dist/src/orchestrator/state-types.d.ts.map +1 -0
- package/dist/src/orchestrator/state-types.js +7 -0
- package/dist/src/orchestrator/stream-parser.d.ts +151 -0
- package/dist/src/orchestrator/stream-parser.d.ts.map +1 -0
- package/dist/src/orchestrator/stream-parser.js +137 -0
- package/dist/src/orchestrator/types.d.ts +232 -0
- package/dist/src/orchestrator/types.d.ts.map +1 -0
- package/dist/src/orchestrator/types.js +4 -0
- package/dist/src/orchestrator/validate-git-remote.test.d.ts +2 -0
- package/dist/src/orchestrator/validate-git-remote.test.d.ts.map +1 -0
- package/dist/src/orchestrator/validate-git-remote.test.js +61 -0
- package/dist/src/providers/a2a-auth.d.ts +81 -0
- package/dist/src/providers/a2a-auth.d.ts.map +1 -0
- package/dist/src/providers/a2a-auth.js +188 -0
- package/dist/src/providers/a2a-auth.test.d.ts +2 -0
- package/dist/src/providers/a2a-auth.test.d.ts.map +1 -0
- package/dist/src/providers/a2a-auth.test.js +232 -0
- package/dist/src/providers/a2a-provider.d.ts +254 -0
- package/dist/src/providers/a2a-provider.d.ts.map +1 -0
- package/dist/src/providers/a2a-provider.integration.test.d.ts +9 -0
- package/dist/src/providers/a2a-provider.integration.test.d.ts.map +1 -0
- package/dist/src/providers/a2a-provider.integration.test.js +665 -0
- package/dist/src/providers/a2a-provider.js +811 -0
- package/dist/src/providers/a2a-provider.test.d.ts +2 -0
- package/dist/src/providers/a2a-provider.test.d.ts.map +1 -0
- package/dist/src/providers/a2a-provider.test.js +681 -0
- package/dist/src/providers/amp-provider.d.ts +20 -0
- package/dist/src/providers/amp-provider.d.ts.map +1 -0
- package/dist/src/providers/amp-provider.js +24 -0
- package/dist/src/providers/claude-provider.d.ts +18 -0
- package/dist/src/providers/claude-provider.d.ts.map +1 -0
- package/dist/src/providers/claude-provider.js +437 -0
- package/dist/src/providers/codex-provider.d.ts +133 -0
- package/dist/src/providers/codex-provider.d.ts.map +1 -0
- package/dist/src/providers/codex-provider.js +381 -0
- package/dist/src/providers/codex-provider.test.d.ts +2 -0
- package/dist/src/providers/codex-provider.test.d.ts.map +1 -0
- package/dist/src/providers/codex-provider.test.js +387 -0
- package/dist/src/providers/index.d.ts +44 -0
- package/dist/src/providers/index.d.ts.map +1 -0
- package/dist/src/providers/index.js +85 -0
- package/dist/src/providers/spring-ai-provider.d.ts +90 -0
- package/dist/src/providers/spring-ai-provider.d.ts.map +1 -0
- package/dist/src/providers/spring-ai-provider.integration.test.d.ts +13 -0
- package/dist/src/providers/spring-ai-provider.integration.test.d.ts.map +1 -0
- package/dist/src/providers/spring-ai-provider.integration.test.js +351 -0
- package/dist/src/providers/spring-ai-provider.js +317 -0
- package/dist/src/providers/spring-ai-provider.test.d.ts +2 -0
- package/dist/src/providers/spring-ai-provider.test.d.ts.map +1 -0
- package/dist/src/providers/spring-ai-provider.test.js +200 -0
- package/dist/src/providers/types.d.ts +165 -0
- package/dist/src/providers/types.d.ts.map +1 -0
- package/dist/src/providers/types.js +13 -0
- package/dist/src/templates/adapters.d.ts +51 -0
- package/dist/src/templates/adapters.d.ts.map +1 -0
- package/dist/src/templates/adapters.js +104 -0
- package/dist/src/templates/adapters.test.d.ts +2 -0
- package/dist/src/templates/adapters.test.d.ts.map +1 -0
- package/dist/src/templates/adapters.test.js +165 -0
- package/dist/src/templates/agent-definition.d.ts +85 -0
- package/dist/src/templates/agent-definition.d.ts.map +1 -0
- package/dist/src/templates/agent-definition.js +97 -0
- package/dist/src/templates/agent-definition.test.d.ts +2 -0
- package/dist/src/templates/agent-definition.test.d.ts.map +1 -0
- package/dist/src/templates/agent-definition.test.js +209 -0
- package/dist/src/templates/index.d.ts +14 -0
- package/dist/src/templates/index.d.ts.map +1 -0
- package/dist/src/templates/index.js +11 -0
- package/dist/src/templates/loader.d.ts +41 -0
- package/dist/src/templates/loader.d.ts.map +1 -0
- package/dist/src/templates/loader.js +114 -0
- package/dist/src/templates/registry.d.ts +80 -0
- package/dist/src/templates/registry.d.ts.map +1 -0
- package/dist/src/templates/registry.js +177 -0
- package/dist/src/templates/registry.test.d.ts +2 -0
- package/dist/src/templates/registry.test.d.ts.map +1 -0
- package/dist/src/templates/registry.test.js +198 -0
- package/dist/src/templates/renderer.d.ts +29 -0
- package/dist/src/templates/renderer.d.ts.map +1 -0
- package/dist/src/templates/renderer.js +35 -0
- package/dist/src/templates/strategy-templates.test.d.ts +2 -0
- package/dist/src/templates/strategy-templates.test.d.ts.map +1 -0
- package/dist/src/templates/strategy-templates.test.js +619 -0
- package/dist/src/templates/types.d.ts +233 -0
- package/dist/src/templates/types.d.ts.map +1 -0
- package/dist/src/templates/types.js +127 -0
- package/dist/src/templates/types.test.d.ts +2 -0
- package/dist/src/templates/types.test.d.ts.map +1 -0
- package/dist/src/templates/types.test.js +232 -0
- package/dist/src/tools/index.d.ts +6 -0
- package/dist/src/tools/index.d.ts.map +1 -0
- package/dist/src/tools/index.js +3 -0
- package/dist/src/tools/linear-runner.d.ts +34 -0
- package/dist/src/tools/linear-runner.d.ts.map +1 -0
- package/dist/src/tools/linear-runner.js +700 -0
- package/dist/src/tools/plugins/linear.d.ts +9 -0
- package/dist/src/tools/plugins/linear.d.ts.map +1 -0
- package/dist/src/tools/plugins/linear.js +138 -0
- package/dist/src/tools/registry.d.ts +9 -0
- package/dist/src/tools/registry.d.ts.map +1 -0
- package/dist/src/tools/registry.js +18 -0
- package/dist/src/tools/types.d.ts +18 -0
- package/dist/src/tools/types.d.ts.map +1 -0
- package/dist/src/tools/types.js +1 -0
- package/package.json +78 -0
|
@@ -0,0 +1,665 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Integration tests for the A2A Client Provider
|
|
3
|
+
*
|
|
4
|
+
* Exercises the full provider lifecycle by mocking global.fetch to simulate
|
|
5
|
+
* an A2A-compliant server. Tests SSE streaming, single JSON responses,
|
|
6
|
+
* error handling, resume, stop/cancel, and message injection.
|
|
7
|
+
*/
|
|
8
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
9
|
+
import { A2aProvider } from './a2a-provider.js';
|
|
10
|
+
// ---------------------------------------------------------------------------
|
|
11
|
+
// Helpers
|
|
12
|
+
// ---------------------------------------------------------------------------
|
|
13
|
+
function makeConfig(overrides) {
|
|
14
|
+
return {
|
|
15
|
+
prompt: 'Test prompt',
|
|
16
|
+
cwd: '/tmp/test',
|
|
17
|
+
env: { A2A_AGENT_URL: 'http://test-agent:8080' },
|
|
18
|
+
abortController: new AbortController(),
|
|
19
|
+
autonomous: true,
|
|
20
|
+
sandboxEnabled: false,
|
|
21
|
+
...overrides,
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
async function collectEvents(stream) {
|
|
25
|
+
const events = [];
|
|
26
|
+
for await (const event of stream) {
|
|
27
|
+
events.push(event);
|
|
28
|
+
}
|
|
29
|
+
return events;
|
|
30
|
+
}
|
|
31
|
+
/** Build an SSE-formatted event string */
|
|
32
|
+
function sseEvent(type, data) {
|
|
33
|
+
return `event: ${type}\ndata: ${JSON.stringify(data)}\n\n`;
|
|
34
|
+
}
|
|
35
|
+
/** Create a Response that streams SSE events */
|
|
36
|
+
function createSSEResponse(events) {
|
|
37
|
+
const encoder = new TextEncoder();
|
|
38
|
+
const stream = new ReadableStream({
|
|
39
|
+
start(controller) {
|
|
40
|
+
for (const event of events) {
|
|
41
|
+
controller.enqueue(encoder.encode(event));
|
|
42
|
+
}
|
|
43
|
+
controller.close();
|
|
44
|
+
},
|
|
45
|
+
});
|
|
46
|
+
return new Response(stream, {
|
|
47
|
+
status: 200,
|
|
48
|
+
headers: { 'content-type': 'text/event-stream' },
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
/** Create a JSON-RPC success response */
|
|
52
|
+
function createJsonRpcResponse(id, result) {
|
|
53
|
+
return new Response(JSON.stringify({ jsonrpc: '2.0', id, result }), {
|
|
54
|
+
status: 200,
|
|
55
|
+
headers: { 'content-type': 'application/json' },
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
/** Create a JSON-RPC error response */
|
|
59
|
+
function createJsonRpcErrorResponse(id, code, message) {
|
|
60
|
+
return new Response(JSON.stringify({ jsonrpc: '2.0', id, error: { code, message } }), {
|
|
61
|
+
status: 200,
|
|
62
|
+
headers: { 'content-type': 'application/json' },
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
// ---------------------------------------------------------------------------
|
|
66
|
+
// Tests
|
|
67
|
+
// ---------------------------------------------------------------------------
|
|
68
|
+
describe('A2aProvider integration', () => {
|
|
69
|
+
let provider;
|
|
70
|
+
let mockFetch;
|
|
71
|
+
beforeEach(() => {
|
|
72
|
+
provider = new A2aProvider();
|
|
73
|
+
mockFetch = vi.fn();
|
|
74
|
+
vi.stubGlobal('fetch', mockFetch);
|
|
75
|
+
});
|
|
76
|
+
afterEach(() => {
|
|
77
|
+
vi.restoreAllMocks();
|
|
78
|
+
});
|
|
79
|
+
// -------------------------------------------------------------------------
|
|
80
|
+
// 1. Happy path — SSE streaming
|
|
81
|
+
// -------------------------------------------------------------------------
|
|
82
|
+
describe('happy path with SSE streaming', () => {
|
|
83
|
+
it('produces init, assistant_text, and result events from SSE stream', async () => {
|
|
84
|
+
mockFetch.mockResolvedValueOnce(createSSEResponse([
|
|
85
|
+
sseEvent('TaskStatusUpdate', {
|
|
86
|
+
id: 'task-1',
|
|
87
|
+
sessionId: 'sess-1',
|
|
88
|
+
status: { state: 'submitted' },
|
|
89
|
+
}),
|
|
90
|
+
sseEvent('TaskStatusUpdate', {
|
|
91
|
+
id: 'task-1',
|
|
92
|
+
sessionId: 'sess-1',
|
|
93
|
+
status: {
|
|
94
|
+
state: 'working',
|
|
95
|
+
message: { role: 'agent', parts: [{ type: 'text', text: 'Thinking...' }] },
|
|
96
|
+
},
|
|
97
|
+
}),
|
|
98
|
+
sseEvent('TaskStatusUpdate', {
|
|
99
|
+
id: 'task-1',
|
|
100
|
+
sessionId: 'sess-1',
|
|
101
|
+
status: {
|
|
102
|
+
state: 'completed',
|
|
103
|
+
message: { role: 'agent', parts: [{ type: 'text', text: 'All done!' }] },
|
|
104
|
+
},
|
|
105
|
+
final: true,
|
|
106
|
+
}),
|
|
107
|
+
]));
|
|
108
|
+
const handle = provider.spawn(makeConfig());
|
|
109
|
+
const events = await collectEvents(handle.stream);
|
|
110
|
+
// init from submitted
|
|
111
|
+
expect(events[0]).toMatchObject({ type: 'init', sessionId: 'sess-1' });
|
|
112
|
+
// assistant_text from working
|
|
113
|
+
const textEvents = events.filter(e => e.type === 'assistant_text');
|
|
114
|
+
expect(textEvents.length).toBeGreaterThanOrEqual(1);
|
|
115
|
+
expect(textEvents[0]).toMatchObject({ type: 'assistant_text', text: 'Thinking...' });
|
|
116
|
+
// result from completed
|
|
117
|
+
const resultEvent = events.find(e => e.type === 'result');
|
|
118
|
+
expect(resultEvent).toMatchObject({ type: 'result', success: true });
|
|
119
|
+
// sessionId is set on the handle
|
|
120
|
+
expect(handle.sessionId).toBe('sess-1');
|
|
121
|
+
});
|
|
122
|
+
it('sends the correct JSON-RPC request', async () => {
|
|
123
|
+
mockFetch.mockResolvedValueOnce(createSSEResponse([
|
|
124
|
+
sseEvent('TaskStatusUpdate', {
|
|
125
|
+
id: 'task-1',
|
|
126
|
+
sessionId: 'sess-1',
|
|
127
|
+
status: { state: 'submitted' },
|
|
128
|
+
}),
|
|
129
|
+
sseEvent('TaskStatusUpdate', {
|
|
130
|
+
id: 'task-1',
|
|
131
|
+
status: { state: 'completed' },
|
|
132
|
+
final: true,
|
|
133
|
+
}),
|
|
134
|
+
]));
|
|
135
|
+
const handle = provider.spawn(makeConfig());
|
|
136
|
+
await collectEvents(handle.stream);
|
|
137
|
+
expect(mockFetch).toHaveBeenCalledTimes(1);
|
|
138
|
+
const [url, opts] = mockFetch.mock.calls[0];
|
|
139
|
+
expect(url).toBe('http://test-agent:8080/a2a');
|
|
140
|
+
const body = JSON.parse(opts.body);
|
|
141
|
+
expect(body.jsonrpc).toBe('2.0');
|
|
142
|
+
expect(body.method).toBe('message/stream');
|
|
143
|
+
expect(body.params.message).toMatchObject({
|
|
144
|
+
role: 'user',
|
|
145
|
+
parts: [{ type: 'text', text: 'Test prompt' }],
|
|
146
|
+
});
|
|
147
|
+
});
|
|
148
|
+
});
|
|
149
|
+
// -------------------------------------------------------------------------
|
|
150
|
+
// 2. Happy path — single JSON response (non-streaming fallback)
|
|
151
|
+
// -------------------------------------------------------------------------
|
|
152
|
+
describe('happy path with single JSON response', () => {
|
|
153
|
+
it('falls back to message/send and emits init + result', async () => {
|
|
154
|
+
// First call (message/stream) fails
|
|
155
|
+
mockFetch.mockRejectedValueOnce(new Error('Streaming not supported'));
|
|
156
|
+
// Second call (message/send) returns a completed task
|
|
157
|
+
mockFetch.mockResolvedValueOnce(createJsonRpcResponse('rpc-1', {
|
|
158
|
+
id: 'task-2',
|
|
159
|
+
sessionId: 'sess-2',
|
|
160
|
+
status: {
|
|
161
|
+
state: 'completed',
|
|
162
|
+
message: { role: 'agent', parts: [{ type: 'text', text: 'Result text' }] },
|
|
163
|
+
},
|
|
164
|
+
}));
|
|
165
|
+
const handle = provider.spawn(makeConfig());
|
|
166
|
+
const events = await collectEvents(handle.stream);
|
|
167
|
+
const initEvent = events.find(e => e.type === 'init');
|
|
168
|
+
expect(initEvent).toMatchObject({ type: 'init', sessionId: 'sess-2' });
|
|
169
|
+
const resultEvent = events.find(e => e.type === 'result');
|
|
170
|
+
expect(resultEvent).toMatchObject({ type: 'result', success: true });
|
|
171
|
+
// Verify two fetch calls: first stream, then send
|
|
172
|
+
expect(mockFetch).toHaveBeenCalledTimes(2);
|
|
173
|
+
const firstBody = JSON.parse(mockFetch.mock.calls[0][1].body);
|
|
174
|
+
expect(firstBody.method).toBe('message/stream');
|
|
175
|
+
const secondBody = JSON.parse(mockFetch.mock.calls[1][1].body);
|
|
176
|
+
expect(secondBody.method).toBe('message/send');
|
|
177
|
+
});
|
|
178
|
+
});
|
|
179
|
+
// -------------------------------------------------------------------------
|
|
180
|
+
// 3. Task with artifacts
|
|
181
|
+
// -------------------------------------------------------------------------
|
|
182
|
+
describe('task with artifacts', () => {
|
|
183
|
+
it('emits assistant_text for artifacts in SSE stream', async () => {
|
|
184
|
+
mockFetch.mockResolvedValueOnce(createSSEResponse([
|
|
185
|
+
sseEvent('TaskStatusUpdate', {
|
|
186
|
+
id: 'task-3',
|
|
187
|
+
sessionId: 'sess-3',
|
|
188
|
+
status: { state: 'submitted' },
|
|
189
|
+
}),
|
|
190
|
+
sseEvent('TaskStatusUpdate', {
|
|
191
|
+
id: 'task-3',
|
|
192
|
+
status: { state: 'working' },
|
|
193
|
+
}),
|
|
194
|
+
sseEvent('TaskArtifactUpdate', {
|
|
195
|
+
id: 'task-3',
|
|
196
|
+
sessionId: 'sess-3',
|
|
197
|
+
artifact: {
|
|
198
|
+
name: 'report.md',
|
|
199
|
+
parts: [{ type: 'text', text: '# Analysis Report\n\nFindings here.' }],
|
|
200
|
+
},
|
|
201
|
+
}),
|
|
202
|
+
sseEvent('TaskStatusUpdate', {
|
|
203
|
+
id: 'task-3',
|
|
204
|
+
status: { state: 'completed' },
|
|
205
|
+
final: true,
|
|
206
|
+
}),
|
|
207
|
+
]));
|
|
208
|
+
const handle = provider.spawn(makeConfig());
|
|
209
|
+
const events = await collectEvents(handle.stream);
|
|
210
|
+
const textEvents = events.filter(e => e.type === 'assistant_text');
|
|
211
|
+
const artifactText = textEvents.find(e => e.type === 'assistant_text' && e.text.includes('Analysis Report'));
|
|
212
|
+
expect(artifactText).toBeDefined();
|
|
213
|
+
expect(artifactText.text).toContain('# Analysis Report');
|
|
214
|
+
});
|
|
215
|
+
it('emits artifact text from single JSON response', async () => {
|
|
216
|
+
mockFetch.mockRejectedValueOnce(new Error('no stream'));
|
|
217
|
+
mockFetch.mockResolvedValueOnce(createJsonRpcResponse('rpc-1', {
|
|
218
|
+
id: 'task-3b',
|
|
219
|
+
sessionId: 'sess-3b',
|
|
220
|
+
status: { state: 'completed' },
|
|
221
|
+
artifacts: [
|
|
222
|
+
{
|
|
223
|
+
name: 'output.txt',
|
|
224
|
+
parts: [{ type: 'text', text: 'Generated content' }],
|
|
225
|
+
},
|
|
226
|
+
],
|
|
227
|
+
}));
|
|
228
|
+
const handle = provider.spawn(makeConfig());
|
|
229
|
+
const events = await collectEvents(handle.stream);
|
|
230
|
+
const textEvents = events.filter(e => e.type === 'assistant_text');
|
|
231
|
+
expect(textEvents.some(e => e.type === 'assistant_text' && e.text === 'Generated content')).toBe(true);
|
|
232
|
+
});
|
|
233
|
+
});
|
|
234
|
+
// -------------------------------------------------------------------------
|
|
235
|
+
// 4. Input-required status
|
|
236
|
+
// -------------------------------------------------------------------------
|
|
237
|
+
describe('input-required status', () => {
|
|
238
|
+
it('emits system event with subtype input_required', async () => {
|
|
239
|
+
mockFetch.mockResolvedValueOnce(createSSEResponse([
|
|
240
|
+
sseEvent('TaskStatusUpdate', {
|
|
241
|
+
id: 'task-4',
|
|
242
|
+
sessionId: 'sess-4',
|
|
243
|
+
status: { state: 'submitted' },
|
|
244
|
+
}),
|
|
245
|
+
sseEvent('TaskStatusUpdate', {
|
|
246
|
+
id: 'task-4',
|
|
247
|
+
status: { state: 'working' },
|
|
248
|
+
}),
|
|
249
|
+
sseEvent('TaskStatusUpdate', {
|
|
250
|
+
id: 'task-4',
|
|
251
|
+
status: {
|
|
252
|
+
state: 'input-required',
|
|
253
|
+
message: {
|
|
254
|
+
role: 'agent',
|
|
255
|
+
parts: [{ type: 'text', text: 'Please provide the API key' }],
|
|
256
|
+
},
|
|
257
|
+
},
|
|
258
|
+
}),
|
|
259
|
+
]));
|
|
260
|
+
const handle = provider.spawn(makeConfig());
|
|
261
|
+
const events = await collectEvents(handle.stream);
|
|
262
|
+
const inputEvent = events.find(e => e.type === 'system' && 'subtype' in e && e.subtype === 'input_required');
|
|
263
|
+
expect(inputEvent).toBeDefined();
|
|
264
|
+
expect(inputEvent).toMatchObject({
|
|
265
|
+
type: 'system',
|
|
266
|
+
subtype: 'input_required',
|
|
267
|
+
message: 'Please provide the API key',
|
|
268
|
+
});
|
|
269
|
+
});
|
|
270
|
+
});
|
|
271
|
+
// -------------------------------------------------------------------------
|
|
272
|
+
// 5. Task failure
|
|
273
|
+
// -------------------------------------------------------------------------
|
|
274
|
+
describe('task failure', () => {
|
|
275
|
+
it('emits result with success: false on failed status', async () => {
|
|
276
|
+
mockFetch.mockResolvedValueOnce(createSSEResponse([
|
|
277
|
+
sseEvent('TaskStatusUpdate', {
|
|
278
|
+
id: 'task-5',
|
|
279
|
+
sessionId: 'sess-5',
|
|
280
|
+
status: { state: 'submitted' },
|
|
281
|
+
}),
|
|
282
|
+
sseEvent('TaskStatusUpdate', {
|
|
283
|
+
id: 'task-5',
|
|
284
|
+
status: {
|
|
285
|
+
state: 'failed',
|
|
286
|
+
message: {
|
|
287
|
+
role: 'agent',
|
|
288
|
+
parts: [{ type: 'text', text: 'Out of memory' }],
|
|
289
|
+
},
|
|
290
|
+
},
|
|
291
|
+
final: true,
|
|
292
|
+
}),
|
|
293
|
+
]));
|
|
294
|
+
const handle = provider.spawn(makeConfig());
|
|
295
|
+
const events = await collectEvents(handle.stream);
|
|
296
|
+
const resultEvent = events.find(e => e.type === 'result');
|
|
297
|
+
expect(resultEvent).toMatchObject({
|
|
298
|
+
type: 'result',
|
|
299
|
+
success: false,
|
|
300
|
+
errors: ['Out of memory'],
|
|
301
|
+
errorSubtype: 'task_failed',
|
|
302
|
+
});
|
|
303
|
+
});
|
|
304
|
+
});
|
|
305
|
+
// -------------------------------------------------------------------------
|
|
306
|
+
// 6. Task cancellation
|
|
307
|
+
// -------------------------------------------------------------------------
|
|
308
|
+
describe('task cancellation', () => {
|
|
309
|
+
it('emits result with errorSubtype canceled', async () => {
|
|
310
|
+
mockFetch.mockResolvedValueOnce(createSSEResponse([
|
|
311
|
+
sseEvent('TaskStatusUpdate', {
|
|
312
|
+
id: 'task-6',
|
|
313
|
+
sessionId: 'sess-6',
|
|
314
|
+
status: { state: 'submitted' },
|
|
315
|
+
}),
|
|
316
|
+
sseEvent('TaskStatusUpdate', {
|
|
317
|
+
id: 'task-6',
|
|
318
|
+
status: {
|
|
319
|
+
state: 'canceled',
|
|
320
|
+
message: {
|
|
321
|
+
role: 'agent',
|
|
322
|
+
parts: [{ type: 'text', text: 'Task was canceled by user' }],
|
|
323
|
+
},
|
|
324
|
+
},
|
|
325
|
+
final: true,
|
|
326
|
+
}),
|
|
327
|
+
]));
|
|
328
|
+
const handle = provider.spawn(makeConfig());
|
|
329
|
+
const events = await collectEvents(handle.stream);
|
|
330
|
+
const resultEvent = events.find(e => e.type === 'result');
|
|
331
|
+
expect(resultEvent).toMatchObject({
|
|
332
|
+
type: 'result',
|
|
333
|
+
success: false,
|
|
334
|
+
errorSubtype: 'canceled',
|
|
335
|
+
errors: ['Task was canceled by user'],
|
|
336
|
+
});
|
|
337
|
+
});
|
|
338
|
+
});
|
|
339
|
+
// -------------------------------------------------------------------------
|
|
340
|
+
// 7. Missing A2A_AGENT_URL
|
|
341
|
+
// -------------------------------------------------------------------------
|
|
342
|
+
describe('missing A2A_AGENT_URL', () => {
|
|
343
|
+
it('emits error and result with configuration_error when no URL configured', async () => {
|
|
344
|
+
const config = makeConfig({ env: {} });
|
|
345
|
+
const handle = provider.spawn(config);
|
|
346
|
+
const events = await collectEvents(handle.stream);
|
|
347
|
+
expect(events).toHaveLength(2);
|
|
348
|
+
expect(events[0]).toMatchObject({
|
|
349
|
+
type: 'error',
|
|
350
|
+
message: expect.stringContaining('A2A_AGENT_URL'),
|
|
351
|
+
});
|
|
352
|
+
expect(events[1]).toMatchObject({
|
|
353
|
+
type: 'result',
|
|
354
|
+
success: false,
|
|
355
|
+
errorSubtype: 'configuration_error',
|
|
356
|
+
});
|
|
357
|
+
// fetch should never be called
|
|
358
|
+
expect(mockFetch).not.toHaveBeenCalled();
|
|
359
|
+
});
|
|
360
|
+
});
|
|
361
|
+
// -------------------------------------------------------------------------
|
|
362
|
+
// 8. Network error
|
|
363
|
+
// -------------------------------------------------------------------------
|
|
364
|
+
describe('network error', () => {
|
|
365
|
+
it('emits error and connection_error when both stream and send fail', async () => {
|
|
366
|
+
// Both message/stream and message/send throw
|
|
367
|
+
mockFetch.mockRejectedValue(new Error('ECONNREFUSED'));
|
|
368
|
+
const handle = provider.spawn(makeConfig());
|
|
369
|
+
const events = await collectEvents(handle.stream);
|
|
370
|
+
const errorEvent = events.find(e => e.type === 'error');
|
|
371
|
+
expect(errorEvent).toMatchObject({
|
|
372
|
+
type: 'error',
|
|
373
|
+
message: expect.stringContaining('ECONNREFUSED'),
|
|
374
|
+
});
|
|
375
|
+
const resultEvent = events.find(e => e.type === 'result');
|
|
376
|
+
expect(resultEvent).toMatchObject({
|
|
377
|
+
type: 'result',
|
|
378
|
+
success: false,
|
|
379
|
+
errorSubtype: 'connection_error',
|
|
380
|
+
});
|
|
381
|
+
});
|
|
382
|
+
});
|
|
383
|
+
// -------------------------------------------------------------------------
|
|
384
|
+
// 9. JSON-RPC error response
|
|
385
|
+
// -------------------------------------------------------------------------
|
|
386
|
+
describe('JSON-RPC error response', () => {
|
|
387
|
+
it('emits error event with jsonrpc_error subtype', async () => {
|
|
388
|
+
// message/stream fails, message/send returns JSON-RPC error
|
|
389
|
+
mockFetch.mockRejectedValueOnce(new Error('no stream'));
|
|
390
|
+
mockFetch.mockResolvedValueOnce(createJsonRpcErrorResponse('rpc-1', -32603, 'Internal error: agent crashed'));
|
|
391
|
+
const handle = provider.spawn(makeConfig());
|
|
392
|
+
const events = await collectEvents(handle.stream);
|
|
393
|
+
const errorEvent = events.find(e => e.type === 'error');
|
|
394
|
+
expect(errorEvent).toMatchObject({
|
|
395
|
+
type: 'error',
|
|
396
|
+
message: 'Internal error: agent crashed',
|
|
397
|
+
});
|
|
398
|
+
const resultEvent = events.find(e => e.type === 'result');
|
|
399
|
+
expect(resultEvent).toMatchObject({
|
|
400
|
+
type: 'result',
|
|
401
|
+
success: false,
|
|
402
|
+
errorSubtype: 'jsonrpc_error',
|
|
403
|
+
errors: ['Internal error: agent crashed'],
|
|
404
|
+
});
|
|
405
|
+
});
|
|
406
|
+
});
|
|
407
|
+
// -------------------------------------------------------------------------
|
|
408
|
+
// 10. Resume session
|
|
409
|
+
// -------------------------------------------------------------------------
|
|
410
|
+
describe('resume session', () => {
|
|
411
|
+
it('includes sessionId in JSON-RPC params', async () => {
|
|
412
|
+
mockFetch.mockResolvedValueOnce(createSSEResponse([
|
|
413
|
+
sseEvent('TaskStatusUpdate', {
|
|
414
|
+
id: 'task-prev',
|
|
415
|
+
sessionId: 'task-previous',
|
|
416
|
+
status: { state: 'submitted' },
|
|
417
|
+
}),
|
|
418
|
+
sseEvent('TaskStatusUpdate', {
|
|
419
|
+
id: 'task-prev',
|
|
420
|
+
status: { state: 'completed' },
|
|
421
|
+
final: true,
|
|
422
|
+
}),
|
|
423
|
+
]));
|
|
424
|
+
const handle = provider.resume('task-previous', makeConfig());
|
|
425
|
+
await collectEvents(handle.stream);
|
|
426
|
+
expect(mockFetch).toHaveBeenCalledTimes(1);
|
|
427
|
+
const body = JSON.parse(mockFetch.mock.calls[0][1].body);
|
|
428
|
+
expect(body.params.sessionId).toBe('task-previous');
|
|
429
|
+
// Handle should have sessionId set before streaming
|
|
430
|
+
expect(handle.sessionId).toBe('task-previous');
|
|
431
|
+
});
|
|
432
|
+
});
|
|
433
|
+
// -------------------------------------------------------------------------
|
|
434
|
+
// 11. Stop / cancel
|
|
435
|
+
// -------------------------------------------------------------------------
|
|
436
|
+
describe('stop / cancel', () => {
|
|
437
|
+
it('sends tasks/cancel when stop() is called after init', async () => {
|
|
438
|
+
// Use a stream that stays open so we can call stop() mid-stream
|
|
439
|
+
let streamController;
|
|
440
|
+
const encoder = new TextEncoder();
|
|
441
|
+
const readableStream = new ReadableStream({
|
|
442
|
+
start(controller) {
|
|
443
|
+
streamController = controller;
|
|
444
|
+
// Immediately emit submitted + working
|
|
445
|
+
controller.enqueue(encoder.encode(sseEvent('TaskStatusUpdate', {
|
|
446
|
+
id: 'task-stop',
|
|
447
|
+
sessionId: 'sess-stop',
|
|
448
|
+
status: { state: 'submitted' },
|
|
449
|
+
})));
|
|
450
|
+
controller.enqueue(encoder.encode(sseEvent('TaskStatusUpdate', {
|
|
451
|
+
id: 'task-stop',
|
|
452
|
+
status: { state: 'working' },
|
|
453
|
+
})));
|
|
454
|
+
},
|
|
455
|
+
});
|
|
456
|
+
const sseResponse = new Response(readableStream, {
|
|
457
|
+
status: 200,
|
|
458
|
+
headers: { 'content-type': 'text/event-stream' },
|
|
459
|
+
});
|
|
460
|
+
// First call returns the SSE stream
|
|
461
|
+
mockFetch.mockResolvedValueOnce(sseResponse);
|
|
462
|
+
// Second call is the cancel request
|
|
463
|
+
mockFetch.mockResolvedValueOnce(createJsonRpcResponse('rpc-cancel', {
|
|
464
|
+
id: 'task-stop',
|
|
465
|
+
status: { state: 'canceled' },
|
|
466
|
+
}));
|
|
467
|
+
const handle = provider.spawn(makeConfig());
|
|
468
|
+
// Collect events asynchronously — will resolve once the stream closes
|
|
469
|
+
const eventsPromise = collectEvents(handle.stream);
|
|
470
|
+
// Give the stream a moment to emit the initial events
|
|
471
|
+
await new Promise(resolve => setTimeout(resolve, 50));
|
|
472
|
+
// Call stop — this should send a cancel request and abort
|
|
473
|
+
await handle.stop();
|
|
474
|
+
// Close the stream controller so the reader finishes
|
|
475
|
+
try {
|
|
476
|
+
streamController.close();
|
|
477
|
+
}
|
|
478
|
+
catch {
|
|
479
|
+
// Already closed by abort — that's fine
|
|
480
|
+
}
|
|
481
|
+
const events = await eventsPromise;
|
|
482
|
+
// Should have an init event at minimum
|
|
483
|
+
const initEvent = events.find(e => e.type === 'init');
|
|
484
|
+
expect(initEvent).toBeDefined();
|
|
485
|
+
// Verify cancel request was made (second fetch call)
|
|
486
|
+
const cancelCall = mockFetch.mock.calls.find((call) => {
|
|
487
|
+
try {
|
|
488
|
+
const body = JSON.parse(call[1].body);
|
|
489
|
+
return body.method === 'tasks/cancel';
|
|
490
|
+
}
|
|
491
|
+
catch {
|
|
492
|
+
return false;
|
|
493
|
+
}
|
|
494
|
+
});
|
|
495
|
+
expect(cancelCall).toBeDefined();
|
|
496
|
+
const cancelBody = JSON.parse(cancelCall[1].body);
|
|
497
|
+
expect(cancelBody.params.id).toBe('task-stop');
|
|
498
|
+
});
|
|
499
|
+
});
|
|
500
|
+
// -------------------------------------------------------------------------
|
|
501
|
+
// 12. InjectMessage
|
|
502
|
+
// -------------------------------------------------------------------------
|
|
503
|
+
describe('injectMessage', () => {
|
|
504
|
+
it('sends a follow-up message/send request', async () => {
|
|
505
|
+
// Set up SSE stream that emits submitted + input-required
|
|
506
|
+
let streamController;
|
|
507
|
+
const encoder = new TextEncoder();
|
|
508
|
+
const readableStream = new ReadableStream({
|
|
509
|
+
start(controller) {
|
|
510
|
+
streamController = controller;
|
|
511
|
+
controller.enqueue(encoder.encode(sseEvent('TaskStatusUpdate', {
|
|
512
|
+
id: 'task-inject',
|
|
513
|
+
sessionId: 'sess-inject',
|
|
514
|
+
status: { state: 'submitted' },
|
|
515
|
+
})));
|
|
516
|
+
controller.enqueue(encoder.encode(sseEvent('TaskStatusUpdate', {
|
|
517
|
+
id: 'task-inject',
|
|
518
|
+
status: {
|
|
519
|
+
state: 'input-required',
|
|
520
|
+
message: { role: 'agent', parts: [{ type: 'text', text: 'Need more info' }] },
|
|
521
|
+
},
|
|
522
|
+
})));
|
|
523
|
+
},
|
|
524
|
+
});
|
|
525
|
+
const sseResponse = new Response(readableStream, {
|
|
526
|
+
status: 200,
|
|
527
|
+
headers: { 'content-type': 'text/event-stream' },
|
|
528
|
+
});
|
|
529
|
+
// First call: SSE stream
|
|
530
|
+
mockFetch.mockResolvedValueOnce(sseResponse);
|
|
531
|
+
// Second call: injectMessage response
|
|
532
|
+
mockFetch.mockResolvedValueOnce(createJsonRpcResponse('rpc-inject', {
|
|
533
|
+
id: 'task-inject',
|
|
534
|
+
sessionId: 'sess-inject',
|
|
535
|
+
status: { state: 'working' },
|
|
536
|
+
}));
|
|
537
|
+
const config = makeConfig();
|
|
538
|
+
const handle = provider.spawn(config);
|
|
539
|
+
// Start consuming the stream in the background
|
|
540
|
+
const eventsPromise = collectEvents(handle.stream);
|
|
541
|
+
// Wait for events to arrive
|
|
542
|
+
await new Promise(resolve => setTimeout(resolve, 50));
|
|
543
|
+
// Inject a follow-up message
|
|
544
|
+
await handle.injectMessage('Here is the additional info');
|
|
545
|
+
// Verify injectMessage was sent as message/send
|
|
546
|
+
const injectCall = mockFetch.mock.calls.find((call) => {
|
|
547
|
+
try {
|
|
548
|
+
const body = JSON.parse(call[1].body);
|
|
549
|
+
return body.method === 'message/send' && body.params?.message?.parts?.[0]?.text === 'Here is the additional info';
|
|
550
|
+
}
|
|
551
|
+
catch {
|
|
552
|
+
return false;
|
|
553
|
+
}
|
|
554
|
+
});
|
|
555
|
+
expect(injectCall).toBeDefined();
|
|
556
|
+
const injectBody = JSON.parse(injectCall[1].body);
|
|
557
|
+
expect(injectBody.params.message).toMatchObject({
|
|
558
|
+
role: 'user',
|
|
559
|
+
parts: [{ type: 'text', text: 'Here is the additional info' }],
|
|
560
|
+
});
|
|
561
|
+
// Should include sessionId from the stream
|
|
562
|
+
expect(injectBody.params.sessionId).toBe('sess-inject');
|
|
563
|
+
// Clean up: close the stream and abort
|
|
564
|
+
try {
|
|
565
|
+
streamController.close();
|
|
566
|
+
}
|
|
567
|
+
catch {
|
|
568
|
+
// Already closed
|
|
569
|
+
}
|
|
570
|
+
config.abortController.abort();
|
|
571
|
+
await eventsPromise.catch(() => { });
|
|
572
|
+
});
|
|
573
|
+
});
|
|
574
|
+
// -------------------------------------------------------------------------
|
|
575
|
+
// Additional: Auth headers
|
|
576
|
+
// -------------------------------------------------------------------------
|
|
577
|
+
describe('auth headers', () => {
|
|
578
|
+
it('sends x-api-key header when A2A_API_KEY is set', async () => {
|
|
579
|
+
mockFetch.mockResolvedValueOnce(createSSEResponse([
|
|
580
|
+
sseEvent('TaskStatusUpdate', {
|
|
581
|
+
id: 'task-auth',
|
|
582
|
+
sessionId: 'sess-auth',
|
|
583
|
+
status: { state: 'submitted' },
|
|
584
|
+
}),
|
|
585
|
+
sseEvent('TaskStatusUpdate', {
|
|
586
|
+
id: 'task-auth',
|
|
587
|
+
status: { state: 'completed' },
|
|
588
|
+
final: true,
|
|
589
|
+
}),
|
|
590
|
+
]));
|
|
591
|
+
const config = makeConfig({
|
|
592
|
+
env: {
|
|
593
|
+
A2A_AGENT_URL: 'http://test-agent:8080',
|
|
594
|
+
A2A_API_KEY: 'test-key-123',
|
|
595
|
+
},
|
|
596
|
+
});
|
|
597
|
+
const handle = provider.spawn(config);
|
|
598
|
+
await collectEvents(handle.stream);
|
|
599
|
+
const headers = mockFetch.mock.calls[0][1].headers;
|
|
600
|
+
expect(headers['x-api-key']).toBe('test-key-123');
|
|
601
|
+
});
|
|
602
|
+
it('sends Authorization Bearer header when A2A_BEARER_TOKEN is set', async () => {
|
|
603
|
+
mockFetch.mockResolvedValueOnce(createSSEResponse([
|
|
604
|
+
sseEvent('TaskStatusUpdate', {
|
|
605
|
+
id: 'task-bearer',
|
|
606
|
+
sessionId: 'sess-bearer',
|
|
607
|
+
status: { state: 'submitted' },
|
|
608
|
+
}),
|
|
609
|
+
sseEvent('TaskStatusUpdate', {
|
|
610
|
+
id: 'task-bearer',
|
|
611
|
+
status: { state: 'completed' },
|
|
612
|
+
final: true,
|
|
613
|
+
}),
|
|
614
|
+
]));
|
|
615
|
+
const config = makeConfig({
|
|
616
|
+
env: {
|
|
617
|
+
A2A_AGENT_URL: 'http://test-agent:8080',
|
|
618
|
+
A2A_BEARER_TOKEN: 'my-bearer-token',
|
|
619
|
+
},
|
|
620
|
+
});
|
|
621
|
+
const handle = provider.spawn(config);
|
|
622
|
+
await collectEvents(handle.stream);
|
|
623
|
+
const headers = mockFetch.mock.calls[0][1].headers;
|
|
624
|
+
expect(headers['Authorization']).toBe('Bearer my-bearer-token');
|
|
625
|
+
});
|
|
626
|
+
});
|
|
627
|
+
// -------------------------------------------------------------------------
|
|
628
|
+
// Additional: Work-type URL resolution
|
|
629
|
+
// -------------------------------------------------------------------------
|
|
630
|
+
describe('work-type-specific URL resolution', () => {
|
|
631
|
+
it('uses A2A_AGENT_URL_RESEARCH when present', async () => {
|
|
632
|
+
mockFetch.mockResolvedValueOnce(createSSEResponse([
|
|
633
|
+
sseEvent('TaskStatusUpdate', {
|
|
634
|
+
id: 'task-wt',
|
|
635
|
+
sessionId: 'sess-wt',
|
|
636
|
+
status: { state: 'submitted' },
|
|
637
|
+
}),
|
|
638
|
+
sseEvent('TaskStatusUpdate', {
|
|
639
|
+
id: 'task-wt',
|
|
640
|
+
status: { state: 'completed' },
|
|
641
|
+
final: true,
|
|
642
|
+
}),
|
|
643
|
+
]));
|
|
644
|
+
const config = makeConfig({
|
|
645
|
+
env: {
|
|
646
|
+
A2A_AGENT_URL_RESEARCH: 'http://research-agent:9090',
|
|
647
|
+
},
|
|
648
|
+
});
|
|
649
|
+
const handle = provider.spawn(config);
|
|
650
|
+
await collectEvents(handle.stream);
|
|
651
|
+
const url = mockFetch.mock.calls[0][0];
|
|
652
|
+
expect(url).toBe('http://research-agent:9090/a2a');
|
|
653
|
+
});
|
|
654
|
+
});
|
|
655
|
+
// -------------------------------------------------------------------------
|
|
656
|
+
// Additional: createProvider factory
|
|
657
|
+
// -------------------------------------------------------------------------
|
|
658
|
+
describe('createProvider integration', () => {
|
|
659
|
+
it('creates A2aProvider via factory', async () => {
|
|
660
|
+
const { createProvider } = await import('./index.js');
|
|
661
|
+
const p = createProvider('a2a');
|
|
662
|
+
expect(p.name).toBe('a2a');
|
|
663
|
+
});
|
|
664
|
+
});
|
|
665
|
+
});
|