Files
tutti/services/tuttid/api/daemon_agent_statuses_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

439 lines
18 KiB
Go

package api
import (
"context"
"net/http"
"testing"
"time"
tuttigenerated "github.com/tutti-os/tutti/services/tuttid/api/generated"
agentstatusservice "github.com/tutti-os/tutti/services/tuttid/service/agentstatus"
)
type stubAgentStatusService struct {
listFn func(context.Context, agentstatusservice.ListInput) (agentstatusservice.Snapshot, error)
probeFn func(context.Context, agentstatusservice.ProbeInput) (agentstatusservice.ProbeResult, error)
runActionFn func(context.Context, agentstatusservice.RunActionInput) (agentstatusservice.RunActionResult, error)
catalogFn func(context.Context, string) (agentstatusservice.CodexRuntimeCatalog, error)
selectionFn func(context.Context, agentstatusservice.SetCodexRuntimeSelectionInput) (agentstatusservice.CodexRuntimeCatalog, error)
}
func (s stubAgentStatusService) List(ctx context.Context, input agentstatusservice.ListInput) (agentstatusservice.Snapshot, error) {
if s.listFn == nil {
return agentstatusservice.Snapshot{}, nil
}
return s.listFn(ctx, input)
}
func (s stubAgentStatusService) Probe(ctx context.Context, input agentstatusservice.ProbeInput) (agentstatusservice.ProbeResult, error) {
if s.probeFn == nil {
return agentstatusservice.ProbeResult{}, nil
}
return s.probeFn(ctx, input)
}
func (s stubAgentStatusService) RunAction(ctx context.Context, input agentstatusservice.RunActionInput) (agentstatusservice.RunActionResult, error) {
if s.runActionFn == nil {
return agentstatusservice.RunActionResult{}, nil
}
return s.runActionFn(ctx, input)
}
func (s stubAgentStatusService) GetCodexRuntimeCatalog(ctx context.Context, provider string) (agentstatusservice.CodexRuntimeCatalog, error) {
if s.catalogFn == nil {
return agentstatusservice.CodexRuntimeCatalog{}, nil
}
return s.catalogFn(ctx, provider)
}
func (s stubAgentStatusService) SetCodexRuntimeSelection(ctx context.Context, input agentstatusservice.SetCodexRuntimeSelectionInput) (agentstatusservice.CodexRuntimeCatalog, error) {
if s.selectionFn == nil {
return agentstatusservice.CodexRuntimeCatalog{}, nil
}
return s.selectionFn(ctx, input)
}
func TestDaemonAPIMapsCodexRuntimeCatalog(t *testing.T) {
capturedAt := time.Date(2026, 7, 28, 8, 0, 0, 0, time.UTC)
api := DaemonAPI{AgentStatusService: stubAgentStatusService{
catalogFn: func(_ context.Context, provider string) (agentstatusservice.CodexRuntimeCatalog, error) {
if provider != "codex" {
t.Fatalf("provider = %q, want codex", provider)
}
return agentstatusservice.CodexRuntimeCatalog{
CapturedAt: capturedAt,
Provider: "codex",
Revision: "catalog-revision",
Selection: agentstatusservice.CodexRuntimeSelection{State: agentstatusservice.CodexRuntimeSelectionSelectionRequired},
Candidates: []agentstatusservice.CodexRuntimeCatalogCandidate{{
ID: "candidate", LauncherPath: "/opt/bun/bin/codex", Sources: []string{"bun_global"}, State: "ready", AppServerReady: true, PackageLayoutOK: true,
}},
}, nil
},
}}
response, err := api.GetAgentProviderRuntimeCandidates(context.Background(), tuttigenerated.GetAgentProviderRuntimeCandidatesRequestObject{Provider: "codex"})
if err != nil {
t.Fatalf("GetAgentProviderRuntimeCandidates() error = %v", err)
}
got, ok := response.(tuttigenerated.GetAgentProviderRuntimeCandidates200JSONResponse)
if !ok || got.Revision != "catalog-revision" || len(got.Candidates) != 1 || got.Candidates[0].LauncherPath != "/opt/bun/bin/codex" {
t.Fatalf("response = %#v", response)
}
}
func TestDaemonAPIForwardsCodexRuntimeSelection(t *testing.T) {
api := DaemonAPI{AgentStatusService: stubAgentStatusService{
selectionFn: func(_ context.Context, input agentstatusservice.SetCodexRuntimeSelectionInput) (agentstatusservice.CodexRuntimeCatalog, error) {
if input.Provider != "codex" || input.CandidateID != "candidate" || input.Revision != "revision" {
t.Fatalf("input = %#v", input)
}
return agentstatusservice.CodexRuntimeCatalog{Provider: "codex", Revision: "revision", Selection: agentstatusservice.CodexRuntimeSelection{State: agentstatusservice.CodexRuntimeSelectionSelected}}, nil
},
}}
candidateID, revision := "candidate", "revision"
response, err := api.SetAgentProviderRuntimeSelection(context.Background(), tuttigenerated.SetAgentProviderRuntimeSelectionRequestObject{
Provider: "codex",
Body: &tuttigenerated.SetAgentProviderRuntimeSelectionJSONRequestBody{
CandidateId: candidateID,
Revision: revision,
},
})
if err != nil {
t.Fatalf("SetAgentProviderRuntimeSelection() error = %v", err)
}
if _, ok := response.(tuttigenerated.SetAgentProviderRuntimeSelection200JSONResponse); !ok {
t.Fatalf("response = %#v", response)
}
}
func TestDaemonAPIRoutesAgentProviderStatuses(t *testing.T) {
capturedAt := time.Date(2026, 6, 2, 8, 0, 0, 0, time.UTC)
mux := http.NewServeMux()
RegisterRoutes(mux, NewRoutes(DaemonAPI{
AgentStatusService: stubAgentStatusService{
listFn: func(_ context.Context, input agentstatusservice.ListInput) (agentstatusservice.Snapshot, error) {
if len(input.Providers) != 1 || input.Providers[0] != "claude-code" {
t.Fatalf("providers = %#v, want [claude-code]", input.Providers)
}
if !input.ForceRefresh {
t.Fatal("ForceRefresh = false, want true")
}
if !input.IncludeUpdates {
t.Fatal("IncludeUpdates = false, want true")
}
if !input.RefreshUpdates {
t.Fatal("RefreshUpdates = false, want true")
}
updateAvailable := true
return agentstatusservice.Snapshot{
CapturedAt: capturedAt,
Providers: []agentstatusservice.ProviderStatus{{
ActiveAction: &agentstatusservice.ActiveAction{
ID: agentstatusservice.ActionInstall,
Status: "running",
Step: "adapter",
Registry: "https://registry.example.test",
Stdout: "installing adapter\nstill installing",
},
Actions: []agentstatusservice.Action{{
ID: agentstatusservice.ActionRefresh,
Kind: agentstatusservice.ActionKindRefresh,
}},
Auth: agentstatusservice.AuthInfo{
Status: agentstatusservice.AuthUnknown,
},
Availability: agentstatusservice.Availability{
Status: agentstatusservice.AvailabilityUnknown,
},
CLI: agentstatusservice.CLIStatus{
Installed: true,
},
Update: agentstatusservice.UpdateStatus{
Capability: agentstatusservice.UpdateCapabilitySupported,
Source: agentstatusservice.UpdateSourceNPM,
CurrentVersion: "1.0.0",
LatestVersion: "1.1.0",
UpdateAvailable: &updateAvailable,
LastCheckedAt: &capturedAt,
},
Provider: "claude-code",
}},
}, nil
},
},
}))
recorder := performGeneratedRouteRequest(t, mux, http.MethodGet, "/v1/agent-providers/status?providers=claude-code&includeUpdates=true&refreshUpdates=true&refresh=true", nil)
if recorder.Code != http.StatusOK {
t.Fatalf("status = %d, want %d; body: %s", recorder.Code, http.StatusOK, recorder.Body.String())
}
var response tuttigenerated.AgentProviderStatusListResponse
decodeGeneratedRouteResponse(t, recorder, &response)
if !response.CapturedAt.Equal(capturedAt) {
t.Fatalf("capturedAt = %s, want %s", response.CapturedAt, capturedAt)
}
if len(response.Providers) != 1 {
t.Fatalf("providers length = %d, want 1", len(response.Providers))
}
if response.Providers[0].Provider != "claude-code" {
t.Fatalf("provider = %q, want claude-code", response.Providers[0].Provider)
}
activeAction := response.Providers[0].ActiveAction
if activeAction == nil {
t.Fatal("activeAction = nil, want install progress")
return
}
if activeAction.Phase != tuttigenerated.AgentProviderActiveActionPhaseInstall {
t.Fatalf("activeAction.phase = %q, want install", activeAction.Phase)
}
if activeAction.Registry == nil || *activeAction.Registry != "https://registry.example.test" {
t.Fatalf("activeAction.registry = %#v, want registry", activeAction.Registry)
}
if len(activeAction.Log) != 2 || activeAction.Log[0] != "installing adapter" {
t.Fatalf("activeAction.log = %#v, want split installer stdout", activeAction.Log)
}
update := response.Providers[0].Update
if update.Capability != tuttigenerated.AgentProviderUpdateCapabilitySupported || update.Source == nil || *update.Source != tuttigenerated.AgentProviderUpdateSourceNpm ||
update.CurrentVersion == nil || *update.CurrentVersion != "1.0.0" || update.LatestVersion == nil || *update.LatestVersion != "1.1.0" ||
update.UpdateAvailable == nil || !*update.UpdateAvailable || update.LastCheckedAt == nil {
t.Fatalf("update = %#v", update)
}
}
func TestDaemonAPIRoutesUnsupportedAgentProviderStatus(t *testing.T) {
capturedAt := time.Date(2026, 6, 2, 8, 0, 0, 0, time.UTC)
mux := http.NewServeMux()
RegisterRoutes(mux, NewRoutes(DaemonAPI{
AgentStatusService: stubAgentStatusService{
listFn: func(_ context.Context, input agentstatusservice.ListInput) (agentstatusservice.Snapshot, error) {
if len(input.Providers) != 1 || input.Providers[0] != "hermes" {
t.Fatalf("providers = %#v, want [hermes]", input.Providers)
}
checkedAt := capturedAt
return agentstatusservice.Snapshot{
CapturedAt: capturedAt,
Providers: []agentstatusservice.ProviderStatus{{
Actions: []agentstatusservice.Action{},
Auth: agentstatusservice.AuthInfo{
Status: agentstatusservice.AuthUnknown,
},
Availability: agentstatusservice.Availability{
CheckedAt: &checkedAt,
ReasonCode: agentstatusservice.DisabledReasonProviderTemporarilyUnsupported,
Status: agentstatusservice.AvailabilityUnsupported,
},
Adapter: agentstatusservice.AdapterStatus{
Installed: false,
},
CLI: agentstatusservice.CLIStatus{
Installed: false,
},
Provider: "hermes",
}},
}, nil
},
},
}))
recorder := performGeneratedRouteRequest(t, mux, http.MethodGet, "/v1/agent-providers/status?providers=hermes", nil)
if recorder.Code != http.StatusOK {
t.Fatalf("status = %d, want %d; body: %s", recorder.Code, http.StatusOK, recorder.Body.String())
}
var response tuttigenerated.AgentProviderStatusListResponse
decodeGeneratedRouteResponse(t, recorder, &response)
if len(response.Providers) != 1 {
t.Fatalf("providers length = %d, want 1", len(response.Providers))
}
status := response.Providers[0]
if status.Provider != tuttigenerated.WorkspaceAgentProvider("hermes") {
t.Fatalf("provider = %q, want hermes", status.Provider)
}
if status.Availability.Status != tuttigenerated.AgentProviderAvailabilityStatusUnsupported {
t.Fatalf("availability = %q, want unsupported", status.Availability.Status)
}
if status.Availability.ReasonCode == nil || *status.Availability.ReasonCode != agentstatusservice.DisabledReasonProviderTemporarilyUnsupported {
t.Fatalf("reasonCode = %#v, want temporarily unsupported", status.Availability.ReasonCode)
}
if status.Cli.Installed || status.Adapter.Installed {
t.Fatalf("status = %#v, want CLI and adapter not installed", status)
}
if len(status.Actions) != 0 {
t.Fatalf("actions = %#v, want none", status.Actions)
}
}
func TestDaemonAPIRoutesAgentProviderAuthIncludesAuthMethod(t *testing.T) {
mux := http.NewServeMux()
RegisterRoutes(mux, NewRoutes(DaemonAPI{
AgentStatusService: stubAgentStatusService{
listFn: func(_ context.Context, input agentstatusservice.ListInput) (agentstatusservice.Snapshot, error) {
if len(input.Providers) != 1 || input.Providers[0] != "claude-code" {
t.Fatalf("providers = %#v, want [claude-code]", input.Providers)
}
return agentstatusservice.Snapshot{
Providers: []agentstatusservice.ProviderStatus{{
Actions: []agentstatusservice.Action{},
Auth: agentstatusservice.AuthInfo{
Status: agentstatusservice.AuthAuthenticated,
AccountLabel: "API Usage Billing",
AuthMethod: "apiKey",
},
Availability: agentstatusservice.Availability{
Status: agentstatusservice.AvailabilityReady,
},
CLI: agentstatusservice.CLIStatus{Installed: true, BinaryPath: "/usr/local/bin/claude"},
Adapter: agentstatusservice.AdapterStatus{Installed: true},
Provider: "claude-code",
}},
}, nil
},
},
}))
recorder := performGeneratedRouteRequest(t, mux, http.MethodGet, "/v1/agent-providers/status?providers=claude-code", nil)
if recorder.Code != http.StatusOK {
t.Fatalf("status = %d, want %d; body: %s", recorder.Code, http.StatusOK, recorder.Body.String())
}
var response tuttigenerated.AgentProviderStatusListResponse
decodeGeneratedRouteResponse(t, recorder, &response)
if len(response.Providers) != 1 {
t.Fatalf("providers length = %d, want 1", len(response.Providers))
}
auth := response.Providers[0].Auth
if auth.Status != tuttigenerated.AgentProviderAuthStatusAuthenticated {
t.Fatalf("auth status = %q, want authenticated", auth.Status)
}
if auth.AccountLabel == nil || *auth.AccountLabel != "API Usage Billing" {
t.Fatalf("accountLabel = %#v, want API Usage Billing", auth.AccountLabel)
}
if auth.AuthMethod == nil || *auth.AuthMethod != "apiKey" {
t.Fatalf("authMethod = %#v, want apiKey", auth.AuthMethod)
}
}
func TestDaemonAPIRoutesAgentProviderProbe(t *testing.T) {
checkedAt := time.Date(2026, 6, 2, 8, 0, 0, 0, time.UTC)
mux := http.NewServeMux()
RegisterRoutes(mux, NewRoutes(DaemonAPI{
AgentStatusService: stubAgentStatusService{
probeFn: func(_ context.Context, input agentstatusservice.ProbeInput) (agentstatusservice.ProbeResult, error) {
if input.Provider != "codex" {
t.Fatalf("provider = %q, want codex", input.Provider)
}
return agentstatusservice.ProbeResult{
Provider: "codex",
Status: agentstatusservice.ProbeFailed,
CheckedAt: checkedAt,
ReasonCode: "probe_exited",
Message: "adapter boom",
BinaryPath: "/usr/local/bin/codex-acp",
Command: []string{"/usr/local/bin/codex-acp"},
}, nil
},
},
}))
recorder := performGeneratedRouteRequest(t, mux, http.MethodPost, "/v1/agent-providers/codex/probe", nil)
if recorder.Code != http.StatusOK {
t.Fatalf("status = %d, want %d; body: %s", recorder.Code, http.StatusOK, recorder.Body.String())
}
var response tuttigenerated.AgentProviderProbeResponse
decodeGeneratedRouteResponse(t, recorder, &response)
if response.Status != tuttigenerated.AgentProviderProbeStatusFailed {
t.Fatalf("status = %q, want failed", response.Status)
}
if response.ReasonCode == nil || *response.ReasonCode != "probe_exited" {
t.Fatalf("reasonCode = %#v, want probe_exited", response.ReasonCode)
}
if response.Message == nil || *response.Message != "adapter boom" {
t.Fatalf("message = %#v, want adapter boom", response.Message)
}
}
func TestDaemonAPIRoutesRunAgentProviderAction(t *testing.T) {
completedAt := time.Date(2026, 6, 2, 8, 0, 0, 0, time.UTC)
mux := http.NewServeMux()
RegisterRoutes(mux, NewRoutes(DaemonAPI{
AgentStatusService: stubAgentStatusService{
runActionFn: func(_ context.Context, input agentstatusservice.RunActionInput) (agentstatusservice.RunActionResult, error) {
if input.Provider != "codex" {
t.Fatalf("provider = %q, want codex", input.Provider)
}
if input.ActionID != agentstatusservice.ActionInstall {
t.Fatalf("actionID = %q, want install", input.ActionID)
}
return agentstatusservice.RunActionResult{
Provider: "codex",
ActionID: agentstatusservice.ActionInstall,
Status: agentstatusservice.RunActionFailed,
CompletedAt: completedAt,
ReasonCode: "post_install_probe_failed",
Message: "adapter boom",
}, nil
},
},
}))
recorder := performGeneratedRouteRequest(t, mux, http.MethodPost, "/v1/agent-providers/codex/actions/install/run", nil)
if recorder.Code != http.StatusOK {
t.Fatalf("status = %d, want %d; body: %s", recorder.Code, http.StatusOK, recorder.Body.String())
}
var response tuttigenerated.AgentProviderActionRunResponse
decodeGeneratedRouteResponse(t, recorder, &response)
if response.Status != tuttigenerated.AgentProviderActionRunStatusFailed {
t.Fatalf("status = %q, want failed", response.Status)
}
if response.ReasonCode == nil || *response.ReasonCode != "post_install_probe_failed" {
t.Fatalf("reasonCode = %#v, want post_install_probe_failed", response.ReasonCode)
}
if response.Message == nil || *response.Message != "adapter boom" {
t.Fatalf("message = %#v, want adapter boom", response.Message)
}
}
func TestDaemonAPIRoutesRunAgentProviderUpdateAction(t *testing.T) {
completedAt := time.Date(2026, 6, 2, 8, 0, 0, 0, time.UTC)
mux := http.NewServeMux()
var readinessTrigger string
RegisterRoutes(mux, NewRoutes(DaemonAPI{
AgentStatusService: stubAgentStatusService{
runActionFn: func(_ context.Context, input agentstatusservice.RunActionInput) (agentstatusservice.RunActionResult, error) {
if input.Provider != "tutti-agent" || input.ActionID != agentstatusservice.ActionUpdate {
t.Fatalf("input = %#v, want tutti-agent update", input)
}
return agentstatusservice.RunActionResult{
Provider: "tutti-agent",
ActionID: agentstatusservice.ActionUpdate,
Status: agentstatusservice.RunActionCompleted,
CompletedAt: completedAt,
}, nil
},
},
TuttiAgentReadiness: stubTuttiAgentReadiness{
providerActionCompletedFn: func(result agentstatusservice.RunActionResult) {
if result.Provider == "tutti-agent" && result.Status == agentstatusservice.RunActionCompleted {
readinessTrigger = "provider_action_completed"
}
},
},
}))
recorder := performGeneratedRouteRequest(t, mux, http.MethodPost, "/v1/agent-providers/tutti-agent/actions/update/run", nil)
if recorder.Code != http.StatusOK {
t.Fatalf("status = %d, want %d; body: %s", recorder.Code, http.StatusOK, recorder.Body.String())
}
var response tuttigenerated.AgentProviderActionRunResponse
decodeGeneratedRouteResponse(t, recorder, &response)
if response.ActionID != tuttigenerated.AgentProviderActionIDUpdate || response.Status != tuttigenerated.AgentProviderActionRunStatusCompleted {
t.Fatalf("response = %#v", response)
}
if readinessTrigger != "provider_action_completed" {
t.Fatalf("readiness trigger = %q, want provider_action_completed", readinessTrigger)
}
}