1import type { OrchestrationV2ThreadProjection } from "@t3tools/contracts";
2import { describe, expect, it } from "vite-plus/test";
3
4import { threadAllowsProviderSwitch } from "./thread-provider-switching";
5
6const startedThread = {
7 latestRun: { id: "run-1" },
8 latestUserMessageAt: "2026-06-01T00:00:00.000Z",
9 runtime: { status: "idle" },
10} as never;
11
12const unstartedThread = {
13 latestRun: null,
14 latestUserMessageAt: null,
15 runtime: null,
16} as never;
17
18function projectionWithSession(supportsProviderSwitchingViaHandoff: boolean) {
19 return {
20 thread: { id: "thread", activeProviderThreadId: "provider-thread" },
21 runs: [],
22 providerThreads: [
23 { id: "provider-thread", appThreadId: "thread", providerSessionId: "provider-session" },
24 ],
25 providerSessions: [
26 {
27 id: "provider-session",
28 status: "running",
29 capabilities: { sessions: { supportsProviderSwitchingViaHandoff } },
30 },
31 ],
32 } as unknown as OrchestrationV2ThreadProjection;
33}
34
35describe("threadAllowsProviderSwitch", () => {
36 it("offers every provider when the session can hand the conversation off", () => {
37 expect(
38 threadAllowsProviderSwitch({
39 thread: startedThread,
40 projection: projectionWithSession(true),
41 }),
42 ).toBe(true);
43 });
44
45 it("keeps a started thread on its provider when the session cannot hand off", () => {
46 expect(
47 threadAllowsProviderSwitch({
48 thread: startedThread,
49 projection: projectionWithSession(false),
50 }),
51 ).toBe(false);
52 });
53
54 it("leaves a thread that never ran a turn unbound", () => {
55 expect(threadAllowsProviderSwitch({ thread: unstartedThread, projection: null })).toBe(true);
56 });
57
58 it("allows an imported thread to hand off before opening a provider session", () => {
59 expect(
60 threadAllowsProviderSwitch({
61 thread: startedThread,
62 projection: {
63 thread: { id: "thread", activeProviderThreadId: null, historyOrigin: "v1_import" },
64 runs: [],
65 providerThreads: [],
66 providerSessions: [],
67 } as unknown as OrchestrationV2ThreadProjection,
68 }),
69 ).toBe(true);
70 });
71
72 it("keeps a preparing turn bound before its provider session appears", () => {
73 expect(
74 threadAllowsProviderSwitch({
75 thread: startedThread,
76 projection: {
77 thread: { id: "thread", activeProviderThreadId: null, historyOrigin: "v1_import" },
78 runs: [{ status: "preparing", providerThreadId: "provider-thread" }],
79 providerThreads: [],
80 providerSessions: [],
81 } as unknown as OrchestrationV2ThreadProjection,
82 }),
83 ).toBe(false);
84 });
85
86 it("keeps a started thread bound until its projection resolves a session", () => {
87 expect(threadAllowsProviderSwitch({ thread: startedThread, projection: null })).toBe(false);
88 });
89});