Files
agent-radar 2ed3415fb7
Publish Claude Code Binaries / Stage and publish binaries (push) Canceled after 0s
Publish Tutti App Runtime / Build darwin-arm64 (push) Canceled after 0s
Publish Tutti App Runtime / Build darwin-amd64 (push) Canceled after 0s
Publish Tutti App Runtime / Build linux-amd64 (push) Canceled after 0s
Publish Tutti App Runtime / Build windows-amd64 (push) Canceled after 0s
Sync Tutti Agent Skills / sync (push) Canceled after 0s
Windows Agent Adapters / Test Windows agent adapters (push) Canceled after 0s
Windows Daemon Adapters / Test Windows daemon adapters (push) Canceled after 0s
Publish Tutti App Runtime / Publish runtime catalog (push) Canceled after 0s
源码快照 @ 2026-09-01 (source-only, 剔除非源码素材)
2026-09-01 20:17:27 +08:00

684 lines
21 KiB
TypeScript

import { describe, expect, it } from "vitest";
import type { AgentGUIComposerSettingsVM } from "./agentGuiNodeTypes";
import {
buildComposerModelMenuModel,
formatModelDisplayLabel,
type AgentComposerSettingsMenuLabels
} from "./composerSettingsMenuModel";
const labels: AgentComposerSettingsMenuLabels = {
modelLabel: "Model",
modelSelectionLabel: "Model selection",
modelContextWindowSuffix: "context window",
modelTooltipVersionLabel: "Version",
defaultModel: "Default model",
loadingOptions: "Loading…",
inheritedUnavailable: "Unavailable",
reasoningLabel: "Reasoning",
reasoningDegreeLabel: "Reasoning degree",
reasoningOptionDefault: "Default",
reasoningOptionMinimal: "Minimal",
reasoningOptionLow: "Low",
reasoningOptionMedium: "Medium",
reasoningOptionHigh: "High",
reasoningOptionXHigh: "X-High",
reasoningOptionMax: "Max",
reasoningOptionUltra: "Ultra",
speedLabel: "Speed",
speedSelectionLabel: "Speed",
speedOptionStandard: "Standard",
speedOptionStandardDescription: "Standard localized",
speedOptionFast: "Fast",
speedOptionFastDescription: "Fast localized",
permissionLabel: "Permissions",
planModeLabel: "Plan",
modelDescriptions: {
frontierComplexCoding: "Frontier",
everydayCoding: "Everyday",
smallFastCostEfficient: "Small",
codingOptimized: "Coding",
ultraFastCoding: "Ultra",
professionalLongRunning: "Pro"
}
};
function vm(
overrides: Partial<AgentGUIComposerSettingsVM> = {}
): AgentGUIComposerSettingsVM {
return {
sessionSettings: null,
draftSettings: {
model: "gpt-5.5",
reasoningEffort: "high",
speed: "standard",
planMode: false,
permissionModeId: "preset"
},
supportsModel: true,
supportsReasoningEffort: true,
supportsSpeed: true,
speedUnavailable: false,
availableSpeeds: [
{ value: "standard", label: "standard", description: "Standard speed" },
{ value: "fast", label: "fast", description: "1.5x speed" }
],
supportsPlanMode: false,
isSettingsLoading: false,
modelUnavailable: false,
reasoningUnavailable: false,
availableModels: [
{ value: "gpt-5.5", label: "gpt-5.5" },
{ value: "gpt-5.4", label: "gpt-5.4" }
],
availableReasoningEfforts: [
{ value: "default", label: "Default" },
{ value: "low", label: "Low" },
{ value: "high", label: "High" },
{ value: "max", label: "Max" },
{ value: "ultra", label: "ultra" },
{ value: "none", label: "Off" }
],
...overrides
};
}
describe("buildComposerModelMenuModel", () => {
it("models the trigger, sections and resolved option labels", () => {
const menu = buildComposerModelMenuModel(vm(), labels);
expect(menu.disabled).toBe(false);
expect(menu.trigger).toMatchObject({
isFast: false,
modelLabel: "GPT-5.5",
reasoningLabel: "High",
combinedLabel: "GPT-5.5 High",
showCombined: false
});
expect(menu.model.show).toBe(true);
expect(menu.model.selectedValue).toBe("gpt-5.5");
expect(menu.model.options).toEqual([
{ value: "gpt-5.5", label: "GPT-5.5", description: undefined },
{ value: "gpt-5.4", label: "GPT-5.4", description: undefined }
]);
expect(menu.reasoning.show).toBe(true);
expect(menu.reasoning.selectedLabel).toBe("High");
expect(menu.reasoning.options).toEqual([
{ value: "default", label: "Default" },
{ value: "low", label: "Low" },
{ value: "high", label: "High" },
{ value: "max", label: "Max" },
{ value: "ultra", label: "Ultra" },
{ value: "none", label: "Off" }
]);
expect(menu.speed.show).toBe(true);
expect(menu.speed.selectedLabel).toBe("Standard");
expect(menu.speed.options).toEqual([
{
value: "standard",
label: "Standard",
description: "Standard localized"
},
{ value: "fast", label: "Fast", description: "Fast localized" }
]);
});
it("localizes ultra in the final menu presentation", () => {
const menu = buildComposerModelMenuModel(
vm({
draftSettings: {
model: "gpt-5.5",
reasoningEffort: "ultra",
speed: "standard",
planMode: false,
permissionModeId: "preset"
},
selectedReasoningEffortValue: "ultra",
availableReasoningEfforts: [{ value: "ultra", label: "ultra" }]
}),
{ ...labels, reasoningOptionUltra: "极致" }
);
expect(menu.reasoning.selectedLabel).toBe("极致");
expect(menu.reasoning.options).toEqual([{ value: "ultra", label: "极致" }]);
});
it("uses the provider-localized label for an extensible reasoning value", () => {
const menu = buildComposerModelMenuModel(
vm({
draftSettings: {
model: "gpt-5.6-sol",
reasoningEffort: "none",
speed: "standard",
planMode: false,
permissionModeId: "preset"
},
selectedReasoningEffortValue: "none",
availableReasoningEfforts: [{ value: "none", label: "关闭" }]
}),
labels
);
expect(menu.reasoning.selectedLabel).toBe("关闭");
expect(menu.reasoning.options).toEqual([{ value: "none", label: "关闭" }]);
});
it("localizes ultra effort in model summaries", () => {
const menu = buildComposerModelMenuModel(
vm({
availableModels: [
{
value: "gpt-5.6-sol",
label: "GPT-5.6-Sol",
description: "GPT-5.6 Sol · ultra effort"
}
],
selectedModelValue: "gpt-5.6-sol"
}),
labels
);
expect(menu.model.options[0]).toMatchObject({
summary: ["Ultra"],
tooltip: { version: "Version: ultra effort" }
});
});
it("presents the typed model consumption multiplier", () => {
const menu = buildComposerModelMenuModel(
vm({
availableModels: [
{
value: "hy3",
label: "Hy3",
consumptionMultiplier: "0.71"
}
],
selectedModelValue: "hy3"
}),
labels
);
expect(menu.model.options[0]).toMatchObject({
label: "Hy3",
tooltip: {
consumptionMultiplier: "0.71x",
title: "Hy3"
}
});
expect(menu.model.options[0]?.tooltip?.description).toBeUndefined();
});
it("marks the trigger as fast and localizes model descriptions", () => {
const menu = buildComposerModelMenuModel(
vm({
draftSettings: {
model: "gpt-5.5",
reasoningEffort: "high",
speed: "fast",
planMode: false,
permissionModeId: "preset"
},
availableModels: [
{
value: "gpt-5.5",
label: "GPT-5.5",
description:
"Frontier model for complex coding, research, and real-world work."
}
]
}),
labels
);
expect(menu.trigger.isFast).toBe(true);
expect(menu.speed.selectedLabel).toBe("Fast");
expect(menu.model.options[0]).toMatchObject({
description: "Frontier",
tooltip: {
description: "Frontier",
title: "GPT-5.5"
}
});
});
it("presents Default with the provider-resolved Claude model", () => {
const menu = buildComposerModelMenuModel(
vm({
draftSettings: {
model: "default",
reasoningEffort: "high",
speed: "standard",
planMode: false,
permissionModeId: "preset"
},
selectedModelValue: "default",
effectiveModelValue: "claude-sonnet-4-6",
availableModels: [
{
value: "default",
label: "Default (recommended)",
description:
"Opus 4.8 with 1M context · Most capable for complex work"
},
{
value: "sonnet",
label: "Sonnet",
description: "Sonnet 4.6 · Best for everyday tasks · medium effort"
},
{
value: "opus",
label: "Opus 4.8",
description:
"Opus 4.8 with 1M context · Most capable for complex work"
}
]
}),
labels
);
expect(menu.trigger.modelLabel).toBe("Default (Sonnet)");
expect(menu.model.options).toEqual([
{
value: "default",
label: "Default (Sonnet)",
description: "Sonnet 4.6 · Best for everyday tasks · medium effort",
summary: ["Medium"],
tooltip: {
description: "Best for everyday tasks",
title: "Sonnet 4.6",
version: "Version: medium effort"
}
},
{
value: "sonnet",
label: "Sonnet",
description: "Sonnet 4.6 · Best for everyday tasks · medium effort",
summary: ["Medium"],
tooltip: {
description: "Best for everyday tasks",
title: "Sonnet 4.6",
version: "Version: medium effort"
}
},
{
value: "opus",
label: "Opus",
description: "Opus 4.8 with 1M context · Most capable for complex work",
summary: ["1M"],
tooltip: {
contextWindow: "1M context window",
description: "Most capable for complex work",
title: "Opus 4.8"
}
}
]);
});
it("does not present the catalog's static Default description as runtime truth", () => {
const menu = buildComposerModelMenuModel(
vm({
draftSettings: {
model: "default",
reasoningEffort: "high",
speed: "standard",
planMode: false,
permissionModeId: "preset"
},
selectedModelValue: "default",
availableModels: [
{
value: "default",
label: "Sonnet 5",
description: "Efficient for routine tasks"
},
{
value: "opus",
label: "Opus 4.8",
description: "Most capable"
}
]
}),
labels
);
expect(menu.trigger.modelLabel).toBe("Default");
expect(menu.model.options[0]).toEqual({
value: "default",
label: "Default",
description: undefined
});
});
it("injects the selected value when missing from the advertised options", () => {
const menu = buildComposerModelMenuModel(
vm({
selectedModelValue: "custom-model",
draftSettings: {
model: "custom-model",
reasoningEffort: "high",
speed: "standard",
planMode: false,
permissionModeId: "preset"
}
}),
labels
);
expect(menu.model.selectedValue).toBe("custom-model");
expect(menu.model.options[0]).toEqual({
value: "custom-model",
label: "Custom-Model",
description: undefined
});
});
it("collapses the model list to the latest version per family when enabled", () => {
const menu = buildComposerModelMenuModel(
vm({
collapseModelOptionsToLatest: true,
selectedModelValue: "claude-sonnet-5[thinking=true]",
draftSettings: {
model: "claude-sonnet-5[thinking=true]",
reasoningEffort: "high",
speed: "standard",
planMode: false,
permissionModeId: "preset"
},
availableModels: [
{ value: "default[]", label: "Auto" },
{
value: "claude-sonnet-4.6[thinking=true]",
label: "claude-sonnet-4.6"
},
{
value: "claude-sonnet-4.7[thinking=true]",
label: "claude-sonnet-4.7"
},
{ value: "claude-sonnet-5[thinking=true]", label: "claude-sonnet-5" },
{ value: "gpt-5.2[fast=false]", label: "gpt-5.2" },
{ value: "gpt-5.3-codex[fast=false]", label: "gpt-5.3-codex" }
]
}),
labels
);
expect(menu.model.options.map((option) => option.value)).toEqual([
"default[]",
"claude-sonnet-5[thinking=true]",
"gpt-5.3-codex[fast=false]"
]);
});
it("keeps a selected older version visible after the family collapses", () => {
const menu = buildComposerModelMenuModel(
vm({
collapseModelOptionsToLatest: true,
selectedModelValue: "claude-sonnet-4.6[thinking=true]",
draftSettings: {
model: "claude-sonnet-4.6[thinking=true]",
reasoningEffort: "high",
speed: "standard",
planMode: false,
permissionModeId: "preset"
},
availableModels: [
{
value: "claude-sonnet-4.6[thinking=true]",
label: "claude-sonnet-4.6"
},
{ value: "claude-sonnet-5[thinking=true]", label: "claude-sonnet-5" }
]
}),
labels
);
const values = menu.model.options.map((option) => option.value);
expect(values).toContain("claude-sonnet-5[thinking=true]");
expect(values).toContain("claude-sonnet-4.6[thinking=true]");
expect(menu.model.selectedValue).toBe("claude-sonnet-4.6[thinking=true]");
});
it("normalizes GPT casing while capitalizing each label segment", () => {
expect(formatModelDisplayLabel("gpt-5.5")).toBe("GPT-5.5");
expect(formatModelDisplayLabel("gpt-5.3-codex")).toBe("GPT-5.3-Codex");
expect(formatModelDisplayLabel("Gpt-5.5-codex")).toBe("GPT-5.5-Codex");
expect(formatModelDisplayLabel("vendor/gpt-5.5")).toBe("Vendor/GPT-5.5");
expect(formatModelDisplayLabel("codex")).toBe("Codex");
expect(formatModelDisplayLabel("custom-model")).toBe("Custom-Model");
});
it("hides dimensions that are unsupported or unavailable", () => {
const menu = buildComposerModelMenuModel(
vm({
supportsSpeed: false,
availableSpeeds: [],
reasoningUnavailable: true
}),
labels
);
expect(menu.model.show).toBe(true);
expect(menu.reasoning.show).toBe(false);
expect(menu.speed.show).toBe(false);
expect(menu.trigger.reasoningLabel).toBe("");
expect(menu.trigger.showCombined).toBe(true);
});
it("does not show a stale default effort when reasoning is not configurable", () => {
// Cursor-like: draft still carries the GUI default "high", but the
// provider does not expose a reasoning selector.
const menu = buildComposerModelMenuModel(
vm({
supportsReasoningEffort: false,
availableReasoningEfforts: [],
draftSettings: {
model: "gpt-5.2[reasoning=medium,fast=false]",
reasoningEffort: "high",
speed: null,
planMode: false,
permissionModeId: "agent"
},
availableModels: [
{
value: "gpt-5.2[reasoning=medium,fast=false]",
label: "gpt-5.2"
}
],
supportsSpeed: false,
availableSpeeds: []
}),
labels
);
expect(menu.reasoning.show).toBe(false);
expect(menu.trigger).toMatchObject({
modelLabel: "GPT-5.2",
reasoningLabel: "",
combinedLabel: "GPT-5.2",
showCombined: true
});
});
it("shows the loading copy on the trigger while options load", () => {
// No model resolved yet — the placeholder must read as loading, not the
// "Default" fallback (which looks like a real choice).
const loadingVm = (overrides: Partial<AgentGUIComposerSettingsVM>) =>
vm({
availableModels: [],
draftSettings: {
model: null,
reasoningEffort: null,
speed: null,
planMode: false,
permissionModeId: null
},
selectedModelValue: null,
...overrides
});
expect(
buildComposerModelMenuModel(
loadingVm({ isSettingsLoading: true }),
labels
).trigger.modelLabel
).toBe("Loading…");
expect(
buildComposerModelMenuModel(
loadingVm({ isModelOptionsLoading: true }),
labels
).trigger.modelLabel
).toBe("Loading…");
});
it("disables the menu while settings load or nothing is configurable", () => {
expect(
buildComposerModelMenuModel(vm({ isSettingsLoading: true }), labels)
.disabled
).toBe(true);
expect(
buildComposerModelMenuModel(
vm({
supportsModel: false,
supportsReasoningEffort: false,
supportsSpeed: false,
availableModels: [],
availableReasoningEfforts: [],
availableSpeeds: []
}),
labels
).disabled
).toBe(true);
});
});
describe("buildComposerModelMenuModel plan, search and history", () => {
function planVm(
overrides: Partial<AgentGUIComposerSettingsVM> = {}
): AgentGUIComposerSettingsVM {
return vm({
modelPlan: { id: "plan-1", name: "Volc Coding Plan", protocol: "openai" },
availableModels: [
{ value: "gpt-5.5", label: "gpt-5.5", description: "Volc Coding Plan" },
{ value: "gpt-5.4", label: "gpt-5.4", description: "Volc Coding Plan" }
],
...overrides
});
}
function manyModels(count: number): AgentGUIComposerSettingsVM {
return vm({
availableModels: Array.from({ length: count }, (_, index) => ({
value: `model-${index + 1}`,
label: `model-${index + 1}`
})),
draftSettings: {
model: "model-1",
reasoningEffort: "high",
speed: "standard",
planMode: false,
permissionModeId: "preset"
}
});
}
it("surfaces the bound plan and inline plan-name descriptions", () => {
const menu = buildComposerModelMenuModel(planVm(), labels);
expect(menu.model.plan).toEqual({ id: "plan-1", name: "Volc Coding Plan" });
expect(menu.model.optionDescriptionInline).toBe(true);
expect(menu.model.options[0]?.description).toBe("Volc Coding Plan");
const withoutPlan = buildComposerModelMenuModel(vm(), labels);
expect(withoutPlan.model.plan).toBeNull();
expect(withoutPlan.model.optionDescriptionInline).toBe(false);
});
it("enables the filter input above the threshold and filters by label/id", () => {
const small = buildComposerModelMenuModel(vm(), labels);
expect(small.model.searchEnabled).toBe(false);
// Below the threshold the query is ignored entirely.
expect(
buildComposerModelMenuModel(vm(), labels, { searchQuery: "5.4" }).model
.options
).toHaveLength(2);
const large = buildComposerModelMenuModel(manyModels(10), labels);
expect(large.model.searchEnabled).toBe(true);
expect(large.model.options).toHaveLength(10);
const filtered = buildComposerModelMenuModel(manyModels(10), labels, {
searchQuery: "MODEL-1"
});
expect(filtered.model.searchQuery).toBe("MODEL-1");
expect(filtered.model.options.map((option) => option.value)).toEqual([
"model-1",
"model-10"
]);
expect(
buildComposerModelMenuModel(manyModels(10), labels, {
searchQuery: "no-match"
}).model.options
).toHaveLength(0);
});
it("splits favorites and recents into pinned groups without duplicates", () => {
const menu = buildComposerModelMenuModel(manyModels(10), labels, {
favoriteModelIds: ["model-3", "model-9", "model-unknown"],
recentModelIds: ["model-9", "model-2", "model-5"]
});
expect(menu.model.favoriteOptions.map((option) => option.value)).toEqual([
"model-3",
"model-9"
]);
// model-9 is favorited, so recents keep only the rest, in recency order.
expect(menu.model.recentOptions.map((option) => option.value)).toEqual([
"model-2",
"model-5"
]);
expect(menu.model.favoriteValues).toEqual(["model-3", "model-9"]);
const remaining = menu.model.options.map((option) => option.value);
expect(remaining).not.toContain("model-3");
expect(remaining).not.toContain("model-9");
expect(remaining).not.toContain("model-2");
expect(remaining).not.toContain("model-5");
expect(remaining).toHaveLength(6);
});
it("applies the search filter to the pinned groups too", () => {
const menu = buildComposerModelMenuModel(manyModels(10), labels, {
favoriteModelIds: ["model-3"],
recentModelIds: ["model-2"],
searchQuery: "model-2"
});
expect(menu.model.favoriteOptions).toHaveLength(0);
expect(menu.model.recentOptions.map((option) => option.value)).toEqual([
"model-2"
]);
expect(menu.model.options).toHaveLength(0);
});
it("shows the switch-effect hint only for active modelSwitch sessions", () => {
expect(
buildComposerModelMenuModel(
vm({ modelSwitchTakesEffectNextTurn: true }),
labels
).switchEffectHint
).toBe(true);
expect(buildComposerModelMenuModel(vm(), labels).switchEffectHint).toBe(
false
);
// The hint is a model-menu affordance: no model section, no hint.
expect(
buildComposerModelMenuModel(
vm({
modelSwitchTakesEffectNextTurn: true,
supportsModel: false,
availableModels: []
}),
labels
).switchEffectHint
).toBe(false);
});
});