Files
triggerdotdev--trigger.dev/packages/trigger-sdk/test/replay-session-in.test.ts
T
2026-07-13 13:32:57 +08:00

226 lines
7.2 KiB
TypeScript

// Import the test entry point first so the resource catalog is installed.
import "../src/v3/test/index.js";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { apiClientManager } from "@trigger.dev/core/v3";
import {
__findLatestSessionInCursorForTests as findLatestSessionInCursor,
__replaySessionInTailProductionPathForTests as replaySessionInTail,
} from "../src/v3/ai.js";
// ── Helpers ────────────────────────────────────────────────────────────
function userMessage(id: string, text: string) {
return {
id,
role: "user" as const,
parts: [{ type: "text" as const, text }],
};
}
function stubReadRecords(chunks: unknown[]) {
const records = chunks.map((chunk, i) => ({
data: chunk,
id: `evt-${i + 1}`,
seqNum: i + 1,
}));
const spy = vi.fn(async () => ({ records }));
vi.spyOn(apiClientManager, "clientOrThrow").mockReturnValue({
readSessionStreamRecords: spy,
} as never);
return spy;
}
beforeEach(() => {
vi.restoreAllMocks();
});
afterEach(() => {
vi.restoreAllMocks();
});
// ── Tests ──────────────────────────────────────────────────────────────
describe("replaySessionInTail", () => {
it("extracts user messages from kind: 'message' records with submit-message trigger", async () => {
const u1 = userMessage("u-1", "hello");
const u2 = userMessage("u-2", "again");
stubReadRecords([
{
kind: "message",
payload: {
chatId: "c1",
trigger: "submit-message",
message: u1,
metadata: { userId: "a" },
},
},
{
kind: "message",
payload: {
chatId: "c1",
trigger: "submit-message",
message: u2,
metadata: { userId: "b" },
},
},
]);
const result = await replaySessionInTail("sess");
expect(result).toHaveLength(2);
expect(result[0]!.message.id).toBe("u-1");
expect(result[0]!.seqNum).toBe(1);
expect(result[0]!.metadata).toEqual({ userId: "a" });
expect(result[1]!.message.id).toBe("u-2");
expect(result[1]!.seqNum).toBe(2);
expect(result[1]!.metadata).toEqual({ userId: "b" });
});
it("ignores non-message variants (stop, handover, handover-skip)", async () => {
const u1 = userMessage("u-1", "real user");
stubReadRecords([
{ kind: "stop", message: "user stopped" },
{ kind: "handover-skip" },
{ kind: "handover", partialAssistantMessage: [], isFinal: false },
{ kind: "message", payload: { chatId: "c1", trigger: "submit-message", message: u1 } },
]);
const result = await replaySessionInTail("sess");
expect(result).toHaveLength(1);
expect(result[0]!.message.id).toBe("u-1");
});
it("ignores message records that aren't submit-message", async () => {
// regenerate-message / preload / close / action / handover-prepare don't
// carry a user message — the chain reconstruction must skip them.
stubReadRecords([
{ kind: "message", payload: { chatId: "c1", trigger: "regenerate-message" } },
{ kind: "message", payload: { chatId: "c1", trigger: "preload" } },
{ kind: "message", payload: { chatId: "c1", trigger: "close" } },
{ kind: "message", payload: { chatId: "c1", trigger: "action", action: { foo: 1 } } },
]);
const result = await replaySessionInTail("sess");
expect(result).toHaveLength(0);
});
it("ignores records whose payload is missing or empty", async () => {
stubReadRecords([
{ kind: "message" }, // no payload
{ kind: "message", payload: { chatId: "c1", trigger: "submit-message" } }, // no message
{ kind: "message", payload: { chatId: "c1", trigger: "submit-message", message: null } },
{
kind: "message",
payload: { chatId: "c1", trigger: "submit-message", message: "not-an-object" },
},
]);
const result = await replaySessionInTail("sess");
expect(result).toHaveLength(0);
});
it("skips non-object record data defensively", async () => {
const u1 = userMessage("u-1", "valid");
stubReadRecords([
42,
null,
"string-data",
{ kind: "message", payload: { chatId: "c1", trigger: "submit-message", message: u1 } },
]);
const result = await replaySessionInTail("sess");
expect(result).toHaveLength(1);
expect(result[0]!.message.id).toBe("u-1");
});
it("passes the afterEventId cursor through to readSessionStreamRecords", async () => {
const spy = stubReadRecords([]);
await replaySessionInTail("sess", { lastEventId: "evt-42" });
expect(spy).toHaveBeenCalledWith("sess", "in", { afterEventId: "evt-42" });
});
it("returns an empty list when the records endpoint returns no records", async () => {
stubReadRecords([]);
const result = await replaySessionInTail("sess");
expect(result).toEqual([]);
});
});
// ── Cursor scan (non-blocking records read, not an SSE drain) ──────────
function stubReadRecordsWithHeaders(
records: Array<{ data?: unknown; headers?: Array<[string, string]> }>
) {
const shaped = records.map((r, i) => ({
data: r.data ?? "",
id: `evt-${i + 1}`,
seqNum: i + 1,
headers: r.headers,
}));
const spy = vi.fn(async () => ({ records: shaped }));
vi.spyOn(apiClientManager, "clientOrThrow").mockReturnValue({
readSessionStreamRecords: spy,
} as never);
return spy;
}
describe("findLatestSessionInCursor", () => {
it("returns the LAST turn-complete's session-in-event-id", async () => {
const spy = stubReadRecordsWithHeaders([
{ data: { type: "text-delta", delta: "hi" } },
{
headers: [
["trigger-control", "turn-complete"],
["session-in-event-id", "3"],
],
},
{ data: { type: "text-delta", delta: "again" } },
{
headers: [
["trigger-control", "turn-complete"],
["session-in-event-id", "7"],
],
},
]);
const cursor = await findLatestSessionInCursor("sess");
expect(cursor).toBe(7);
// Non-blocking records read on `.out`, no SSE subscribe.
expect(spy).toHaveBeenCalledWith("sess", "out");
});
it("ignores other control subtypes and turn-completes without the header", async () => {
stubReadRecordsWithHeaders([
{
headers: [
["trigger-control", "upgrade-required"],
["session-in-event-id", "99"],
],
},
{ headers: [["trigger-control", "turn-complete"]] },
{
headers: [
["trigger-control", "turn-complete"],
["session-in-event-id", "4"],
],
},
]);
const cursor = await findLatestSessionInCursor("sess");
expect(cursor).toBe(4);
});
it("returns undefined when records carry no headers (older server)", async () => {
stubReadRecordsWithHeaders([
{ data: { type: "text-delta", delta: "hi" } },
{ data: { type: "finish" } },
]);
const cursor = await findLatestSessionInCursor("sess");
expect(cursor).toBeUndefined();
});
});