|
1 | | -// Import the test entry point first so chat.customAgent() registers its task. |
2 | | -import "../src/v3/test/index.js"; |
3 | | - |
4 | | -import { afterEach, describe, expect, it, vi } from "vitest"; |
5 | | -import { apiClientManager, resourceCatalog } from "@trigger.dev/core/v3"; |
6 | | -import { runInMockTaskContext, TestSessionStreamManager } from "@trigger.dev/core/v3/test"; |
| 1 | +import { describe, expect, it } from "vitest"; |
7 | 2 | import { chat } from "../src/v3/ai.js"; |
8 | 3 |
|
9 | | -const CHAT_ID = "chat-end-and-continue"; |
10 | | -const CALLING_RUN_ID = "run_before_handoff"; |
11 | | -const CONTINUATION_RUN_ID = "run_after_handoff"; |
12 | | - |
13 | | -type CustomAgentRun = ( |
14 | | - payload: Record<string, unknown>, |
15 | | - options: { ctx: unknown; signal: AbortSignal } |
16 | | -) => Promise<unknown>; |
17 | | - |
18 | | -function getCustomAgentRun(id: string): CustomAgentRun { |
19 | | - const taskEntry = resourceCatalog.getTask(id); |
20 | | - if (!taskEntry) { |
21 | | - throw new Error(`Task ${id} was not registered`); |
22 | | - } |
23 | | - |
24 | | - return taskEntry.fns.run as CustomAgentRun; |
25 | | -} |
26 | | - |
27 | | -class DurableTestSessionStreamManager extends TestSessionStreamManager { |
28 | | - override reset(): void { |
29 | | - // The Session stream outlives either task run. Drop run-local listeners, |
30 | | - // but preserve buffered input for the continuation run. |
31 | | - this.clearHandlers(); |
32 | | - } |
33 | | - |
34 | | - dispose(): void { |
35 | | - super.reset(); |
36 | | - } |
37 | | -} |
38 | | - |
39 | 4 | describe("chat.endAndContinue", () => { |
40 | | - afterEach(() => { |
41 | | - vi.restoreAllMocks(); |
42 | | - }); |
43 | | - |
44 | | - it("ends cleanly and leaves unconsumed input for the continuation run", async () => { |
45 | | - let continuationMessage: unknown; |
46 | | - |
47 | | - const agent = chat.customAgent({ |
48 | | - id: "end-and-continue-custom-agent", |
49 | | - run: async (payload) => { |
50 | | - if (!payload.continuation) { |
51 | | - return chat.endAndContinue(); |
52 | | - } |
53 | | - |
54 | | - const next = await chat.messages.waitWithIdleTimeout({ |
55 | | - idleTimeoutInSeconds: 1, |
56 | | - timeout: "1m", |
57 | | - }); |
58 | | - if (!next.ok) { |
59 | | - throw next.error; |
60 | | - } |
61 | | - |
62 | | - continuationMessage = next.output.message; |
63 | | - }, |
64 | | - }); |
65 | | - |
66 | | - const runFn = getCustomAgentRun(agent.id); |
67 | | - |
68 | | - const readSessionStreamRecords = vi.fn(async () => ({ records: [] })); |
69 | | - const endAndContinueSession = vi.fn(async () => ({ |
70 | | - runId: CONTINUATION_RUN_ID, |
71 | | - swapped: true, |
72 | | - })); |
73 | | - vi.spyOn(apiClientManager, "clientOrThrow").mockReturnValue({ |
74 | | - readSessionStreamRecords, |
75 | | - endAndContinueSession, |
76 | | - } as never); |
77 | | - |
78 | | - const sessionStreams = new DurableTestSessionStreamManager(); |
79 | | - const pendingPayload = { |
80 | | - chatId: CHAT_ID, |
81 | | - trigger: "submit-message", |
82 | | - message: { |
83 | | - id: "pending-user-message", |
84 | | - role: "user", |
85 | | - parts: [{ type: "text", text: "deliver after handoff" }], |
86 | | - }, |
87 | | - metadata: {}, |
88 | | - }; |
89 | | - |
90 | | - try { |
91 | | - await runInMockTaskContext( |
92 | | - async (drivers) => { |
93 | | - // This record is durable Session input, not run-local input. It is |
94 | | - // written before the old run requests its handoff. |
95 | | - await drivers.sessions.in.send(CHAT_ID, { |
96 | | - kind: "message", |
97 | | - payload: pendingPayload, |
98 | | - }); |
99 | | - |
100 | | - await expect( |
101 | | - runFn( |
102 | | - { chatId: CHAT_ID, trigger: "preload", metadata: {} }, |
103 | | - { ctx: drivers.ctx, signal: new AbortController().signal } |
104 | | - ) |
105 | | - ).resolves.toBeUndefined(); |
106 | | - }, |
107 | | - { |
108 | | - ctx: { run: { id: CALLING_RUN_ID } }, |
109 | | - sessionStreamManager: sessionStreams, |
110 | | - } |
111 | | - ); |
112 | | - |
113 | | - expect(endAndContinueSession).toHaveBeenCalledWith(CHAT_ID, { |
114 | | - callingRunId: CALLING_RUN_ID, |
115 | | - reason: "upgrade", |
116 | | - }); |
117 | | - |
118 | | - await runInMockTaskContext( |
119 | | - async (drivers) => { |
120 | | - await expect( |
121 | | - runFn( |
122 | | - { chatId: CHAT_ID, continuation: true, metadata: {} }, |
123 | | - { ctx: drivers.ctx, signal: new AbortController().signal } |
124 | | - ) |
125 | | - ).resolves.toBeUndefined(); |
126 | | - }, |
127 | | - { |
128 | | - ctx: { run: { id: CONTINUATION_RUN_ID } }, |
129 | | - sessionStreamManager: sessionStreams, |
130 | | - } |
131 | | - ); |
132 | | - |
133 | | - expect(continuationMessage).toEqual(pendingPayload.message); |
134 | | - } finally { |
135 | | - sessionStreams.dispose(); |
136 | | - } |
137 | | - }); |
138 | | - |
139 | | - it("rejects when the server handoff fails", async () => { |
140 | | - const agent = chat.customAgent({ |
141 | | - id: "end-and-continue-failure-agent", |
142 | | - run: async () => { |
143 | | - return chat.endAndContinue(); |
144 | | - }, |
145 | | - }); |
146 | | - |
147 | | - const runFn = getCustomAgentRun(agent.id); |
148 | | - |
149 | | - const readSessionStreamRecords = vi.fn(async () => ({ records: [] })); |
150 | | - const endAndContinueSession = vi.fn(async () => { |
151 | | - throw new Error("handoff failed"); |
152 | | - }); |
153 | | - vi.spyOn(apiClientManager, "clientOrThrow").mockReturnValue({ |
154 | | - readSessionStreamRecords, |
155 | | - endAndContinueSession, |
156 | | - } as never); |
157 | | - |
158 | | - await runInMockTaskContext( |
159 | | - async (drivers) => { |
160 | | - await expect( |
161 | | - runFn( |
162 | | - { chatId: CHAT_ID, trigger: "preload", metadata: {} }, |
163 | | - { ctx: drivers.ctx, signal: new AbortController().signal } |
164 | | - ) |
165 | | - ).rejects.toThrow("handoff failed"); |
166 | | - }, |
167 | | - { ctx: { run: { id: CALLING_RUN_ID } } } |
168 | | - ); |
169 | | - |
170 | | - expect(endAndContinueSession).toHaveBeenCalledWith(CHAT_ID, { |
171 | | - callingRunId: CALLING_RUN_ID, |
172 | | - reason: "upgrade", |
173 | | - }); |
174 | | - }); |
175 | | - |
176 | 5 | it("rejects calls outside a custom agent run", async () => { |
177 | | - const endAndContinueSession = vi.fn(); |
178 | | - vi.spyOn(apiClientManager, "clientOrThrow").mockReturnValue({ |
179 | | - endAndContinueSession, |
180 | | - } as never); |
181 | | - |
182 | | - await runInMockTaskContext(async () => { |
183 | | - await expect(chat.endAndContinue()).rejects.toThrow( |
184 | | - "chat.endAndContinue() can only be called from inside a chat.customAgent() run" |
185 | | - ); |
186 | | - }); |
187 | | - |
188 | | - expect(endAndContinueSession).not.toHaveBeenCalled(); |
| 6 | + await expect(chat.endAndContinue()).rejects.toThrow( |
| 7 | + "chat.endAndContinue() can only be called from inside a chat.customAgent() run" |
| 8 | + ); |
189 | 9 | }); |
190 | 10 | }); |
0 commit comments