1import { describe, expect, it } from "vite-plus/test";
2import {
3 ProviderDriverKind,
4 type ProviderOptionDescriptor,
5 type ProviderOptionSelection,
6 type ServerProviderModel,
7} from "@t3tools/contracts";
8import { getProviderOptionDescriptors } from "@t3tools/shared/model";
9import { getProviderModelCapabilities } from "../../providerModels";
10import {
11 getComposerPromptInjectionState,
12 getComposerProviderState,
13 renderProviderTraitsMenuContent,
14 renderProviderTraitsPicker,
15 withImplicitFastModeDefault,
16} from "./composerProviderState";
17
18// Everything in composerProviderState is now data-driven by the model's
19// optionDescriptors, so these tests use a single synthetic provider/model and
20// vary only the descriptor shape per scenario.
21
22const PROVIDER: ProviderDriverKind = ProviderDriverKind.make("codex");
23const MODEL = "test-model";
24
25function selectDescriptor(
26 id: string,
27 options: ReadonlyArray<{ id: string; label: string; isDefault?: boolean }>,
28 promptInjectedValues?: ReadonlyArray<string>,
29): Extract<ProviderOptionDescriptor, { type: "select" }> {
30 const defaultId = options.find((option) => option.isDefault)?.id;
31 return {
32 id,
33 label: id,
34 type: "select",
35 options: [...options],
36 ...(defaultId ? { currentValue: defaultId } : {}),
37 ...(promptInjectedValues && promptInjectedValues.length > 0
38 ? { promptInjectedValues: [...promptInjectedValues] }
39 : {}),
40 };
41}
42
43function booleanDescriptor(
44 id: string,
45 currentValue?: boolean,
46): Extract<ProviderOptionDescriptor, { type: "boolean" }> {
47 return {
48 id,
49 label: id,
50 type: "boolean",
51 ...(typeof currentValue === "boolean" ? { currentValue } : {}),
52 };
53}
54
55function modelWith(
56 descriptors: ReadonlyArray<ProviderOptionDescriptor>,
57): ReadonlyArray<ServerProviderModel> {
58 return [
59 { slug: MODEL, name: MODEL, isCustom: false, capabilities: { optionDescriptors: descriptors } },
60 ];
61}
62
63function selections(
64 ...entries: Array<[string, string | boolean]>
65): ReadonlyArray<ProviderOptionSelection> {
66 return entries.map(([id, value]) => ({ id, value }));
67}
68
69const ULTRATHINK_FRAME_CLASSES = {
70 composerFrameClassName: "ultrathink-frame",
71 composerSurfaceClassName: "shadow-[0_0_0_1px_rgba(255,255,255,0.07)_inset]",
72 modelPickerIconClassName: "ultrathink-chroma",
73} as const;
74
75describe("getComposerProviderState", () => {
76 it("derives a stable prompt injection state for ordinary prompt edits", () => {
77 expect(getComposerPromptInjectionState("Investigate this failure")).toBe("none");
78 expect(getComposerPromptInjectionState("Ultrathink:\nInvestigate this failure")).toBe(
79 "ultrathink",
80 );
81 });
82
83 it("uses descriptor defaults for display without dispatching them as overrides", () => {
84 const state = getComposerProviderState({
85 provider: PROVIDER,
86 model: MODEL,
87 models: modelWith([
88 selectDescriptor("effort", [
89 { id: "low", label: "Low" },
90 { id: "high", label: "High", isDefault: true },
91 ]),
92 ]),
93 modelOptions: undefined,
94 planModeEnabled: true,
95 });
96
97 expect(state).toEqual({
98 provider: PROVIDER,
99 promptEffort: "high",
100 modelOptionsForDispatch: undefined,
101 });
102 });
103
104 it("lets selections override defaults and propagates them through dispatch", () => {
105 const state = getComposerProviderState({
106 provider: PROVIDER,
107 model: MODEL,
108 models: modelWith([
109 selectDescriptor("effort", [
110 { id: "low", label: "Low" },
111 { id: "high", label: "High", isDefault: true },
112 ]),
113 booleanDescriptor("fastMode"),
114 ]),
115 modelOptions: selections(["effort", "low"], ["fastMode", true]),
116 planModeEnabled: true,
117 });
118
119 expect(state).toEqual({
120 provider: PROVIDER,
121 promptEffort: "low",
122 modelOptionsForDispatch: selections(["effort", "low"], ["fastMode", true]),
123 });
124 });
125
126 it("preserves selections that match defaults so deepMerge can overwrite prior state", () => {
127 const state = getComposerProviderState({
128 provider: PROVIDER,
129 model: MODEL,
130 models: modelWith([
131 selectDescriptor("effort", [{ id: "high", label: "High", isDefault: true }]),
132 booleanDescriptor("fastMode"),
133 ]),
134 modelOptions: selections(["effort", "high"], ["fastMode", false]),
135 planModeEnabled: true,
136 });
137
138 expect(state.modelOptionsForDispatch).toEqual(
139 selections(["effort", "high"], ["fastMode", false]),
140 );
141 });
142
143 it("drops selections for descriptors the model does not declare", () => {
144 const state = getComposerProviderState({
145 provider: PROVIDER,
146 model: MODEL,
147 models: modelWith([booleanDescriptor("thinking")]),
148 modelOptions: selections(["effort", "max"], ["thinking", false]),
149 planModeEnabled: true,
150 });
151
152 expect(state).toEqual({
153 provider: PROVIDER,
154 promptEffort: null,
155 modelOptionsForDispatch: selections(["thinking", false]),
156 });
157 });
158
159 it("derives promptEffort from the first select descriptor and preserves all others for dispatch", () => {
160 const state = getComposerProviderState({
161 provider: PROVIDER,
162 model: MODEL,
163 models: modelWith([
164 selectDescriptor("effort", [{ id: "high", label: "High", isDefault: true }]),
165 selectDescriptor("contextWindow", [
166 { id: "200k", label: "200k", isDefault: true },
167 { id: "1m", label: "1M" },
168 ]),
169 selectDescriptor("agent", [
170 { id: "build", label: "Build", isDefault: true },
171 { id: "plan", label: "Plan" },
172 ]),
173 ]),
174 modelOptions: selections(["agent", "plan"]),
175 planModeEnabled: true,
176 });
177
178 expect(state.promptEffort).toBe("high");
179 expect(state.modelOptionsForDispatch).toEqual(selections(["agent", "plan"]));
180 });
181
182 it("drops the plan agent from dispatch when legacy plan mode is disabled", () => {
183 const state = getComposerProviderState({
184 provider: PROVIDER,
185 model: MODEL,
186 models: modelWith([
187 selectDescriptor("agent", [
188 { id: "build", label: "Build", isDefault: true },
189 { id: "plan", label: "Plan" },
190 ]),
191 ]),
192 modelOptions: selections(["agent", "plan"]),
193 planModeEnabled: false,
194 });
195
196 expect(state.modelOptionsForDispatch).toEqual(selections(["agent", "build"]));
197 });
198
199 it("drops the agent descriptor entirely when plan is the only option and plan mode is disabled", () => {
200 const state = getComposerProviderState({
201 provider: PROVIDER,
202 model: MODEL,
203 models: modelWith([
204 selectDescriptor("agent", [{ id: "plan", label: "Plan", isDefault: true }]),
205 ]),
206 modelOptions: selections(["agent", "plan"]),
207 planModeEnabled: false,
208 });
209
210 expect(state).toEqual({
211 provider: PROVIDER,
212 promptEffort: null,
213 modelOptionsForDispatch: undefined,
214 });
215 });
216
217 it("falls back to a surviving agent when plan was the descriptor default and plan mode is disabled", () => {
218 const state = getComposerProviderState({
219 provider: PROVIDER,
220 model: MODEL,
221 models: modelWith([
222 selectDescriptor("agent", [
223 { id: "plan", label: "Plan", isDefault: true },
224 { id: "research", label: "Research" },
225 ]),
226 ]),
227 modelOptions: undefined,
228 planModeEnabled: false,
229 });
230
231 expect(state.modelOptionsForDispatch).toBeUndefined();
232 });
233
234 it("returns undefined dispatch options when the model declares no descriptors", () => {
235 const state = getComposerProviderState({
236 provider: PROVIDER,
237 model: MODEL,
238 models: modelWith([]),
239 modelOptions: selections(["anything", "value"]),
240 planModeEnabled: true,
241 });
242
243 expect(state).toEqual({
244 provider: PROVIDER,
245 promptEffort: null,
246 modelOptionsForDispatch: undefined,
247 });
248 });
249
250 it("preserves explicit options when the selected model is absent from the catalog", () => {
251 const state = getComposerProviderState({
252 provider: ProviderDriverKind.make("opencode"),
253 model: "opencode/kimi-k3",
254 models: [
255 {
256 slug: "opencode/big-pickle",
257 name: "Big Pickle",
258 isCustom: false,
259 capabilities: {},
260 },
261 ],
262 modelOptions: selections(["variant", "max"], ["agent", "build"]),
263 planModeEnabled: false,
264 });
265
266 expect(state.modelOptionsForDispatch).toEqual(
267 selections(["variant", "max"], ["agent", "build"]),
268 );
269 });
270
271 it.each(["codex", "claudeAgent", "cursor", "grok"])(
272 "does not preserve unknown options for a missing %s model",
273 (provider) => {
274 const state = getComposerProviderState({
275 provider: ProviderDriverKind.make(provider),
276 model: "missing-model",
277 models: modelWith([]),
278 modelOptions: selections(["unknown", "value"]),
279 planModeEnabled: true,
280 });
281
282 expect(state.modelOptionsForDispatch).toBeUndefined();
283 },
284 );
285
286 it("preserves explicit options while the catalog is empty", () => {
287 const state = getComposerProviderState({
288 provider: ProviderDriverKind.make("opencode"),
289 model: "opencode/kimi-k3",
290 models: [],
291 modelOptions: selections(["variant", "max"], ["agent", "build"]),
292 planModeEnabled: false,
293 });
294
295 expect(state.modelOptionsForDispatch).toEqual(
296 selections(["variant", "max"], ["agent", "build"]),
297 );
298 });
299
300 it("validates options for a known model selected through a legacy alias", () => {
301 const state = getComposerProviderState({
302 provider: ProviderDriverKind.make("claudeAgent"),
303 model: "legacy-test-model",
304 models: [
305 {
306 slug: "test-model",
307 name: "Test Model",
308 aliases: ["legacy-test-model"],
309 isCustom: false,
310 capabilities: {
311 optionDescriptors: [
312 selectDescriptor("effort", [
313 { id: "low", label: "Low" },
314 { id: "high", label: "High", isDefault: true },
315 ]),
316 ],
317 },
318 },
319 ],
320 modelOptions: selections(["effort", "low"], ["unknown", "value"]),
321 planModeEnabled: false,
322 });
323
324 expect(state.modelOptionsForDispatch).toEqual(selections(["effort", "low"]));
325 });
326
327 it("still drops the plan agent when an absent model has a saved plan selection", () => {
328 const state = getComposerProviderState({
329 provider: ProviderDriverKind.make("opencode"),
330 model: "opencode/kimi-k3",
331 models: [],
332 modelOptions: selections(["variant", "max"], ["agent", "plan"]),
333 planModeEnabled: false,
334 });
335
336 expect(state.modelOptionsForDispatch).toEqual(selections(["variant", "max"]));
337 });
338
339 it("adds ultrathink class names when the prompt triggers a promptInjectedValues descriptor", () => {
340 const state = getComposerProviderState({
341 provider: PROVIDER,
342 model: MODEL,
343 models: modelWith([
344 selectDescriptor(
345 "effort",
346 [
347 { id: "medium", label: "Medium" },
348 { id: "high", label: "High", isDefault: true },
349 { id: "ultrathink", label: "Ultrathink" },
350 ],
351 ["ultrathink"],
352 ),
353 ]),
354 promptInjectionState: getComposerPromptInjectionState(
355 "Ultrathink:\nInvestigate this failure",
356 ),
357 modelOptions: selections(["effort", "medium"]),
358 planModeEnabled: true,
359 });
360
361 expect(state).toEqual({
362 provider: PROVIDER,
363 promptEffort: "medium",
364 modelOptionsForDispatch: selections(["effort", "medium"]),
365 ...ULTRATHINK_FRAME_CLASSES,
366 });
367 });
368
369 it("does not add ultrathink class names when the descriptor has no promptInjectedValues", () => {
370 const state = getComposerProviderState({
371 provider: PROVIDER,
372 model: MODEL,
373 models: modelWith([
374 selectDescriptor("effort", [{ id: "high", label: "High", isDefault: true }]),
375 ]),
376 promptInjectionState: getComposerPromptInjectionState(
377 "Ultrathink:\nInvestigate this failure",
378 ),
379 modelOptions: undefined,
380 planModeEnabled: true,
381 });
382
383 expect(state).not.toHaveProperty("composerFrameClassName");
384 expect(state).not.toHaveProperty("composerSurfaceClassName");
385 expect(state).not.toHaveProperty("modelPickerIconClassName");
386 });
387
388 it("defaults fastMode to false when the provider reports true but the user has not selected it", () => {
389 const state = getComposerProviderState({
390 provider: ProviderDriverKind.make("cursor"),
391 model: MODEL,
392 models: modelWith([booleanDescriptor("fastMode", true)]),
393 modelOptions: undefined,
394 planModeEnabled: true,
395 });
396
397 expect(state.modelOptionsForDispatch).toEqual(selections(["fastMode", false]));
398 });
399
400 it("keeps explicit fastMode true when the user selected Fast", () => {
401 const state = getComposerProviderState({
402 provider: ProviderDriverKind.make("cursor"),
403 model: MODEL,
404 models: modelWith([booleanDescriptor("fastMode", true)]),
405 modelOptions: selections(["fastMode", true]),
406 planModeEnabled: true,
407 });
408
409 expect(state.modelOptionsForDispatch).toEqual(selections(["fastMode", true]));
410 });
411
412 it("keeps explicit fastMode false when the user selected Normal", () => {
413 const state = getComposerProviderState({
414 provider: ProviderDriverKind.make("cursor"),
415 model: MODEL,
416 models: modelWith([booleanDescriptor("fastMode", true)]),
417 modelOptions: selections(["fastMode", false]),
418 planModeEnabled: true,
419 });
420
421 expect(state.modelOptionsForDispatch).toEqual(selections(["fastMode", false]));
422 });
423});
424
425describe("withImplicitFastModeDefault", () => {
426 it("injects fastMode false only when the model exposes fastMode and no selection exists", () => {
427 expect(
428 withImplicitFastModeDefault(
429 {
430 optionDescriptors: [booleanDescriptor("fastMode", true)],
431 },
432 undefined,
433 ),
434 ).toEqual(selections(["fastMode", false]));
435
436 expect(
437 withImplicitFastModeDefault(
438 {
439 optionDescriptors: [booleanDescriptor("fastMode", true)],
440 },
441 selections(["fastMode", true]),
442 ),
443 ).toEqual(selections(["fastMode", true]));
444 });
445
446 it("does not add fastMode when the model does not expose it", () => {
447 expect(
448 withImplicitFastModeDefault(
449 {
450 optionDescriptors: [booleanDescriptor("thinking", true)],
451 },
452 undefined,
453 ),
454 ).toBeUndefined();
455 });
456});
457
458describe("trait controls fastMode display", () => {
459 it("resolves traits fastMode to Normal when the provider defaults to true without a user selection", () => {
460 const models = modelWith([booleanDescriptor("fastMode", true)]);
461 const provider = ProviderDriverKind.make("cursor");
462 const caps = getProviderModelCapabilities(models, MODEL, provider);
463 const resolved = withImplicitFastModeDefault(caps, undefined);
464 const descriptors = getProviderOptionDescriptors({ caps, selections: resolved });
465 const fastMode = descriptors.find((descriptor) => descriptor.id === "fastMode");
466
467 expect(fastMode?.type).toBe("boolean");
468 if (fastMode?.type === "boolean") {
469 expect(fastMode.currentValue).toBe(false);
470 }
471 });
472});
473
474describe("provider traits render guards", () => {
475 it("returns null when no thread target is provided", () => {
476 const models = modelWith([
477 selectDescriptor("effort", [{ id: "high", label: "High", isDefault: true }]),
478 ]);
479 const args = {
480 provider: PROVIDER,
481 model: MODEL,
482 models,
483 modelOptions: undefined,
484 prompt: "",
485 onPromptChange: () => {},
486 planModeEnabled: true,
487 };
488
489 expect(renderProviderTraitsPicker(args)).toBeNull();
490 expect(renderProviderTraitsMenuContent(args)).toBeNull();
491 });
492});