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
336 lines
14 KiB
Go
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)
|
|
}
|
|
}
|