Skip to content
Source
Sign inSign up

ChatView.handoffCompaction.browser.test.tsx

125 lines4.8 KB

1import { createMemoryHistory } from "@tanstack/react-router";
2import {
3 ProviderDriverKind,
4 ProviderInstanceId,
5 ProviderThreadId,
6 type OrchestrationV2ProviderThread,
7} from "@t3tools/contracts";
8import { DEFAULT_CLIENT_SETTINGS } from "@t3tools/contracts/settings";
9import type * as Context from "effect/Context";
10import * as DateTime from "effect/DateTime";
11import { expect, it, vi } from "vite-plus/test";
12import { page, userEvent } from "vite-plus/test/browser";
13​
14import { AppRoot } from "~/AppRoot";
15// The route loads ChatView lazily, and its first transform (React Compiler over ~4k lines)
16// takes 6-11s; importing it here keeps that out of the test's timeout.
17import "~/components/ChatView";
18import { __setClientSettingsForTests } from "~/hooks/useSettings";
19import { getRouter } from "~/router";
20import {
21 CLAUDE_PROVIDER,
22 CODEX_PROVIDER,
23 ENVIRONMENT_ID,
24 dispatchedCommands,
25 installFakeEnvironment,
26 threadWithHistory,
27 type FakeDriver,
28} from "../../test/fakeEnvironment";
29import { render } from "../../test/render";
30​
31// The socket is the only fake layer; see test/appScenarios.renders.tsx.
32vi.mock("../../../../packages/client-runtime/src/connection/driver.ts", async (importOriginal) => {
33 const [actual, { fakeDriver }, Layer] = await Promise.all([
34 importOriginal<{ readonly ConnectionDriver: Context.Key<unknown, FakeDriver> }>(),
35 import("../../test/fakeEnvironment"),
36 import("effect/Layer"),
37 ]);
38 return { ...actual, layer: Layer.succeed(actual.ConnectionDriver, fakeDriver) };
39});
40​
41vi.mock(import("~/rpc/requestLatencyState"), async (original) => ({
42 ...(await original()),
43 trackRpcRequestSent: () => {},
44}));
45​
46const CLAUDE = ProviderInstanceId.make("claudeAgent");
47const CLAUDE_DRIVER = ProviderDriverKind.make("claudeAgent");
48​
49/** A Claude thread whose 250k-token context went cold long ago, with its native thread kept for handoff. */
50function coldClaudeThread() {
51 const thread = threadWithHistory("thread-claude", 3, {
52 instanceId: CLAUDE,
53 model: "claude-opus-4-6",
54 });
55 const at = DateTime.makeUnsafe("2026-03-17T19:12:28.000Z");
56 const providerThread: OrchestrationV2ProviderThread = {
57 id: ProviderThreadId.make("provider-thread-claude"),
58 driver: CLAUDE_DRIVER,
59 providerInstanceId: CLAUDE,
60 providerSessionId: null,
61 appThreadId: thread.id,
62 ownerNodeId: null,
63 nativeThreadRef: { driver: CLAUDE_DRIVER, nativeId: "native-claude", strength: "strong" },
64 nativeConversationHeadRef: null,
65 status: "idle",
66 firstRunOrdinal: 1,
67 lastRunOrdinal: 3,
68 handoffIds: [],
69 forkedFrom: null,
70 pendingBackgroundTasks: [],
71 contextUsage: { usedTokens: 250_000, maxTokens: 1_000_000 },
72 nativeMetadata: null,
73 goal: null,
74 createdAt: at,
75 updatedAt: at,
76 };
77 return { ...thread, providerThreads: [providerThread] };
79​
80it("sends a Codex follow-up on a cold Claude thread without compacting first", async () => {
81 const thread = coldClaudeThread();
82 using _environment = installFakeEnvironment([thread], {
83 acceptCommands: true,
84 providers: [CLAUDE_PROVIDER, CODEX_PROVIDER],
85 });
86 __setClientSettingsForTests({
87 ...DEFAULT_CLIENT_SETTINGS,
88 onboardingCompletedAt: "2026-01-01T00:00:00.000Z",
89 });
90 const router = getRouter(
91 createMemoryHistory({ initialEntries: [`/${ENVIRONMENT_ID}/${thread.id}`] }),
92 );
93 await router.load();
94 await using _app = await render(<AppRoot router={router} />, {
95 // AppRoot brings its own router.
96 router: false,
97 });
98 await expect.element(page.getByText(`Answer 2 for ${thread.id}.`)).toBeVisible();
99 const compactAndSend = page.getByRole("button", { name: "Compact and send" });
100 await expect.element(compactAndSend).toBeVisible();
101​
102 await page.getByRole("button", { name: "Claude Opus 4.6", exact: true }).click();
103 // The picker lists only the active provider's models until you search.
104 await userEvent.type(page.getByPlaceholder("Search models..."), "GPT-5.4");
105 await page.getByRole("option", { name: "GPT-5.4", exact: false }).click();
106 await expect.element(page.getByRole("button", { name: "GPT-5.4", exact: true })).toBeVisible();
107 await expect.element(compactAndSend).not.toBeInTheDocument();
108 // Picking a model leaves the composer's settings menu open.
109 await userEvent.keyboard("{Escape}");
110​
111 const editor = page.getByTestId("composer-editor");
112 await userEvent.click(editor);
113 await userEvent.type(editor, "keep going");
114 await userEvent.keyboard("{Enter}");
115​
116 const messages = () =>
117 dispatchedCommands().flatMap((command) =>
118 command.type === "message.dispatch"
119 ? [{ text: command.text, instanceId: command.modelSelection?.instanceId }]
120 : [],
121 );
122 const sent = { text: "keep going", instanceId: CODEX_PROVIDER.instanceId };
123 await vi.waitFor(() => expect(messages()).toContainEqual(sent));
124 expect(messages()).toEqual([sent]);
125});