import { afterEach, describe, expect, expectTypeOf, it } from "vitest"; import { z } from "zod"; import type { CreateSessionRequestBody, CreatedSessionResponseBody } from "@trigger.dev/core/v3"; import { chat } from "./ai.js"; import { __setSessionStartImplForTests, __setSessionOpenImplForTests, SessionHandle, } from "./sessions.js"; import { apiClientManager } from "@trigger.dev/core/v3"; // `auth.createPublicToken` is called by the action when no start token is // supplied. Provide a minimal API client config so the mint path doesn't // throw before we get to assert the captured request body. apiClientManager.setGlobalAPIClientConfiguration({ baseURL: "https://example.invalid", accessToken: "tr_test_secret", }); // Capture the request body the action would send to `sessions.start()`. let lastStartBody: CreateSessionRequestBody | undefined; function installStartFixture() { __setSessionStartImplForTests(async (body): Promise => { lastStartBody = body; return { id: "session_fixture", externalId: body.externalId ?? null, type: body.type, taskIdentifier: body.taskIdentifier, triggerConfig: body.triggerConfig, currentRunId: "run_fixture", tags: body.triggerConfig.tags ?? [], metadata: body.metadata ?? null, closedAt: null, closedReason: null, expiresAt: null, createdAt: new Date(), updatedAt: new Date(), runId: "run_fixture", publicAccessToken: "tr_pat_fixture", isCached: false, }; }); __setSessionOpenImplForTests(() => new SessionHandle("session_fixture")); } afterEach(() => { __setSessionStartImplForTests(undefined); __setSessionOpenImplForTests(undefined); lastStartBody = undefined; }); // Build a fake chat agent task shape that the generic can narrow against. // We only need the static type — the runtime never invokes this task because // `__setSessionStartImplForTests` intercepts the network call. const fakeChat = chat .withClientData({ schema: z.object({ userId: z.string(), plan: z.enum(["free", "pro"]), }), }) .agent({ id: "fake-chat", run: async () => undefined as any, }); describe("chat.createStartSessionAction — runtime", () => { it("folds typed clientData into basePayload.metadata so onChatStart sees it on the first turn", async () => { installStartFixture(); const start = chat.createStartSessionAction("fake-chat"); const result = await start({ chatId: "chat-1", clientData: { userId: "u-1", plan: "pro" }, }); expect(result.publicAccessToken).toBe("tr_pat_fixture"); expect(lastStartBody?.triggerConfig.basePayload).toMatchObject({ messages: [], trigger: "preload", metadata: { userId: "u-1", plan: "pro" }, chatId: "chat-1", }); }); it("leaves basePayload.metadata unset when clientData is not provided", async () => { installStartFixture(); const start = chat.createStartSessionAction("fake-chat"); await start({ chatId: "chat-2" }); expect(lastStartBody?.triggerConfig.basePayload).not.toHaveProperty("metadata"); }); it("prepends chat:{chatId} to triggerConfig.tags and caps at 5", async () => { installStartFixture(); const start = chat.createStartSessionAction("fake-chat", { triggerConfig: { tags: ["org:acme", "a", "b", "c", "d", "e"], }, }); await start({ chatId: "chat-tags" }); expect(lastStartBody?.triggerConfig.tags).toEqual([ "chat:chat-tags", "org:acme", "a", "b", "c", ]); }); it("forwards maxDuration, region, and lockToVersion from triggerConfig", async () => { installStartFixture(); const start = chat.createStartSessionAction("fake-chat", { triggerConfig: { maxDuration: 120, region: "us-east-1", lockToVersion: "20260101.1", }, }); await start({ chatId: "chat-parity" }); expect(lastStartBody?.triggerConfig.maxDuration).toBe(120); expect(lastStartBody?.triggerConfig.region).toBe("us-east-1"); expect(lastStartBody?.triggerConfig.lockToVersion).toBe("20260101.1"); }); it("keeps session-level metadata distinct from per-turn clientData", async () => { installStartFixture(); const start = chat.createStartSessionAction("fake-chat"); await start({ chatId: "chat-3", clientData: { userId: "u-3", plan: "free" }, metadata: { source: "marketing-site" }, }); // Per-turn shape (visible to onPreload / onChatStart): expect(lastStartBody?.triggerConfig.basePayload).toMatchObject({ metadata: { userId: "u-3", plan: "free" }, }); // Session-row metadata (opaque, never typed via clientDataSchema): expect(lastStartBody?.metadata).toEqual({ source: "marketing-site" }); }); }); describe("chat.createStartSessionAction — types", () => { it("narrows clientData against the chat agent's clientDataSchema", () => { const start = chat.createStartSessionAction("fake-chat"); // The clientData field is typed off the agent's schema. expectTypeOf[0]["clientData"]>().toEqualTypeOf< { userId: string; plan: "free" | "pro" } | undefined >(); // The agent's typed clientData is strictly narrower than `unknown`. expectTypeOf[0]["clientData"]>().not.toEqualTypeOf(); }); it("defaults clientData to unknown when called without a generic", () => { const start = chat.createStartSessionAction("fake-chat"); expectTypeOf(start).parameter(0).toHaveProperty("clientData"); // Untyped variant — clientData is `unknown`. expectTypeOf[0]["clientData"]>().toEqualTypeOf(); }); });