173 lines
5.7 KiB
TypeScript
173 lines
5.7 KiB
TypeScript
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<CreatedSessionResponseBody> => {
|
|
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<typeof fakeChat>("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<typeof fakeChat>("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<typeof fakeChat>("fake-chat");
|
|
|
|
// The clientData field is typed off the agent's schema.
|
|
expectTypeOf<Parameters<typeof start>[0]["clientData"]>().toEqualTypeOf<
|
|
{ userId: string; plan: "free" | "pro" } | undefined
|
|
>();
|
|
// The agent's typed clientData is strictly narrower than `unknown`.
|
|
expectTypeOf<Parameters<typeof start>[0]["clientData"]>().not.toEqualTypeOf<unknown>();
|
|
});
|
|
|
|
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<Parameters<typeof start>[0]["clientData"]>().toEqualTypeOf<unknown>();
|
|
});
|
|
});
|