Skip to content

Commit dfcf032

Browse files
committed
test(agent): add TanStack AI converter tests
1 parent c07aa13 commit dfcf032

1 file changed

Lines changed: 285 additions & 0 deletions

File tree

Lines changed: 285 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,285 @@
1+
import { describe, it, expect } from "vitest";
2+
import { EventType, type BaseEvent } from "@ag-ui/client";
3+
import {
4+
createAgent,
5+
createDefaultInput,
6+
collectEvents,
7+
expectLifecycleWrapped,
8+
expectEventSequence,
9+
tanstackTextChunk,
10+
tanstackToolCallStart,
11+
tanstackToolCallArgs,
12+
tanstackToolCallEnd,
13+
} from "./agent-test-helpers";
14+
15+
describe("TanStack AI converter (via Agent)", () => {
16+
// -------------------------------------------------------------------------
17+
// Text Events
18+
// -------------------------------------------------------------------------
19+
describe("Text Events", () => {
20+
it("TEXT_MESSAGE_CONTENT chunk produces TEXT_MESSAGE_CHUNK with role assistant and correct delta", async () => {
21+
const agent = createAgent("tanstack", [tanstackTextChunk("Hello world")]);
22+
const events = await collectEvents(agent.run(createDefaultInput()));
23+
24+
expectLifecycleWrapped(events);
25+
26+
const textEvents = events.filter(
27+
(e) => e.type === EventType.TEXT_MESSAGE_CHUNK,
28+
);
29+
expect(textEvents).toHaveLength(1);
30+
31+
const textEvent = textEvents[0] as BaseEvent & {
32+
role: string;
33+
messageId: string;
34+
delta: string;
35+
};
36+
expect(textEvent.role).toBe("assistant");
37+
expect(textEvent.delta).toBe("Hello world");
38+
expect(textEvent.messageId).toBeDefined();
39+
expect(typeof textEvent.messageId).toBe("string");
40+
expect(textEvent.messageId.length).toBeGreaterThan(0);
41+
});
42+
43+
it("multiple text chunks share the same messageId", async () => {
44+
const agent = createAgent("tanstack", [
45+
tanstackTextChunk("Hello "),
46+
tanstackTextChunk("world"),
47+
tanstackTextChunk("!"),
48+
]);
49+
const events = await collectEvents(agent.run(createDefaultInput()));
50+
51+
expectLifecycleWrapped(events);
52+
53+
const textEvents = events.filter(
54+
(e) => e.type === EventType.TEXT_MESSAGE_CHUNK,
55+
) as (BaseEvent & { messageId: string })[];
56+
expect(textEvents).toHaveLength(3);
57+
58+
const messageIds = new Set(textEvents.map((e) => e.messageId));
59+
expect(messageIds.size).toBe(1);
60+
});
61+
62+
it("empty stream produces only RUN_STARTED + RUN_FINISHED", async () => {
63+
const agent = createAgent("tanstack", []);
64+
const events = await collectEvents(agent.run(createDefaultInput()));
65+
66+
expectEventSequence(events, [
67+
EventType.RUN_STARTED,
68+
EventType.RUN_FINISHED,
69+
]);
70+
});
71+
});
72+
73+
// -------------------------------------------------------------------------
74+
// Tool Call Events
75+
// -------------------------------------------------------------------------
76+
describe("Tool Call Events", () => {
77+
it("full tool call lifecycle produces START, ARGS, END events in order", async () => {
78+
const agent = createAgent("tanstack", [
79+
tanstackToolCallStart("tc-1", "myTool"),
80+
tanstackToolCallArgs("tc-1", '{"key":'),
81+
tanstackToolCallArgs("tc-1", '"value"}'),
82+
tanstackToolCallEnd("tc-1"),
83+
]);
84+
const events = await collectEvents(agent.run(createDefaultInput()));
85+
86+
expectLifecycleWrapped(events);
87+
expectEventSequence(events, [
88+
EventType.RUN_STARTED,
89+
EventType.TOOL_CALL_START,
90+
EventType.TOOL_CALL_ARGS,
91+
EventType.TOOL_CALL_ARGS,
92+
EventType.TOOL_CALL_END,
93+
EventType.RUN_FINISHED,
94+
]);
95+
96+
const startEvent = events[1] as BaseEvent & {
97+
toolCallId: string;
98+
toolCallName: string;
99+
};
100+
expect(startEvent.toolCallId).toBe("tc-1");
101+
expect(startEvent.toolCallName).toBe("myTool");
102+
103+
const argsEvent1 = events[2] as BaseEvent & {
104+
toolCallId: string;
105+
delta: string;
106+
};
107+
expect(argsEvent1.toolCallId).toBe("tc-1");
108+
expect(argsEvent1.delta).toBe('{"key":');
109+
110+
const argsEvent2 = events[3] as BaseEvent & {
111+
toolCallId: string;
112+
delta: string;
113+
};
114+
expect(argsEvent2.toolCallId).toBe("tc-1");
115+
expect(argsEvent2.delta).toBe('"value"}');
116+
117+
const endEvent = events[4] as BaseEvent & { toolCallId: string };
118+
expect(endEvent.toolCallId).toBe("tc-1");
119+
});
120+
121+
it("TOOL_CALL_START sets parentMessageId", async () => {
122+
const agent = createAgent("tanstack", [
123+
tanstackTextChunk("before"),
124+
tanstackToolCallStart("tc-1", "myTool"),
125+
tanstackToolCallEnd("tc-1"),
126+
]);
127+
const events = await collectEvents(agent.run(createDefaultInput()));
128+
129+
const textEvent = events.find(
130+
(e) => e.type === EventType.TEXT_MESSAGE_CHUNK,
131+
) as BaseEvent & { messageId: string };
132+
const toolStartEvent = events.find(
133+
(e) => e.type === EventType.TOOL_CALL_START,
134+
) as BaseEvent & { parentMessageId: string };
135+
136+
expect(toolStartEvent.parentMessageId).toBeDefined();
137+
expect(toolStartEvent.parentMessageId).toBe(textEvent.messageId);
138+
});
139+
140+
it("multiple tool calls in sequence each get correct events", async () => {
141+
const agent = createAgent("tanstack", [
142+
tanstackToolCallStart("tc-1", "toolA"),
143+
tanstackToolCallArgs("tc-1", '{"a":1}'),
144+
tanstackToolCallEnd("tc-1"),
145+
tanstackToolCallStart("tc-2", "toolB"),
146+
tanstackToolCallArgs("tc-2", '{"b":2}'),
147+
tanstackToolCallEnd("tc-2"),
148+
]);
149+
const events = await collectEvents(agent.run(createDefaultInput()));
150+
151+
expectLifecycleWrapped(events);
152+
expectEventSequence(events, [
153+
EventType.RUN_STARTED,
154+
EventType.TOOL_CALL_START,
155+
EventType.TOOL_CALL_ARGS,
156+
EventType.TOOL_CALL_END,
157+
EventType.TOOL_CALL_START,
158+
EventType.TOOL_CALL_ARGS,
159+
EventType.TOOL_CALL_END,
160+
EventType.RUN_FINISHED,
161+
]);
162+
163+
// Verify first tool call
164+
const start1 = events[1] as BaseEvent & {
165+
toolCallId: string;
166+
toolCallName: string;
167+
};
168+
expect(start1.toolCallId).toBe("tc-1");
169+
expect(start1.toolCallName).toBe("toolA");
170+
171+
const args1 = events[2] as BaseEvent & {
172+
toolCallId: string;
173+
delta: string;
174+
};
175+
expect(args1.toolCallId).toBe("tc-1");
176+
177+
const end1 = events[3] as BaseEvent & { toolCallId: string };
178+
expect(end1.toolCallId).toBe("tc-1");
179+
180+
// Verify second tool call
181+
const start2 = events[4] as BaseEvent & {
182+
toolCallId: string;
183+
toolCallName: string;
184+
};
185+
expect(start2.toolCallId).toBe("tc-2");
186+
expect(start2.toolCallName).toBe("toolB");
187+
188+
const args2 = events[5] as BaseEvent & {
189+
toolCallId: string;
190+
delta: string;
191+
};
192+
expect(args2.toolCallId).toBe("tc-2");
193+
194+
const end2 = events[6] as BaseEvent & { toolCallId: string };
195+
expect(end2.toolCallId).toBe("tc-2");
196+
});
197+
198+
it("tool call with no ARGS chunks produces only START + END", async () => {
199+
const agent = createAgent("tanstack", [
200+
tanstackToolCallStart("tc-1", "noArgsTool"),
201+
tanstackToolCallEnd("tc-1"),
202+
]);
203+
const events = await collectEvents(agent.run(createDefaultInput()));
204+
205+
expectLifecycleWrapped(events);
206+
expectEventSequence(events, [
207+
EventType.RUN_STARTED,
208+
EventType.TOOL_CALL_START,
209+
EventType.TOOL_CALL_END,
210+
EventType.RUN_FINISHED,
211+
]);
212+
});
213+
});
214+
215+
// -------------------------------------------------------------------------
216+
// Mixed Content
217+
// -------------------------------------------------------------------------
218+
describe("Mixed Content", () => {
219+
it("text interleaved with tool calls produces correct event types and order", async () => {
220+
const agent = createAgent("tanstack", [
221+
tanstackTextChunk("Let me help. "),
222+
tanstackToolCallStart("tc-1", "search"),
223+
tanstackToolCallArgs("tc-1", '{"q":"test"}'),
224+
tanstackToolCallEnd("tc-1"),
225+
tanstackTextChunk("Here are the results."),
226+
]);
227+
const events = await collectEvents(agent.run(createDefaultInput()));
228+
229+
expectLifecycleWrapped(events);
230+
expectEventSequence(events, [
231+
EventType.RUN_STARTED,
232+
EventType.TEXT_MESSAGE_CHUNK,
233+
EventType.TOOL_CALL_START,
234+
EventType.TOOL_CALL_ARGS,
235+
EventType.TOOL_CALL_END,
236+
EventType.TEXT_MESSAGE_CHUNK,
237+
EventType.RUN_FINISHED,
238+
]);
239+
240+
// Verify content of text events
241+
const textEvents = events.filter(
242+
(e) => e.type === EventType.TEXT_MESSAGE_CHUNK,
243+
) as (BaseEvent & { delta: string })[];
244+
expect(textEvents[0].delta).toBe("Let me help. ");
245+
expect(textEvents[1].delta).toBe("Here are the results.");
246+
});
247+
});
248+
249+
// -------------------------------------------------------------------------
250+
// Edge Cases
251+
// -------------------------------------------------------------------------
252+
describe("Edge Cases", () => {
253+
it("unknown chunk types are silently ignored", async () => {
254+
const agent = createAgent("tanstack", [
255+
tanstackTextChunk("hello"),
256+
{ type: "SOME_UNKNOWN_TYPE", data: "foo" },
257+
{ type: "ANOTHER_MYSTERY", value: 42 },
258+
tanstackTextChunk(" world"),
259+
]);
260+
const events = await collectEvents(agent.run(createDefaultInput()));
261+
262+
expectLifecycleWrapped(events);
263+
expectEventSequence(events, [
264+
EventType.RUN_STARTED,
265+
EventType.TEXT_MESSAGE_CHUNK,
266+
EventType.TEXT_MESSAGE_CHUNK,
267+
EventType.RUN_FINISHED,
268+
]);
269+
});
270+
271+
it("large deltas (100k chars) are passed through", async () => {
272+
const largeDelta = "x".repeat(100_000);
273+
const agent = createAgent("tanstack", [tanstackTextChunk(largeDelta)]);
274+
const events = await collectEvents(agent.run(createDefaultInput()));
275+
276+
expectLifecycleWrapped(events);
277+
278+
const textEvent = events.find(
279+
(e) => e.type === EventType.TEXT_MESSAGE_CHUNK,
280+
) as BaseEvent & { delta: string };
281+
expect(textEvent.delta).toBe(largeDelta);
282+
expect(textEvent.delta.length).toBe(100_000);
283+
});
284+
});
285+
});

0 commit comments

Comments
 (0)