Skip to content

Commit ffa693e

Browse files
test: cover new turn after a stop without turn-complete
Co-authored-by: Wei-Wei Wu <wei-wei@momentic.ai>
1 parent e4010e5 commit ffa693e

1 file changed

Lines changed: 64 additions & 0 deletions

File tree

packages/trigger-sdk/test/chat-transport-events.test.ts

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -174,6 +174,70 @@ describe("transport send events", () => {
174174
});
175175
});
176176

177+
describe("stopped turn followed by a new turn", () => {
178+
/**
179+
* `.out` stub that honours the `Last-Event-ID` cursor like the server does, so
180+
* a resubscribe cannot replay records the reader already consumed. A stop that
181+
* never saw its turn-complete is therefore unrecoverable unless the new send
182+
* clears the skip state.
183+
*/
184+
function cursoredOneTurnTransport() {
185+
const frames = [
186+
{ id: "1", data: `{"type":"text-delta","id":"t1","delta":"hello"}` },
187+
{ id: "2", data: `{"type":"trigger:turn-complete"}` },
188+
];
189+
190+
return makeTransport({
191+
fetch: async (_url, init, ctx) => {
192+
if (ctx.endpoint === "in") return jsonOk();
193+
194+
const cursor = new Headers(init.headers).get("Last-Event-ID");
195+
const from = cursor ? frames.findIndex((f) => f.id === cursor) + 1 : 0;
196+
const remaining = frames.slice(from);
197+
const response = sseResponse(
198+
remaining.map((f) => `id: ${f.id}\ndata: ${f.data}\n\n`).join("")
199+
);
200+
// Nothing left to send: the session is settled, so the reader stops
201+
// instead of resubscribing.
202+
if (remaining.length === 0) response.headers.set("X-Session-Settled", "true");
203+
return response;
204+
},
205+
});
206+
}
207+
208+
it("streams a sendMessages turn after a stop that never saw turn-complete", async () => {
209+
const { transport, events } = cursoredOneTurnTransport();
210+
211+
expect(await transport.stopGeneration("c1")).toBe(true);
212+
events.length = 0;
213+
214+
const stream = await transport.sendMessages({
215+
trigger: "submit-message",
216+
chatId: "c1",
217+
messageId: undefined,
218+
messages: [user("after stop", "u-2")],
219+
abortSignal: undefined,
220+
});
221+
const chunks = await readAll(stream);
222+
223+
expect(chunks).toEqual([{ type: "text-delta", id: "t1", delta: "hello" }]);
224+
expect(events.some((e) => e.type === "turn-completed")).toBe(true);
225+
});
226+
227+
it("streams a sendAction turn after a stop that never saw turn-complete", async () => {
228+
const { transport, events } = cursoredOneTurnTransport();
229+
230+
expect(await transport.stopGeneration("c1")).toBe(true);
231+
events.length = 0;
232+
233+
const stream = await transport.sendAction("c1", { type: "undo" });
234+
const chunks = await readAll(stream);
235+
236+
expect(chunks).toEqual([{ type: "text-delta", id: "t1", delta: "hello" }]);
237+
expect(events.some((e) => e.type === "turn-completed")).toBe(true);
238+
});
239+
});
240+
177241
describe("transport stream events", () => {
178242
it("marks reconnectToStream subscriptions as resumed", async () => {
179243
const { transport, events } = makeTransport({

0 commit comments

Comments
 (0)