@assistant-ui/react-a2a 0.2.31 → 0.2.33

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.
@@ -43,6 +43,7 @@ function mockSSETextResponse(
43
43
  return Promise.resolve({ done: true, value: undefined });
44
44
  }),
45
45
  releaseLock: vi.fn(),
46
+ cancel: vi.fn(async () => {}),
46
47
  }),
47
48
  },
48
49
  } as unknown as Response;
@@ -742,6 +743,20 @@ describe("A2AClient", () => {
742
743
  "Invalid A2A tasks:get response: expected a valid task payload.",
743
744
  );
744
745
  });
746
+
747
+ it("rejects a non-string contextId", async () => {
748
+ fetchMock.mockResolvedValue(
749
+ mockFetchResponse({
750
+ id: "t1",
751
+ contextId: 999,
752
+ status: { state: "completed" },
753
+ }),
754
+ );
755
+
756
+ await expect(client.getTask("t1")).rejects.toThrow(
757
+ "Invalid A2A tasks:get response: expected a valid task payload.",
758
+ );
759
+ });
745
760
  });
746
761
 
747
762
  // --- listTasks ---
@@ -1016,6 +1031,228 @@ describe("A2AClient", () => {
1016
1031
  expect(evt.event.status.message?.role).toBe("agent");
1017
1032
  });
1018
1033
 
1034
+ it("drops a wrapped task or message whose ids are not strings", async () => {
1035
+ const frames = [
1036
+ {
1037
+ task: {
1038
+ id: "t1",
1039
+ context_id: 999,
1040
+ status: { state: "TASK_STATE_WORKING" },
1041
+ },
1042
+ },
1043
+ {
1044
+ message: {
1045
+ message_id: "m1",
1046
+ context_id: 42,
1047
+ role: "ROLE_AGENT",
1048
+ parts: [{ text: "hi" }],
1049
+ },
1050
+ },
1051
+ {
1052
+ message: {
1053
+ message_id: "m2",
1054
+ task_id: { nested: "object" },
1055
+ role: "ROLE_AGENT",
1056
+ parts: [{ text: "hi" }],
1057
+ },
1058
+ },
1059
+ ];
1060
+
1061
+ for (const frame of frames) {
1062
+ fetchMock.mockResolvedValue(
1063
+ mockSSEResponse([`data: ${JSON.stringify(frame)}`, "", ""]),
1064
+ );
1065
+
1066
+ const events: A2AStreamEvent[] = [];
1067
+ for await (const event of client.streamMessage(userMessage)) {
1068
+ events.push(event);
1069
+ }
1070
+
1071
+ expect(events).toEqual([]);
1072
+ }
1073
+ });
1074
+
1075
+ it("drops a wrapped status update whose contextId is not a string", async () => {
1076
+ const sseData = JSON.stringify({
1077
+ status_update: {
1078
+ task_id: "t1",
1079
+ context_id: 999,
1080
+ status: { state: "TASK_STATE_WORKING" },
1081
+ },
1082
+ });
1083
+
1084
+ fetchMock.mockResolvedValue(
1085
+ mockSSEResponse([`data: ${sseData}`, "", ""]),
1086
+ );
1087
+
1088
+ const events: A2AStreamEvent[] = [];
1089
+ for await (const event of client.streamMessage(userMessage)) {
1090
+ events.push(event);
1091
+ }
1092
+
1093
+ expect(events).toEqual([]);
1094
+ });
1095
+
1096
+ it("drops a wrapped artifact update whose ids are not strings", async () => {
1097
+ const artifact = { artifact_id: "a1", parts: [{ text: "hi" }] };
1098
+ for (const ids of [
1099
+ { task_id: 12345, context_id: "c1" },
1100
+ { task_id: "t1", context_id: { nested: "object" } },
1101
+ ]) {
1102
+ const sseData = JSON.stringify({
1103
+ artifact_update: { ...ids, artifact },
1104
+ });
1105
+
1106
+ fetchMock.mockResolvedValue(
1107
+ mockSSEResponse([`data: ${sseData}`, "", ""]),
1108
+ );
1109
+
1110
+ const events: A2AStreamEvent[] = [];
1111
+ for await (const event of client.streamMessage(userMessage)) {
1112
+ events.push(event);
1113
+ }
1114
+
1115
+ expect(events).toEqual([]);
1116
+ }
1117
+ });
1118
+
1119
+ it("normalizes default-valued wrapped status updates", async () => {
1120
+ const sseData = JSON.stringify({
1121
+ status_update: {
1122
+ status: {
1123
+ message: {
1124
+ message_id: "s1",
1125
+ role: "ROLE_AGENT",
1126
+ parts: [{ text: "Waiting..." }],
1127
+ },
1128
+ },
1129
+ },
1130
+ });
1131
+
1132
+ fetchMock.mockResolvedValue(
1133
+ mockSSEResponse([`data: ${sseData}`, "", ""]),
1134
+ );
1135
+
1136
+ const events: A2AStreamEvent[] = [];
1137
+ for await (const event of client.streamMessage(userMessage)) {
1138
+ events.push(event);
1139
+ }
1140
+
1141
+ expect(events).toHaveLength(1);
1142
+ const evt = events[0] as Extract<
1143
+ A2AStreamEvent,
1144
+ { type: "statusUpdate" }
1145
+ >;
1146
+ expect(evt.event).toMatchObject({
1147
+ taskId: "",
1148
+ contextId: "",
1149
+ status: {
1150
+ state: "unspecified",
1151
+ message: { parts: [{ text: "Waiting..." }] },
1152
+ },
1153
+ });
1154
+ });
1155
+
1156
+ it("normalizes null-valued wrapped status defaults", async () => {
1157
+ const sseData = JSON.stringify({
1158
+ status_update: {
1159
+ task_id: null,
1160
+ context_id: null,
1161
+ status: { state: null },
1162
+ },
1163
+ });
1164
+
1165
+ fetchMock.mockResolvedValue(
1166
+ mockSSEResponse([`data: ${sseData}`, "", ""]),
1167
+ );
1168
+
1169
+ const events: A2AStreamEvent[] = [];
1170
+ for await (const event of client.streamMessage(userMessage)) {
1171
+ events.push(event);
1172
+ }
1173
+
1174
+ expect(events).toMatchObject([
1175
+ {
1176
+ type: "statusUpdate",
1177
+ event: {
1178
+ taskId: "",
1179
+ contextId: "",
1180
+ status: { state: "unspecified" },
1181
+ },
1182
+ },
1183
+ ]);
1184
+ });
1185
+
1186
+ it("normalizes ProtoJSON defaults across wrapped event types", async () => {
1187
+ const frames = [
1188
+ {
1189
+ task: {
1190
+ id: "t1",
1191
+ status: {
1192
+ message: {
1193
+ message_id: "m1",
1194
+ role: "ROLE_AGENT",
1195
+ parts: [{ text: "Working..." }],
1196
+ },
1197
+ },
1198
+ },
1199
+ },
1200
+ {
1201
+ message: {
1202
+ message_id: "m2",
1203
+ role: "ROLE_AGENT",
1204
+ },
1205
+ },
1206
+ {
1207
+ artifact_update: {
1208
+ artifact: {},
1209
+ },
1210
+ },
1211
+ ];
1212
+
1213
+ fetchMock.mockResolvedValue(
1214
+ mockSSEResponse(
1215
+ frames
1216
+ .flatMap((frame) => [`data: ${JSON.stringify(frame)}`, ""])
1217
+ .concat(""),
1218
+ ),
1219
+ );
1220
+
1221
+ const events: A2AStreamEvent[] = [];
1222
+ for await (const event of client.streamMessage(userMessage)) {
1223
+ events.push(event);
1224
+ }
1225
+
1226
+ expect(events).toMatchObject([
1227
+ {
1228
+ type: "task",
1229
+ task: {
1230
+ id: "t1",
1231
+ contextId: "",
1232
+ status: { state: "unspecified" },
1233
+ },
1234
+ },
1235
+ {
1236
+ type: "message",
1237
+ message: {
1238
+ messageId: "m2",
1239
+ contextId: "",
1240
+ taskId: "",
1241
+ role: "agent",
1242
+ parts: [],
1243
+ },
1244
+ },
1245
+ {
1246
+ type: "artifactUpdate",
1247
+ event: {
1248
+ taskId: "",
1249
+ contextId: "",
1250
+ artifact: { artifactId: "", parts: [] },
1251
+ },
1252
+ },
1253
+ ]);
1254
+ });
1255
+
1019
1256
  it("cancels the response body when iteration stops early", async () => {
1020
1257
  const sseData = JSON.stringify({
1021
1258
  status_update: {
@@ -1425,6 +1662,14 @@ describe("A2AClient", () => {
1425
1662
  "",
1426
1663
  "data: {}",
1427
1664
  "",
1665
+ 'data: {"task":{}}',
1666
+ "",
1667
+ 'data: {"message":{}}',
1668
+ "",
1669
+ 'data: {"status_update":{}}',
1670
+ "",
1671
+ 'data: {"artifact_update":{}}',
1672
+ "",
1428
1673
  `data: ${second}`,
1429
1674
  "",
1430
1675
  "",
@@ -1731,4 +1976,423 @@ describe("A2AClient", () => {
1731
1976
  expect(card.skills[0]).toMatchObject({ id: "s", tags: [] });
1732
1977
  });
1733
1978
  });
1979
+
1980
+ describe("streamMessage - JSON-RPC kind-discriminated results", () => {
1981
+ const rpc = (result: unknown) =>
1982
+ `data: ${JSON.stringify({ jsonrpc: "2.0", id: 1, result })}`;
1983
+
1984
+ it("parses a flat status-update result", async () => {
1985
+ fetchMock.mockResolvedValue(
1986
+ mockSSEResponse([
1987
+ rpc({
1988
+ kind: "status-update",
1989
+ taskId: "t1",
1990
+ contextId: "ctx-1",
1991
+ status: { state: "working" },
1992
+ final: false,
1993
+ }),
1994
+ "",
1995
+ "",
1996
+ ]),
1997
+ );
1998
+
1999
+ const events: A2AStreamEvent[] = [];
2000
+ for await (const event of client.streamMessage(userMessage)) {
2001
+ events.push(event);
2002
+ }
2003
+
2004
+ expect(events).toHaveLength(1);
2005
+ expect(events[0]!.type).toBe("statusUpdate");
2006
+ const evt = events[0] as Extract<
2007
+ A2AStreamEvent,
2008
+ { type: "statusUpdate" }
2009
+ >;
2010
+ expect(evt.event.taskId).toBe("t1");
2011
+ expect(evt.event.status.state).toBe("working");
2012
+ });
2013
+
2014
+ it("parses a flat task result and maps hyphenated states", async () => {
2015
+ fetchMock.mockResolvedValue(
2016
+ mockSSEResponse([
2017
+ rpc({
2018
+ kind: "task",
2019
+ id: "t1",
2020
+ contextId: "ctx-1",
2021
+ status: { state: "input-required" },
2022
+ }),
2023
+ "",
2024
+ "",
2025
+ ]),
2026
+ );
2027
+
2028
+ const events: A2AStreamEvent[] = [];
2029
+ for await (const event of client.streamMessage(userMessage)) {
2030
+ events.push(event);
2031
+ }
2032
+
2033
+ expect(events).toHaveLength(1);
2034
+ expect(events[0]!.type).toBe("task");
2035
+ const evt = events[0] as Extract<A2AStreamEvent, { type: "task" }>;
2036
+ expect(evt.task.status.state).toBe("input_required");
2037
+ });
2038
+
2039
+ it("maps the unknown wire state to unspecified", async () => {
2040
+ fetchMock.mockResolvedValue(
2041
+ mockSSEResponse([
2042
+ rpc({
2043
+ kind: "task",
2044
+ id: "t1",
2045
+ contextId: "ctx-1",
2046
+ status: { state: "unknown" },
2047
+ }),
2048
+ "",
2049
+ "",
2050
+ ]),
2051
+ );
2052
+
2053
+ const events: A2AStreamEvent[] = [];
2054
+ for await (const event of client.streamMessage(userMessage)) {
2055
+ events.push(event);
2056
+ }
2057
+
2058
+ const evt = events[0] as Extract<A2AStreamEvent, { type: "task" }>;
2059
+ expect(evt.task.status.state).toBe("unspecified");
2060
+ });
2061
+
2062
+ it("strips the wire discriminators from emitted events", async () => {
2063
+ fetchMock.mockResolvedValue(
2064
+ mockSSEResponse([
2065
+ rpc({
2066
+ kind: "status-update",
2067
+ taskId: "t1",
2068
+ contextId: "ctx-1",
2069
+ status: { state: "working" },
2070
+ final: true,
2071
+ }),
2072
+ "",
2073
+ "",
2074
+ ]),
2075
+ );
2076
+
2077
+ const events: A2AStreamEvent[] = [];
2078
+ for await (const event of client.streamMessage(userMessage)) {
2079
+ events.push(event);
2080
+ }
2081
+
2082
+ const evt = events[0] as Extract<
2083
+ A2AStreamEvent,
2084
+ { type: "statusUpdate" }
2085
+ >;
2086
+ expect(evt.event).not.toHaveProperty("kind");
2087
+ expect(evt.event).not.toHaveProperty("final");
2088
+ });
2089
+
2090
+ it("strips the kind discriminator from text and data parts", async () => {
2091
+ fetchMock.mockResolvedValue(
2092
+ mockSSEResponse([
2093
+ rpc({
2094
+ kind: "message",
2095
+ messageId: "m1",
2096
+ role: "agent",
2097
+ parts: [
2098
+ { kind: "text", text: "hello" },
2099
+ { kind: "data", data: { answer: 42 } },
2100
+ ],
2101
+ }),
2102
+ "",
2103
+ "",
2104
+ ]),
2105
+ );
2106
+
2107
+ const events: A2AStreamEvent[] = [];
2108
+ for await (const event of client.streamMessage(userMessage)) {
2109
+ events.push(event);
2110
+ }
2111
+
2112
+ const evt = events[0] as Extract<A2AStreamEvent, { type: "message" }>;
2113
+ expect(evt.message.parts).toEqual([
2114
+ { text: "hello" },
2115
+ { data: { answer: 42 } },
2116
+ ]);
2117
+ });
2118
+
2119
+ it("flattens nested file parts onto the internal part shape", async () => {
2120
+ fetchMock.mockResolvedValue(
2121
+ mockSSEResponse([
2122
+ rpc({
2123
+ kind: "message",
2124
+ messageId: "m1",
2125
+ role: "agent",
2126
+ parts: [
2127
+ {
2128
+ kind: "file",
2129
+ file: {
2130
+ uri: "https://files.test/y.png",
2131
+ mimeType: "image/png",
2132
+ name: "y.png",
2133
+ },
2134
+ },
2135
+ ],
2136
+ }),
2137
+ "",
2138
+ "",
2139
+ ]),
2140
+ );
2141
+
2142
+ const events: A2AStreamEvent[] = [];
2143
+ for await (const event of client.streamMessage(userMessage)) {
2144
+ events.push(event);
2145
+ }
2146
+
2147
+ const evt = events[0] as Extract<A2AStreamEvent, { type: "message" }>;
2148
+ expect(evt.message.parts[0]).toEqual({
2149
+ url: "https://files.test/y.png",
2150
+ mediaType: "image/png",
2151
+ filename: "y.png",
2152
+ });
2153
+ });
2154
+
2155
+ it("surfaces JSON-RPC error frames instead of an empty stream", async () => {
2156
+ fetchMock.mockResolvedValue(
2157
+ mockSSEResponse([
2158
+ `data: ${JSON.stringify({
2159
+ jsonrpc: "2.0",
2160
+ id: 1,
2161
+ error: { code: -32001, message: "Task not found" },
2162
+ })}`,
2163
+ "",
2164
+ "",
2165
+ ]),
2166
+ );
2167
+
2168
+ const read = async () => {
2169
+ for await (const _event of client.streamMessage(userMessage)) {
2170
+ // consume
2171
+ }
2172
+ };
2173
+
2174
+ await expect(read()).rejects.toThrow("Task not found");
2175
+ });
2176
+
2177
+ it("unwraps JSON-RPC envelopes on non-streaming responses", async () => {
2178
+ fetchMock.mockResolvedValue({
2179
+ ok: true,
2180
+ json: async () => ({
2181
+ jsonrpc: "2.0",
2182
+ id: 1,
2183
+ result: {
2184
+ kind: "task",
2185
+ id: "t1",
2186
+ contextId: "ctx-1",
2187
+ status: { state: "working" },
2188
+ },
2189
+ }),
2190
+ });
2191
+
2192
+ const task = await client.getTask("t1");
2193
+ expect(task.id).toBe("t1");
2194
+ expect(task.status.state).toBe("working");
2195
+ expect(task).not.toHaveProperty("kind");
2196
+ });
2197
+
2198
+ it("surfaces JSON-RPC error responses on non-streaming requests", async () => {
2199
+ fetchMock.mockResolvedValue({
2200
+ ok: true,
2201
+ json: async () => ({
2202
+ jsonrpc: "2.0",
2203
+ id: 1,
2204
+ error: {
2205
+ code: -32001,
2206
+ message: "Task not found",
2207
+ data: { taskId: "t1" },
2208
+ },
2209
+ }),
2210
+ });
2211
+
2212
+ const error = await client.getTask("t1").catch((e: unknown) => e);
2213
+ expect(error).toBeInstanceOf(A2AError);
2214
+ const a2aError = error as A2AError;
2215
+ expect(a2aError.message).toBe("Task not found");
2216
+ expect(a2aError.code).toBe(-32001);
2217
+ expect(a2aError.status).toBe("JSONRPC_ERROR");
2218
+ expect(a2aError.details).toEqual([{ taskId: "t1" }]);
2219
+ });
2220
+
2221
+ it("skips malformed kind-discriminated frames instead of emitting empty events", async () => {
2222
+ fetchMock.mockResolvedValue(
2223
+ mockSSEResponse([
2224
+ rpc({ kind: "message" }),
2225
+ "",
2226
+ rpc({ kind: "status-update", status: { state: "working" } }),
2227
+ "",
2228
+ rpc({ kind: "artifact-update", taskId: "t1", artifact: {} }),
2229
+ "",
2230
+ rpc({
2231
+ kind: "status-update",
2232
+ taskId: "t1",
2233
+ contextId: "ctx-1",
2234
+ status: { state: "working" },
2235
+ }),
2236
+ "",
2237
+ "",
2238
+ ]),
2239
+ );
2240
+
2241
+ const events: A2AStreamEvent[] = [];
2242
+ for await (const event of client.streamMessage(userMessage)) {
2243
+ events.push(event);
2244
+ }
2245
+
2246
+ expect(events).toHaveLength(1);
2247
+ expect(events[0]!.type).toBe("statusUpdate");
2248
+ });
2249
+
2250
+ it("skips kind-discriminated frames whose ids are not strings", async () => {
2251
+ fetchMock.mockResolvedValue(
2252
+ mockSSEResponse([
2253
+ rpc({
2254
+ kind: "status-update",
2255
+ taskId: "t1",
2256
+ contextId: 999,
2257
+ status: { state: "working" },
2258
+ }),
2259
+ "",
2260
+ rpc({
2261
+ kind: "artifact-update",
2262
+ taskId: "t1",
2263
+ contextId: 999,
2264
+ artifact: {
2265
+ artifactId: "a1",
2266
+ parts: [{ kind: "text", text: "x" }],
2267
+ },
2268
+ }),
2269
+ "",
2270
+ rpc({
2271
+ kind: "task",
2272
+ id: "t1",
2273
+ contextId: 999,
2274
+ status: { state: "working" },
2275
+ }),
2276
+ "",
2277
+ rpc({
2278
+ kind: "message",
2279
+ messageId: "m1",
2280
+ contextId: 999,
2281
+ role: "agent",
2282
+ parts: [{ kind: "text", text: "x" }],
2283
+ }),
2284
+ "",
2285
+ rpc({
2286
+ kind: "message",
2287
+ messageId: "m2",
2288
+ taskId: 999,
2289
+ role: "agent",
2290
+ parts: [{ kind: "text", text: "x" }],
2291
+ }),
2292
+ "",
2293
+ rpc({
2294
+ kind: "artifact-update",
2295
+ taskId: 999,
2296
+ artifact: {
2297
+ artifactId: "a1",
2298
+ parts: [{ kind: "text", text: "x" }],
2299
+ },
2300
+ }),
2301
+ "",
2302
+ rpc({
2303
+ kind: "status-update",
2304
+ taskId: "t1",
2305
+ contextId: "ctx-1",
2306
+ status: { state: "working" },
2307
+ }),
2308
+ "",
2309
+ "",
2310
+ ]),
2311
+ );
2312
+
2313
+ const events: A2AStreamEvent[] = [];
2314
+ for await (const event of client.streamMessage(userMessage)) {
2315
+ events.push(event);
2316
+ }
2317
+
2318
+ expect(events).toHaveLength(1);
2319
+ expect(events[0]!.type).toBe("statusUpdate");
2320
+ });
2321
+
2322
+ it("accepts kind-discriminated frames that omit contextId", async () => {
2323
+ fetchMock.mockResolvedValue(
2324
+ mockSSEResponse([
2325
+ rpc({
2326
+ kind: "status-update",
2327
+ taskId: "t1",
2328
+ status: { state: "working" },
2329
+ }),
2330
+ "",
2331
+ rpc({
2332
+ kind: "artifact-update",
2333
+ taskId: "t1",
2334
+ artifact: {
2335
+ artifactId: "a1",
2336
+ parts: [{ kind: "text", text: "x" }],
2337
+ },
2338
+ }),
2339
+ "",
2340
+ rpc({ kind: "task", id: "t1", status: { state: "working" } }),
2341
+ "",
2342
+ rpc({
2343
+ kind: "message",
2344
+ messageId: "m1",
2345
+ role: "agent",
2346
+ parts: [{ kind: "text", text: "x" }],
2347
+ }),
2348
+ "",
2349
+ "",
2350
+ ]),
2351
+ );
2352
+
2353
+ const events: A2AStreamEvent[] = [];
2354
+ for await (const event of client.streamMessage(userMessage)) {
2355
+ events.push(event);
2356
+ }
2357
+
2358
+ expect(events.map((event) => event.type)).toEqual([
2359
+ "statusUpdate",
2360
+ "artifactUpdate",
2361
+ "task",
2362
+ "message",
2363
+ ]);
2364
+ });
2365
+
2366
+ it("parses flat message and artifact-update results", async () => {
2367
+ fetchMock.mockResolvedValue(
2368
+ mockSSEResponse([
2369
+ rpc({
2370
+ kind: "message",
2371
+ messageId: "m1",
2372
+ role: "agent",
2373
+ parts: [{ kind: "text", text: "hello" }],
2374
+ }),
2375
+ "",
2376
+ rpc({
2377
+ kind: "artifact-update",
2378
+ taskId: "t1",
2379
+ contextId: "ctx-1",
2380
+ artifact: {
2381
+ artifactId: "a1",
2382
+ parts: [{ kind: "text", text: "x" }],
2383
+ },
2384
+ }),
2385
+ "",
2386
+ "",
2387
+ ]),
2388
+ );
2389
+
2390
+ const events: A2AStreamEvent[] = [];
2391
+ for await (const event of client.streamMessage(userMessage)) {
2392
+ events.push(event);
2393
+ }
2394
+
2395
+ expect(events.map((e) => e.type)).toEqual(["message", "artifactUpdate"]);
2396
+ });
2397
+ });
1734
2398
  });