Files
tutti/services/tuttid/api/daemon_agent_composer_options_test.go
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

336 lines
14 KiB
Go

package api
import (
"context"
"net/http"
"slices"
"testing"
"github.com/tutti-os/tutti/packages/agent/daemon/providerregistry"
tuttigenerated "github.com/tutti-os/tutti/services/tuttid/api/generated"
preferencesbiz "github.com/tutti-os/tutti/services/tuttid/biz/preferences"
agentservice "github.com/tutti-os/tutti/services/tuttid/service/agent"
)
// Requested-origin model entries (warm-catalog append of the requested model,
// bootstrap echo) must keep their provenance across the API projection so
// clients can exclude them from catalog testimony; catalog entries omit the
// field entirely (backward-compatible optional).
func TestGeneratedComposerConfigOptionKeepsRequestedProvenance(t *testing.T) {
generated := generatedComposerConfigOption(agentservice.ComposerConfigOption{
Configurable: true,
CurrentValue: "default",
EffectiveValue: "claude-haiku-4-5-20251001",
Options: []agentservice.ComposerConfigOptionValue{
{ID: "gpt-5.6-sol", Label: "GPT-5.6 Sol", Value: "gpt-5.6-sol", ConsumptionMultiplier: "0.71"},
{ID: "x-ai/grok-4.5", Label: "x-ai/grok-4.5", Value: "x-ai/grok-4.5", Requested: true},
},
})
if len(generated.Options) != 2 {
t.Fatalf("expected both options, got %d", len(generated.Options))
}
if generated.Options[0].Requested != nil {
t.Fatal("catalog entry must omit the requested field")
}
if generated.Options[0].ConsumptionMultiplier == nil || *generated.Options[0].ConsumptionMultiplier != "0.71" {
t.Fatalf("consumption multiplier = %#v, want 0.71", generated.Options[0].ConsumptionMultiplier)
}
if generated.Options[1].Requested == nil || !*generated.Options[1].Requested {
t.Fatal("requested-origin entry must project requested=true")
}
if generated.CurrentValue == nil || *generated.CurrentValue != "default" {
t.Fatalf("current value = %#v, want default", generated.CurrentValue)
}
if generated.EffectiveValue == nil ||
*generated.EffectiveValue != "claude-haiku-4-5-20251001" {
t.Fatalf("effective value = %#v, want resolved Haiku model", generated.EffectiveValue)
}
}
func TestDaemonAPIGeneratedRoutesGetAgentProviderComposerOptions(t *testing.T) {
mux := http.NewServeMux()
RegisterRoutes(mux, NewRoutes(DaemonAPI{
AgentSessionService: stubAgentSessionService{
composerOptionsFn: func(_ context.Context, input agentservice.ComposerOptionsInput) (agentservice.ComposerOptions, error) {
if input.Locale != "zh-CN" {
t.Fatalf("locale = %q, want zh-CN", input.Locale)
}
if input.Provider != "codex" {
t.Fatalf("provider = %q, want codex", input.Provider)
}
if input.Cwd != "/workspace/project" {
t.Fatalf("cwd = %q, want /workspace/project", input.Cwd)
}
if input.Settings.Model != "gpt-5" || input.Settings.ReasoningEffort != "high" {
t.Fatalf("settings = %#v", input.Settings)
}
if input.Section != agentservice.ComposerOptionsSectionCore {
t.Fatalf("section = %q, want core", input.Section)
}
if !input.WaitForFreshModelCatalog {
t.Fatal("waitForFreshModelCatalog = false, want true")
}
return agentservice.ComposerOptions{
Capabilities: []string{"imageInput", "planMode", "browserUse"},
Commands: []agentservice.ComposerCommandOption{{
Name: "memory",
Description: "Manage memory",
InputHint: "show | refresh",
}},
EffectiveSettings: input.Settings,
ModelConfig: agentservice.ComposerConfigOption{
Configurable: true,
CurrentValue: "gpt-5",
DefaultValue: "gpt-5",
Options: []agentservice.ComposerConfigOptionValue{{
ID: "gpt-5",
Label: "GPT-5",
Value: "gpt-5",
}},
},
PermissionConfig: agentservice.PermissionConfig{
Configurable: true,
DefaultValue: "auto",
Modes: []agentservice.PermissionModeOption{{
ID: "auto",
Label: "替我审批",
Description: "仅对检测到的风险操作请求批准",
Semantic: agentservice.PermissionModeSemanticAuto,
}},
},
Provider: input.Provider,
ReasoningConfig: agentservice.ComposerConfigOption{
Configurable: true,
CurrentValue: "high",
DefaultValue: "high",
Options: []agentservice.ComposerConfigOptionValue{{
ID: "high",
Label: "高",
Value: "high",
}},
},
ReasoningOptionsByModel: map[string]agentservice.ComposerReasoningProfile{
"gpt-5": {
DefaultValue: "high",
Options: []agentservice.ComposerConfigOptionValue{{
ID: "high", Label: "高", Value: "high",
}},
},
},
RuntimeContext: map[string]any{
"configOptions": []map[string]any{
{
"currentValue": "gpt-5",
"id": "model",
"options": []map[string]string{
{"name": "GPT-5", "value": "gpt-5"},
},
},
},
},
Skills: []agentservice.ComposerSkillOption{{
Name: "architecture-review",
Trigger: "$architecture-review",
SourceKind: "project",
Description: "Review architecture changes",
}},
Behavior: providerregistry.ComposerBehaviorDescriptor{
ModelOptionsAuthoritative: true,
RefreshModelOptionsAfterSettings: true,
PrewarmDraftSession: true,
PlanModeExclusiveWithPermissionMode: true,
},
SlashCommandPolicy: &providerregistry.SlashCommandPolicyDescriptor{
FallbackCommands: []string{"compact", "goal"},
CommandCatalogAuthoritative: true,
CommandEffects: []providerregistry.SlashCommandEffectDescriptor{
{Command: "compact", Effect: providerregistry.SlashCommandEffectSubmitImmediate},
{Command: "goal", Effect: providerregistry.SlashCommandEffectActivateGoalMode},
},
},
}, nil
},
},
}))
recorder := performGeneratedRouteRequest(t, mux, http.MethodPost, "/v1/agent-providers/codex/composer-options", map[string]any{
"cwd": "/workspace/project",
"locale": "zh-CN",
"section": "core",
"waitForFreshModelCatalog": true,
"settings": map[string]any{
"model": "gpt-5",
"permissionModeId": "auto",
"reasoningEffort": "high",
},
})
if recorder.Code != http.StatusOK {
t.Fatalf("status = %d, want %d; body: %s", recorder.Code, http.StatusOK, recorder.Body.String())
}
var response tuttigenerated.AgentProviderComposerOptionsResponse
decodeGeneratedRouteResponse(t, recorder, &response)
if response.Provider != tuttigenerated.WorkspaceAgentProvider("codex") {
t.Fatalf("provider = %q, want codex", response.Provider)
}
if response.Capabilities == nil || !response.Capabilities.ImageInput || !response.Capabilities.PlanMode || !response.Capabilities.BrowserUse || response.Capabilities.ComputerUse {
t.Fatalf("capabilities = %#v", response.Capabilities)
}
if response.EffectiveSettings.Model == nil || *response.EffectiveSettings.Model != "gpt-5" {
t.Fatalf("model = %#v, want gpt-5", response.EffectiveSettings.Model)
}
if response.ModelConfig.CurrentValue == nil || *response.ModelConfig.CurrentValue != "gpt-5" {
t.Fatalf("modelConfig = %#v", response.ModelConfig)
}
if response.PermissionConfig.DefaultValue == nil || *response.PermissionConfig.DefaultValue != "auto" || response.PermissionConfig.Modes[0].Label != "替我审批" {
t.Fatalf("permissionConfig = %#v", response.PermissionConfig)
}
if response.ReasoningConfig.Options[0].Label != "高" {
t.Fatalf("reasoningConfig = %#v", response.ReasoningConfig)
}
if profile, ok := response.ReasoningOptionsByModel["gpt-5"]; !ok || profile.DefaultValue == nil || *profile.DefaultValue != "high" || len(profile.Options) != 1 {
t.Fatalf("reasoningOptionsByModel = %#v", response.ReasoningOptionsByModel)
}
if len(response.Commands) != 1 || response.Commands[0].Name != "memory" || response.Commands[0].Description == nil || *response.Commands[0].Description != "Manage memory" {
t.Fatalf("commands = %#v", response.Commands)
}
if response.RuntimeContext["configOptions"] == nil {
t.Fatalf("runtimeContext = %#v", response.RuntimeContext)
}
if len(response.Skills) != 1 || response.Skills[0].Trigger != "$architecture-review" || response.Skills[0].SourceKind != tuttigenerated.AgentProviderSkillOptionSourceKindProject {
t.Fatalf("skills = %#v", response.Skills)
}
if !response.Behavior.ModelOptionsAuthoritative ||
!response.Behavior.RefreshModelOptionsAfterSettings || !response.Behavior.PrewarmDraftSession ||
!response.Behavior.PlanModeExclusiveWithPermissionMode {
t.Fatalf("behavior = %#v", response.Behavior)
}
if response.SlashCommandPolicy == nil ||
!slices.Equal(response.SlashCommandPolicy.FallbackCommands, []string{"compact", "goal"}) ||
len(response.SlashCommandPolicy.CommandEffects) != 2 ||
response.SlashCommandPolicy.CommandEffects[1].Effect != tuttigenerated.ActivateGoalMode ||
response.SlashCommandPolicy.CommandCatalogAuthoritative == nil ||
!*response.SlashCommandPolicy.CommandCatalogAuthoritative {
t.Fatalf("slashCommandPolicy = %#v", response.SlashCommandPolicy)
}
}
func TestDaemonAPIGeneratedRoutesGetAgentProviderComposerOptionsPassesAgentTargetID(t *testing.T) {
var gotAgentTargetID string
mux := http.NewServeMux()
RegisterRoutes(mux, NewRoutes(DaemonAPI{
AgentSessionService: stubAgentSessionService{
composerOptionsFn: func(_ context.Context, input agentservice.ComposerOptionsInput) (agentservice.ComposerOptions, error) {
gotAgentTargetID = input.AgentTargetID
return agentservice.ComposerOptions{Provider: input.Provider}, nil
},
},
}))
recorder := performGeneratedRouteRequest(t, mux, http.MethodPost, "/v1/agent-providers/codex/composer-options", map[string]any{
"agentTargetId": "shared-agent:abc",
"workspaceId": "ws-1",
})
if recorder.Code != http.StatusOK {
t.Fatalf("status = %d, want %d; body: %s", recorder.Code, http.StatusOK, recorder.Body.String())
}
if gotAgentTargetID != "shared-agent:abc" {
t.Fatalf("agentTargetID = %q, want shared-agent:abc", gotAgentTargetID)
}
}
func TestDaemonAPIGeneratedRoutesGetAgentProviderComposerOptionsLeavesTargetDefaultsToAgentService(t *testing.T) {
mux := http.NewServeMux()
RegisterRoutes(mux, NewRoutes(DaemonAPI{
AgentSessionService: stubAgentSessionService{
composerOptionsFn: func(_ context.Context, input agentservice.ComposerOptionsInput) (agentservice.ComposerOptions, error) {
if input.Settings.Model != "" ||
input.Settings.PermissionModeID != "" ||
input.Settings.ReasoningEffort != "" {
t.Fatalf("settings = %#v", input.Settings)
}
return agentservice.ComposerOptions{
EffectiveSettings: input.Settings,
ModelConfig: agentservice.ComposerConfigOption{
Configurable: true,
CurrentValue: input.Settings.Model,
DefaultValue: input.Settings.Model,
Options: []agentservice.ComposerConfigOptionValue{{
ID: input.Settings.Model,
Label: "GPT-5",
Value: input.Settings.Model,
}},
},
PermissionConfig: agentservice.PermissionConfig{
Configurable: true,
DefaultValue: input.Settings.PermissionModeID,
Modes: []agentservice.PermissionModeOption{{
ID: input.Settings.PermissionModeID,
Label: "Full access",
Semantic: agentservice.PermissionModeSemanticFullAccess,
}},
},
Provider: input.Provider,
ReasoningConfig: agentservice.ComposerConfigOption{
Configurable: true,
CurrentValue: input.Settings.ReasoningEffort,
DefaultValue: input.Settings.ReasoningEffort,
Options: []agentservice.ComposerConfigOptionValue{{
ID: input.Settings.ReasoningEffort,
Label: "High",
Value: input.Settings.ReasoningEffort,
}},
},
RuntimeContext: map[string]any{},
}, nil
},
},
PreferencesService: stubPreferencesService{
getFn: func(context.Context) (preferencesbiz.DesktopPreferences, error) {
return preferencesbiz.DesktopPreferences{
AgentComposerDefaultsByAgentTarget: map[string]preferencesbiz.AgentComposerDefaults{
"local:codex": {
Model: "gpt-5",
PermissionModeID: "full-access",
ReasoningEffort: "high",
},
},
}, nil
},
},
}))
recorder := performGeneratedRouteRequest(t, mux, http.MethodPost, "/v1/agent-providers/codex/composer-options", map[string]any{})
if recorder.Code != http.StatusOK {
t.Fatalf("status = %d, want %d; body: %s", recorder.Code, http.StatusOK, recorder.Body.String())
}
var response tuttigenerated.AgentProviderComposerOptionsResponse
decodeGeneratedRouteResponse(t, recorder, &response)
if response.EffectiveSettings.PermissionModeId != nil {
t.Fatalf("effectiveSettings = %#v", response.EffectiveSettings)
}
if response.PermissionConfig.DefaultValue != nil {
t.Fatalf("permissionConfig = %#v", response.PermissionConfig)
}
}
func TestGeneratedAgentProviderCapabilityOptionsProjectsIconURL(t *testing.T) {
options := generatedAgentProviderCapabilityOptions([]agentservice.ComposerCapabilityOption{{
ID: "connector:github",
Kind: "connector",
Name: "github",
Label: "GitHub",
IconURL: "https://cdn.example.test/tutti/connector-market/github/1.0.0/github-1.0.0-icon.svg",
InstalledAtUnixMS: 1786089600000,
Status: "available",
Invocation: "textTrigger",
}})
if len(options) != 1 || options[0].IconUrl == nil || *options[0].IconUrl != "https://cdn.example.test/tutti/connector-market/github/1.0.0/github-1.0.0-icon.svg" {
t.Fatalf("options = %#v, want connector icon URL", options)
}
if options[0].InstalledAtUnixMs == nil || *options[0].InstalledAtUnixMs != 1786089600000 {
t.Fatalf("options = %#v, want connector installation time", options)
}
}