282 lines
7.8 KiB
TypeScript
282 lines
7.8 KiB
TypeScript
import { describe, expect, it } from "vitest";
|
|
import {
|
|
triggerLabel,
|
|
type ModelSelectorOption,
|
|
usableConfigOptions,
|
|
} from "@/components/model-config-selector";
|
|
import { hasCompleteDynamicConfig, requiredConfigKeys } from "@/components/task/model-selector";
|
|
import type { Agent, TaskSession } from "@/lib/types/http";
|
|
import type {
|
|
ConfigOptionEntry,
|
|
SessionModelEntry,
|
|
} from "@/lib/state/slices/session-runtime/types";
|
|
|
|
function compactTriggerLabel(
|
|
modelOptions: ModelSelectorOption[],
|
|
currentModel: string,
|
|
configOptions: ConfigOptionEntry[],
|
|
configBaseline?: Record<string, string>,
|
|
): string {
|
|
return triggerLabel(modelOptions, currentModel, configOptions, {
|
|
summary: "changed",
|
|
configBaseline,
|
|
});
|
|
}
|
|
|
|
const providerModelId = "gpt-5.6-sol";
|
|
const providerModelName = "GPT-5.6-Sol";
|
|
const modelOptions = [{ id: providerModelId, name: providerModelName }];
|
|
|
|
function sessionConfigOptions(reasoningEffort = "high", fastMode = "off"): ConfigOptionEntry[] {
|
|
return [
|
|
{
|
|
type: "select",
|
|
id: "model",
|
|
name: "Model",
|
|
currentValue: providerModelId,
|
|
category: "model",
|
|
options: [{ value: providerModelId, name: providerModelName }],
|
|
},
|
|
{
|
|
type: "select",
|
|
id: "reasoning_effort",
|
|
name: "Reasoning Effort",
|
|
currentValue: reasoningEffort,
|
|
options: [
|
|
{ value: "high", name: "High" },
|
|
{ value: "low", name: "Low" },
|
|
],
|
|
},
|
|
{
|
|
type: "select",
|
|
id: "fast_mode",
|
|
name: "Fast Mode",
|
|
currentValue: fastMode,
|
|
options: [
|
|
{ value: "off", name: "Off" },
|
|
{ value: "on", name: "On" },
|
|
],
|
|
},
|
|
];
|
|
}
|
|
|
|
describe("model selector config options", () => {
|
|
it("keeps model-adjacent config options and excludes mode", () => {
|
|
const options: ConfigOptionEntry[] = [
|
|
{
|
|
type: "select",
|
|
id: "mode",
|
|
name: "Mode",
|
|
currentValue: "agent",
|
|
category: "mode",
|
|
options: [{ value: "agent", name: "Agent" }],
|
|
},
|
|
{
|
|
type: "select",
|
|
id: "model",
|
|
name: "Model",
|
|
currentValue: "gpt-5.5",
|
|
category: "model",
|
|
options: [{ value: "gpt-5.5", name: "GPT-5.5" }],
|
|
},
|
|
{
|
|
type: "select",
|
|
id: "reasoning_effort",
|
|
name: "Reasoning Effort",
|
|
currentValue: "high",
|
|
category: "thought_level",
|
|
options: [{ value: "high", name: "High" }],
|
|
},
|
|
];
|
|
|
|
expect(usableConfigOptions(options).map((option) => option.id)).toEqual([
|
|
"model",
|
|
"reasoning_effort",
|
|
]);
|
|
});
|
|
|
|
it("summarizes model and extra option values in one toolbar label", () => {
|
|
const label = triggerLabel([{ id: "gpt-5.5", name: "GPT-5.5" }], "gpt-5.5", [
|
|
{
|
|
type: "select",
|
|
id: "model",
|
|
name: "Model",
|
|
currentValue: "gpt-5.5",
|
|
category: "model",
|
|
options: [{ value: "gpt-5.5", name: "GPT-5.5" }],
|
|
},
|
|
{
|
|
type: "select",
|
|
id: "reasoning_effort",
|
|
name: "Reasoning Effort",
|
|
currentValue: "medium",
|
|
category: "thought_level",
|
|
options: [{ value: "medium", name: "Medium" }],
|
|
},
|
|
]);
|
|
|
|
expect(label).toBe("GPT-5.5 / Medium");
|
|
});
|
|
});
|
|
|
|
describe("task model selector compact label", () => {
|
|
it("shows only the model when task configuration matches its baseline", () => {
|
|
const label = compactTriggerLabel(modelOptions, providerModelId, sessionConfigOptions(), {
|
|
model: providerModelId,
|
|
reasoning_effort: "high",
|
|
fast_mode: "off",
|
|
});
|
|
|
|
expect(label).toBe(providerModelName);
|
|
});
|
|
|
|
it("shows all current values while the task baseline is unavailable", () => {
|
|
const label = compactTriggerLabel(
|
|
modelOptions,
|
|
providerModelId,
|
|
sessionConfigOptions("low", "on"),
|
|
);
|
|
|
|
expect(label).toBe(`${providerModelName} / Low / On`);
|
|
});
|
|
|
|
it("shows every changed value in ACP option order", () => {
|
|
const label = compactTriggerLabel(
|
|
modelOptions,
|
|
providerModelId,
|
|
sessionConfigOptions("low", "on"),
|
|
{
|
|
model: providerModelId,
|
|
reasoning_effort: "high",
|
|
fast_mode: "off",
|
|
},
|
|
);
|
|
|
|
expect(label).toBe(`${providerModelName} / Low / On`);
|
|
});
|
|
|
|
it("shows a profile-selected value that differs from the provider default", () => {
|
|
const label = compactTriggerLabel(modelOptions, providerModelId, sessionConfigOptions("high"), {
|
|
model: providerModelId,
|
|
reasoning_effort: "medium",
|
|
fast_mode: "off",
|
|
});
|
|
|
|
expect(label).toBe(`${providerModelName} / High`);
|
|
});
|
|
|
|
it("hides a value after it returns to baseline", () => {
|
|
const label = compactTriggerLabel(modelOptions, providerModelId, sessionConfigOptions("high"), {
|
|
reasoning_effort: "high",
|
|
fast_mode: "off",
|
|
});
|
|
|
|
expect(label).toBe(providerModelName);
|
|
});
|
|
|
|
it("treats a currently advertised option with no baseline entry as changed", () => {
|
|
const label = compactTriggerLabel(modelOptions, providerModelId, sessionConfigOptions(), {
|
|
reasoning_effort: "high",
|
|
removed_option: "legacy",
|
|
});
|
|
|
|
expect(label).toBe(`${providerModelName} / Off`);
|
|
});
|
|
});
|
|
|
|
function makeSession(overrides: Partial<TaskSession> = {}): TaskSession {
|
|
return {
|
|
id: "session-1",
|
|
task_id: "task-1",
|
|
state: "RUNNING",
|
|
started_at: "2026-01-01T00:00:00Z",
|
|
updated_at: "2026-01-01T00:00:00Z",
|
|
...overrides,
|
|
} as TaskSession;
|
|
}
|
|
|
|
const flatModelId = "claude-opus-4-8";
|
|
|
|
function flatModelEntries(): SessionModelEntry[] {
|
|
return [
|
|
{ modelId: flatModelId, name: "Claude Opus 4.8" },
|
|
{ modelId: "claude-sonnet-4-8", name: "Claude Sonnet 4.8" },
|
|
];
|
|
}
|
|
|
|
describe("hasCompleteDynamicConfig", () => {
|
|
const noAgents: Agent[] = [];
|
|
|
|
it("treats a required model key as satisfied by a flat ACP model list", () => {
|
|
const session = makeSession({
|
|
metadata: { runtime_config: { config_options: { model: flatModelId } } },
|
|
});
|
|
expect(requiredConfigKeys(session, noAgents)).toEqual(["model"]);
|
|
|
|
const hydrated = hasCompleteDynamicConfig(
|
|
session,
|
|
{ currentModelId: flatModelId, models: flatModelEntries(), configOptions: [] },
|
|
noAgents,
|
|
);
|
|
|
|
expect(hydrated).toBe(true);
|
|
});
|
|
|
|
it("still requires the model config option when there is no flat model list", () => {
|
|
const session = makeSession({
|
|
metadata: { runtime_config: { config_options: { model: providerModelId } } },
|
|
});
|
|
|
|
const hydrated = hasCompleteDynamicConfig(
|
|
session,
|
|
{ currentModelId: providerModelId, models: [], configOptions: [] },
|
|
noAgents,
|
|
);
|
|
|
|
expect(hydrated).toBe(false);
|
|
});
|
|
|
|
it("hydrates a config-option agent once the model option is present", () => {
|
|
const session = makeSession({
|
|
metadata: { runtime_config: { config_options: { model: providerModelId } } },
|
|
});
|
|
|
|
const modelOption: ConfigOptionEntry = {
|
|
type: "select",
|
|
id: "model",
|
|
name: "Model",
|
|
currentValue: providerModelId,
|
|
category: "model",
|
|
options: [{ value: providerModelId, name: providerModelName }],
|
|
};
|
|
|
|
const hydrated = hasCompleteDynamicConfig(
|
|
session,
|
|
{ currentModelId: providerModelId, models: [], configOptions: [modelOption] },
|
|
noAgents,
|
|
);
|
|
|
|
expect(hydrated).toBe(true);
|
|
});
|
|
|
|
it("does not let a flat model list satisfy a non-model required key", () => {
|
|
const session = makeSession({
|
|
metadata: {
|
|
runtime_config: { config_options: { model: flatModelId, reasoning_effort: "high" } },
|
|
},
|
|
});
|
|
|
|
const hydrated = hasCompleteDynamicConfig(
|
|
session,
|
|
{ currentModelId: flatModelId, models: flatModelEntries(), configOptions: [] },
|
|
noAgents,
|
|
);
|
|
|
|
expect(hydrated).toBe(false);
|
|
});
|
|
|
|
it("is hydrated when there are no required config keys", () => {
|
|
expect(hasCompleteDynamicConfig(makeSession(), undefined, noAgents)).toBe(true);
|
|
});
|
|
});
|