@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.
Files changed (246) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +125 -0
  3. package/dist/src/config/index.d.ts +3 -0
  4. package/dist/src/config/index.d.ts.map +1 -0
  5. package/dist/src/config/index.js +1 -0
  6. package/dist/src/config/repository-config.d.ts +44 -0
  7. package/dist/src/config/repository-config.d.ts.map +1 -0
  8. package/dist/src/config/repository-config.js +88 -0
  9. package/dist/src/config/repository-config.test.d.ts +2 -0
  10. package/dist/src/config/repository-config.test.d.ts.map +1 -0
  11. package/dist/src/config/repository-config.test.js +249 -0
  12. package/dist/src/deployment/deployment-checker.d.ts +110 -0
  13. package/dist/src/deployment/deployment-checker.d.ts.map +1 -0
  14. package/dist/src/deployment/deployment-checker.js +242 -0
  15. package/dist/src/deployment/index.d.ts +3 -0
  16. package/dist/src/deployment/index.d.ts.map +1 -0
  17. package/dist/src/deployment/index.js +2 -0
  18. package/dist/src/frontend/index.d.ts +2 -0
  19. package/dist/src/frontend/index.d.ts.map +1 -0
  20. package/dist/src/frontend/index.js +1 -0
  21. package/dist/src/frontend/types.d.ts +106 -0
  22. package/dist/src/frontend/types.d.ts.map +1 -0
  23. package/dist/src/frontend/types.js +11 -0
  24. package/dist/src/governor/decision-engine.d.ts +52 -0
  25. package/dist/src/governor/decision-engine.d.ts.map +1 -0
  26. package/dist/src/governor/decision-engine.js +220 -0
  27. package/dist/src/governor/decision-engine.test.d.ts +2 -0
  28. package/dist/src/governor/decision-engine.test.d.ts.map +1 -0
  29. package/dist/src/governor/decision-engine.test.js +629 -0
  30. package/dist/src/governor/event-bus.d.ts +43 -0
  31. package/dist/src/governor/event-bus.d.ts.map +1 -0
  32. package/dist/src/governor/event-bus.js +8 -0
  33. package/dist/src/governor/event-deduplicator.d.ts +43 -0
  34. package/dist/src/governor/event-deduplicator.d.ts.map +1 -0
  35. package/dist/src/governor/event-deduplicator.js +53 -0
  36. package/dist/src/governor/event-driven-governor.d.ts +131 -0
  37. package/dist/src/governor/event-driven-governor.d.ts.map +1 -0
  38. package/dist/src/governor/event-driven-governor.js +379 -0
  39. package/dist/src/governor/event-driven-governor.test.d.ts +2 -0
  40. package/dist/src/governor/event-driven-governor.test.d.ts.map +1 -0
  41. package/dist/src/governor/event-driven-governor.test.js +673 -0
  42. package/dist/src/governor/event-types.d.ts +78 -0
  43. package/dist/src/governor/event-types.d.ts.map +1 -0
  44. package/dist/src/governor/event-types.js +32 -0
  45. package/dist/src/governor/governor-types.d.ts +82 -0
  46. package/dist/src/governor/governor-types.d.ts.map +1 -0
  47. package/dist/src/governor/governor-types.js +21 -0
  48. package/dist/src/governor/governor.d.ts +100 -0
  49. package/dist/src/governor/governor.d.ts.map +1 -0
  50. package/dist/src/governor/governor.js +262 -0
  51. package/dist/src/governor/governor.test.d.ts +2 -0
  52. package/dist/src/governor/governor.test.d.ts.map +1 -0
  53. package/dist/src/governor/governor.test.js +514 -0
  54. package/dist/src/governor/human-touchpoints.d.ts +131 -0
  55. package/dist/src/governor/human-touchpoints.d.ts.map +1 -0
  56. package/dist/src/governor/human-touchpoints.js +251 -0
  57. package/dist/src/governor/human-touchpoints.test.d.ts +2 -0
  58. package/dist/src/governor/human-touchpoints.test.d.ts.map +1 -0
  59. package/dist/src/governor/human-touchpoints.test.js +366 -0
  60. package/dist/src/governor/in-memory-event-bus.d.ts +29 -0
  61. package/dist/src/governor/in-memory-event-bus.d.ts.map +1 -0
  62. package/dist/src/governor/in-memory-event-bus.js +79 -0
  63. package/dist/src/governor/index.d.ts +14 -0
  64. package/dist/src/governor/index.d.ts.map +1 -0
  65. package/dist/src/governor/index.js +13 -0
  66. package/dist/src/governor/override-parser.d.ts +60 -0
  67. package/dist/src/governor/override-parser.d.ts.map +1 -0
  68. package/dist/src/governor/override-parser.js +98 -0
  69. package/dist/src/governor/override-parser.test.d.ts +2 -0
  70. package/dist/src/governor/override-parser.test.d.ts.map +1 -0
  71. package/dist/src/governor/override-parser.test.js +312 -0
  72. package/dist/src/governor/platform-adapter.d.ts +69 -0
  73. package/dist/src/governor/platform-adapter.d.ts.map +1 -0
  74. package/dist/src/governor/platform-adapter.js +11 -0
  75. package/dist/src/governor/processing-state.d.ts +66 -0
  76. package/dist/src/governor/processing-state.d.ts.map +1 -0
  77. package/dist/src/governor/processing-state.js +43 -0
  78. package/dist/src/governor/processing-state.test.d.ts +2 -0
  79. package/dist/src/governor/processing-state.test.d.ts.map +1 -0
  80. package/dist/src/governor/processing-state.test.js +96 -0
  81. package/dist/src/governor/top-of-funnel.d.ts +118 -0
  82. package/dist/src/governor/top-of-funnel.d.ts.map +1 -0
  83. package/dist/src/governor/top-of-funnel.js +168 -0
  84. package/dist/src/governor/top-of-funnel.test.d.ts +2 -0
  85. package/dist/src/governor/top-of-funnel.test.d.ts.map +1 -0
  86. package/dist/src/governor/top-of-funnel.test.js +331 -0
  87. package/dist/src/index.d.ts +11 -0
  88. package/dist/src/index.d.ts.map +1 -0
  89. package/dist/src/index.js +10 -0
  90. package/dist/src/linear-cli.d.ts +38 -0
  91. package/dist/src/linear-cli.d.ts.map +1 -0
  92. package/dist/src/linear-cli.js +674 -0
  93. package/dist/src/logger.d.ts +117 -0
  94. package/dist/src/logger.d.ts.map +1 -0
  95. package/dist/src/logger.js +430 -0
  96. package/dist/src/manifest/generate.d.ts +20 -0
  97. package/dist/src/manifest/generate.d.ts.map +1 -0
  98. package/dist/src/manifest/generate.js +65 -0
  99. package/dist/src/manifest/index.d.ts +4 -0
  100. package/dist/src/manifest/index.d.ts.map +1 -0
  101. package/dist/src/manifest/index.js +2 -0
  102. package/dist/src/manifest/route-manifest.d.ts +34 -0
  103. package/dist/src/manifest/route-manifest.d.ts.map +1 -0
  104. package/dist/src/manifest/route-manifest.js +148 -0
  105. package/dist/src/orchestrator/activity-emitter.d.ts +119 -0
  106. package/dist/src/orchestrator/activity-emitter.d.ts.map +1 -0
  107. package/dist/src/orchestrator/activity-emitter.js +306 -0
  108. package/dist/src/orchestrator/api-activity-emitter.d.ts +167 -0
  109. package/dist/src/orchestrator/api-activity-emitter.d.ts.map +1 -0
  110. package/dist/src/orchestrator/api-activity-emitter.js +417 -0
  111. package/dist/src/orchestrator/heartbeat-writer.d.ts +57 -0
  112. package/dist/src/orchestrator/heartbeat-writer.d.ts.map +1 -0
  113. package/dist/src/orchestrator/heartbeat-writer.js +137 -0
  114. package/dist/src/orchestrator/index.d.ts +20 -0
  115. package/dist/src/orchestrator/index.d.ts.map +1 -0
  116. package/dist/src/orchestrator/index.js +22 -0
  117. package/dist/src/orchestrator/log-analyzer.d.ts +160 -0
  118. package/dist/src/orchestrator/log-analyzer.d.ts.map +1 -0
  119. package/dist/src/orchestrator/log-analyzer.js +572 -0
  120. package/dist/src/orchestrator/log-config.d.ts +39 -0
  121. package/dist/src/orchestrator/log-config.d.ts.map +1 -0
  122. package/dist/src/orchestrator/log-config.js +45 -0
  123. package/dist/src/orchestrator/orchestrator.d.ts +316 -0
  124. package/dist/src/orchestrator/orchestrator.d.ts.map +1 -0
  125. package/dist/src/orchestrator/orchestrator.js +3290 -0
  126. package/dist/src/orchestrator/parse-work-result.d.ts +16 -0
  127. package/dist/src/orchestrator/parse-work-result.d.ts.map +1 -0
  128. package/dist/src/orchestrator/parse-work-result.js +135 -0
  129. package/dist/src/orchestrator/parse-work-result.test.d.ts +2 -0
  130. package/dist/src/orchestrator/parse-work-result.test.d.ts.map +1 -0
  131. package/dist/src/orchestrator/parse-work-result.test.js +234 -0
  132. package/dist/src/orchestrator/progress-logger.d.ts +72 -0
  133. package/dist/src/orchestrator/progress-logger.d.ts.map +1 -0
  134. package/dist/src/orchestrator/progress-logger.js +135 -0
  135. package/dist/src/orchestrator/session-logger.d.ts +159 -0
  136. package/dist/src/orchestrator/session-logger.d.ts.map +1 -0
  137. package/dist/src/orchestrator/session-logger.js +275 -0
  138. package/dist/src/orchestrator/state-recovery.d.ts +96 -0
  139. package/dist/src/orchestrator/state-recovery.d.ts.map +1 -0
  140. package/dist/src/orchestrator/state-recovery.js +302 -0
  141. package/dist/src/orchestrator/state-types.d.ts +165 -0
  142. package/dist/src/orchestrator/state-types.d.ts.map +1 -0
  143. package/dist/src/orchestrator/state-types.js +7 -0
  144. package/dist/src/orchestrator/stream-parser.d.ts +151 -0
  145. package/dist/src/orchestrator/stream-parser.d.ts.map +1 -0
  146. package/dist/src/orchestrator/stream-parser.js +137 -0
  147. package/dist/src/orchestrator/types.d.ts +232 -0
  148. package/dist/src/orchestrator/types.d.ts.map +1 -0
  149. package/dist/src/orchestrator/types.js +4 -0
  150. package/dist/src/orchestrator/validate-git-remote.test.d.ts +2 -0
  151. package/dist/src/orchestrator/validate-git-remote.test.d.ts.map +1 -0
  152. package/dist/src/orchestrator/validate-git-remote.test.js +61 -0
  153. package/dist/src/providers/a2a-auth.d.ts +81 -0
  154. package/dist/src/providers/a2a-auth.d.ts.map +1 -0
  155. package/dist/src/providers/a2a-auth.js +188 -0
  156. package/dist/src/providers/a2a-auth.test.d.ts +2 -0
  157. package/dist/src/providers/a2a-auth.test.d.ts.map +1 -0
  158. package/dist/src/providers/a2a-auth.test.js +232 -0
  159. package/dist/src/providers/a2a-provider.d.ts +254 -0
  160. package/dist/src/providers/a2a-provider.d.ts.map +1 -0
  161. package/dist/src/providers/a2a-provider.integration.test.d.ts +9 -0
  162. package/dist/src/providers/a2a-provider.integration.test.d.ts.map +1 -0
  163. package/dist/src/providers/a2a-provider.integration.test.js +665 -0
  164. package/dist/src/providers/a2a-provider.js +811 -0
  165. package/dist/src/providers/a2a-provider.test.d.ts +2 -0
  166. package/dist/src/providers/a2a-provider.test.d.ts.map +1 -0
  167. package/dist/src/providers/a2a-provider.test.js +681 -0
  168. package/dist/src/providers/amp-provider.d.ts +20 -0
  169. package/dist/src/providers/amp-provider.d.ts.map +1 -0
  170. package/dist/src/providers/amp-provider.js +24 -0
  171. package/dist/src/providers/claude-provider.d.ts +18 -0
  172. package/dist/src/providers/claude-provider.d.ts.map +1 -0
  173. package/dist/src/providers/claude-provider.js +437 -0
  174. package/dist/src/providers/codex-provider.d.ts +133 -0
  175. package/dist/src/providers/codex-provider.d.ts.map +1 -0
  176. package/dist/src/providers/codex-provider.js +381 -0
  177. package/dist/src/providers/codex-provider.test.d.ts +2 -0
  178. package/dist/src/providers/codex-provider.test.d.ts.map +1 -0
  179. package/dist/src/providers/codex-provider.test.js +387 -0
  180. package/dist/src/providers/index.d.ts +44 -0
  181. package/dist/src/providers/index.d.ts.map +1 -0
  182. package/dist/src/providers/index.js +85 -0
  183. package/dist/src/providers/spring-ai-provider.d.ts +90 -0
  184. package/dist/src/providers/spring-ai-provider.d.ts.map +1 -0
  185. package/dist/src/providers/spring-ai-provider.integration.test.d.ts +13 -0
  186. package/dist/src/providers/spring-ai-provider.integration.test.d.ts.map +1 -0
  187. package/dist/src/providers/spring-ai-provider.integration.test.js +351 -0
  188. package/dist/src/providers/spring-ai-provider.js +317 -0
  189. package/dist/src/providers/spring-ai-provider.test.d.ts +2 -0
  190. package/dist/src/providers/spring-ai-provider.test.d.ts.map +1 -0
  191. package/dist/src/providers/spring-ai-provider.test.js +200 -0
  192. package/dist/src/providers/types.d.ts +165 -0
  193. package/dist/src/providers/types.d.ts.map +1 -0
  194. package/dist/src/providers/types.js +13 -0
  195. package/dist/src/templates/adapters.d.ts +51 -0
  196. package/dist/src/templates/adapters.d.ts.map +1 -0
  197. package/dist/src/templates/adapters.js +104 -0
  198. package/dist/src/templates/adapters.test.d.ts +2 -0
  199. package/dist/src/templates/adapters.test.d.ts.map +1 -0
  200. package/dist/src/templates/adapters.test.js +165 -0
  201. package/dist/src/templates/agent-definition.d.ts +85 -0
  202. package/dist/src/templates/agent-definition.d.ts.map +1 -0
  203. package/dist/src/templates/agent-definition.js +97 -0
  204. package/dist/src/templates/agent-definition.test.d.ts +2 -0
  205. package/dist/src/templates/agent-definition.test.d.ts.map +1 -0
  206. package/dist/src/templates/agent-definition.test.js +209 -0
  207. package/dist/src/templates/index.d.ts +14 -0
  208. package/dist/src/templates/index.d.ts.map +1 -0
  209. package/dist/src/templates/index.js +11 -0
  210. package/dist/src/templates/loader.d.ts +41 -0
  211. package/dist/src/templates/loader.d.ts.map +1 -0
  212. package/dist/src/templates/loader.js +114 -0
  213. package/dist/src/templates/registry.d.ts +80 -0
  214. package/dist/src/templates/registry.d.ts.map +1 -0
  215. package/dist/src/templates/registry.js +177 -0
  216. package/dist/src/templates/registry.test.d.ts +2 -0
  217. package/dist/src/templates/registry.test.d.ts.map +1 -0
  218. package/dist/src/templates/registry.test.js +198 -0
  219. package/dist/src/templates/renderer.d.ts +29 -0
  220. package/dist/src/templates/renderer.d.ts.map +1 -0
  221. package/dist/src/templates/renderer.js +35 -0
  222. package/dist/src/templates/strategy-templates.test.d.ts +2 -0
  223. package/dist/src/templates/strategy-templates.test.d.ts.map +1 -0
  224. package/dist/src/templates/strategy-templates.test.js +619 -0
  225. package/dist/src/templates/types.d.ts +233 -0
  226. package/dist/src/templates/types.d.ts.map +1 -0
  227. package/dist/src/templates/types.js +127 -0
  228. package/dist/src/templates/types.test.d.ts +2 -0
  229. package/dist/src/templates/types.test.d.ts.map +1 -0
  230. package/dist/src/templates/types.test.js +232 -0
  231. package/dist/src/tools/index.d.ts +6 -0
  232. package/dist/src/tools/index.d.ts.map +1 -0
  233. package/dist/src/tools/index.js +3 -0
  234. package/dist/src/tools/linear-runner.d.ts +34 -0
  235. package/dist/src/tools/linear-runner.d.ts.map +1 -0
  236. package/dist/src/tools/linear-runner.js +700 -0
  237. package/dist/src/tools/plugins/linear.d.ts +9 -0
  238. package/dist/src/tools/plugins/linear.d.ts.map +1 -0
  239. package/dist/src/tools/plugins/linear.js +138 -0
  240. package/dist/src/tools/registry.d.ts +9 -0
  241. package/dist/src/tools/registry.d.ts.map +1 -0
  242. package/dist/src/tools/registry.js +18 -0
  243. package/dist/src/tools/types.d.ts +18 -0
  244. package/dist/src/tools/types.d.ts.map +1 -0
  245. package/dist/src/tools/types.js +1 -0
  246. 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
+ });