Skip to content
Source
Sign inSign up

contextWindow.test.ts

264 lines8.9 KB

1import {
2 DEFAULT_SERVER_SETTINGS,
3 ProviderDriverKind,
4 ProviderInstanceId,
5 ProviderThreadId,
6 RunId,
7 type ServerProvider,
8 type ServerSettings,
9} from "@t3tools/contracts";
10import {
11 CLAUDE_RESUME_COMPACTION_NEVER_ANSWER,
12 formatClaudeResumeCompactionQuestion,
13} from "@t3tools/shared/claudeCompaction";
14import * as DateTime from "effect/DateTime";
15import { describe, expect, it } from "vite-plus/test";
16​
17import { compactBeforeSendTokens } from "./contextWindow.ts";
18import type { ThreadStructure } from "./models.ts";
19import { v2Projection } from "./orchestrationV2TestFixtures.ts";
20​
21const claude = ProviderInstanceId.make("claudeAgent");
22const claudeWork = ProviderInstanceId.make("claude_work");
23const claudeOtherHome = ProviderInstanceId.make("claude_other_home");
24const codex = ProviderInstanceId.make("codex");
25const lastTurnAt = "2026-10-30T14:23:02.000Z";
26const returnedAt = "2026-10-30T23:03:21.000Z";
27​
28function sendInput(
29 overrides: {
30 readonly usedTokens?: number;
31 readonly promptCacheTtlMs?: number;
32 readonly now?: string;
33 readonly settings?: Partial<ServerSettings>;
34 readonly source?: ProviderInstanceId;
35 readonly selected?: ProviderInstanceId;
36 readonly onlyEarlierConversationUsage?: boolean;
37 readonly unstartedFork?: boolean;
38 readonly slashCommands?: ReadonlyArray<string>;
39 readonly dismissed?: boolean;
40 readonly heldQueue?: boolean;
41 readonly pendingRequest?: boolean;
42 readonly onlyCompactMessages?: boolean;
43 readonly promptOutsideWindow?: boolean;
44 readonly text?: string;
45 readonly threadIdle?: boolean;
46 } = {},
47) {
48 const source = overrides.source ?? claude;
49 const sourceSelection = {
50 instanceId: source,
51 model: source === codex ? "gpt-5.4" : "claude-opus-4-6",
52 };
53 const providers = [
54 { instanceId: claude, driver: "claudeAgent", groupKey: "claude:~/.claude" },
55 { instanceId: claudeWork, driver: "claudeAgent", groupKey: "claude:~/.claude" },
56 { instanceId: claudeOtherHome, driver: "claudeAgent", groupKey: "claude:~/.claude-other" },
57 { instanceId: codex, driver: "codex", groupKey: "codex:~/.codex" },
58 ].map(({ instanceId, driver, groupKey }) => ({
59 instanceId,
60 driver: ProviderDriverKind.make(driver),
61 continuation: { groupKey },
62 slashCommands: (overrides.slashCommands ?? ["compact"]).map((name) => ({ name })),
63 })) as unknown as ReadonlyArray<ServerProvider>;
64 const providerThread = (id: string) => ({
65 id: ProviderThreadId.make(id),
66 driver: providers.find((provider) => provider.instanceId === source)!.driver,
67 providerInstanceId: source,
68 contextUsage: null,
69 updatedAt: DateTime.makeUnsafe(lastTurnAt),
70 });
71 const visibleTurnItems =
72 overrides.promptOutsideWindow === true
73 ? [{ item: { type: "assistant_message", text: "Build fixed" } }]
74 : [
75 {
76 item: {
77 type: "user_message",
78 runId: RunId.make("run-1"),
79 text: overrides.onlyCompactMessages === true ? "/compact" : "Fix the build",
80 attachments: [],
81 },
82 },
83 ];
84 const projection = {
85 ...v2Projection,
86 thread: {
87 ...v2Projection.thread,
88 providerInstanceId: source,
89 modelSelection: sourceSelection,
90 activeProviderThreadId: ProviderThreadId.make("provider-thread-current"),
91 },
92 providerThreads: [
93 providerThread("provider-thread-earlier"),
94 providerThread("provider-thread-current"),
95 ],
96 providerTurns: [
97 {
98 providerThreadId: ProviderThreadId.make(
99 overrides.onlyEarlierConversationUsage === true
100 ? "provider-thread-earlier"
101 : "provider-thread-current",
102 ),
103 tokenUsage: {
104 usedTokens: overrides.usedTokens ?? 293_611,
105 updatedAt: lastTurnAt,
106 ...(overrides.promptCacheTtlMs === undefined
107 ? {}
108 : { promptCacheTtlMs: overrides.promptCacheTtlMs }),
109 },
110 },
111 ],
112 runs: [
113 {
114 id: RunId.make("run-1"),
115 ordinal: 1,
116 providerInstanceId: source,
117 modelSelection: sourceSelection,
118 status: "completed",
119 startedAt: "2026-10-30T14:20:00.000Z",
120 contextHandoffId: null,
121 },
122 ...(overrides.heldQueue === true
123 ? [{ id: RunId.make("queued-run"), status: "queued", queueHeld: true, startedAt: null }]
124 : []),
125 ],
126 runtimeRequests: [
127 ...(overrides.dismissed === true
128 ? [
129 {
130 kind: "user_input",
131 status: "resolved",
132 answers: {
133 [formatClaudeResumeCompactionQuestion({
134 ageMinutes: 90,
135 estimatedTokens: 250_000,
136 })]: CLAUDE_RESUME_COMPACTION_NEVER_ANSWER,
137 },
138 },
139 ]
140 : []),
141 ...(overrides.pendingRequest === true ? [{ kind: "approval", status: "pending" }] : []),
142 ],
143 visibleTurnItems,
144 } as unknown as ThreadStructure;
145 if (overrides.unstartedFork === true) {
146 Object.assign(projection, {
147 thread: {
148 ...projection.thread,
149 activeProviderThreadId: null,
150 forkedFrom: { type: "run", threadId: "source-thread", runId: "source-run" },
151 },
152 runs: [],
153 providerThreads: [],
154 providerTurns: [],
155 contextTransfers: [
156 {
157 type: "fork",
158 targetThreadId: projection.thread.id,
159 sourceThreadId: "source-thread",
160 sourcePoint: { runId: "source-run" },
161 sourceProviderInstanceId: source,
162 status: "pending",
163 sourceTokenUsage: { usedTokens: 293_611, updatedAt: lastTurnAt },
164 },
165 ],
166 });
167 }
168 const selected = overrides.selected ?? claude;
169 return {
170 projection,
171 visibleTurnItems: visibleTurnItems as never,
172 selection: {
173 instanceId: selected,
174 model: selected === codex ? "gpt-5.4" : "claude-opus-4-6",
175 },
176 providers,
177 settings: { ...DEFAULT_SERVER_SETTINGS, ...overrides.settings },
178 text: overrides.text ?? "back again",
179 threadIdle: overrides.threadIdle ?? true,
180 now: overrides.now ?? returnedAt,
181 };
183​
184describe("compactBeforeSendTokens", () => {
185 it.each([
186 { name: "a stale Claude thread over 200K", expected: true },
187 {
188 name: "when the original prompt is outside the loaded window",
189 promptOutsideWindow: true,
190 expected: true,
191 },
192 {
193 name: "when the environment compacts before the cache expires",
194 settings: { idleCompaction: "beforeCacheExpires" },
195 expected: true,
196 },
197 {
198 name: "past a reported TTL shorter than the fallback",
199 promptCacheTtlMs: 5 * 60_000,
200 now: "2026-10-30T14:30:00.000Z",
201 expected: true,
202 },
203 { name: "within the fallback TTL", now: "2026-10-30T15:23:02.000Z", expected: false },
204 { name: "under 200K", usedTokens: 150_000, expected: false },
205 { name: "with idle compaction off", settings: { idleCompaction: "off" }, expected: false },
206 {
207 name: "with the Claude instance overriding idle compaction to off",
208 settings: {
209 idleCompaction: "onReturn",
210 providerInstances: {
211 [claude]: {
212 driver: ProviderDriverKind.make("claudeAgent"),
213 config: { idleCompaction: "off" },
214 },
215 },
216 },
217 expected: false,
218 },
219 { name: "on a Codex thread", source: codex, selected: codex, expected: false },
220 {
221 name: "after switching a stale Codex thread to Claude",
222 source: codex,
223 selected: claude,
224 expected: false,
225 },
226 {
227 name: "after switching to a Claude instance that continues the conversation",
228 selected: claudeWork,
229 expected: true,
230 },
231 {
232 name: "after switching to a Claude instance with another home",
233 selected: claudeOtherHome,
234 expected: false,
235 },
236 {
237 name: "when only an earlier conversation reported usage",
238 onlyEarlierConversationUsage: true,
239 expected: false,
240 },
241 { name: "an unstarted fork resuming its source instance", unstartedFork: true, expected: true },
242 {
243 name: "an unstarted fork selecting another Claude home",
244 unstartedFork: true,
245 selected: claudeOtherHome,
246 expected: false,
247 },
248 { name: "for a provider without /compact", slashCommands: [], expected: false },
249 {
250 name: "after Claude's resume dialog was answered Don't ask again",
251 dismissed: true,
252 expected: false,
253 },
254 { name: "while the queue is held", heldQueue: true, expected: false },
255 { name: "while a request waits for an answer", pendingRequest: true, expected: false },
256 { name: "while the thread is working", threadIdle: false, expected: false },
257 { name: "with no conversation but /compact", onlyCompactMessages: true, expected: false },
258 { name: "for a typed /compact", text: "/compact keep the plan", expected: false },
259 ] as const)("$name", ({ name: _name, expected, ...overrides }) => {
260 expect(compactBeforeSendTokens(sendInput(overrides as Parameters<typeof sendInput>[0]))).toBe(
261 expected ? 293_611 : null,
262 );
263 });
264});