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

272 lines
9.1 KiB
Go

package agentstatus
import (
"context"
"errors"
"os"
"path/filepath"
"strings"
"sync"
"sync/atomic"
"testing"
"time"
"github.com/tutti-os/tutti/packages/agent/daemon/providerregistry"
)
func TestProviderStatusCacheReusesProviderAcrossRequestShapes(t *testing.T) {
var authCalls atomic.Int32
service := testService(func(string) (string, error) {
return "/usr/bin/true", nil
}, map[string]bool{"/home/test/.cursor/cli-config.json": true})
service.StatusCache = NewProviderStatusCache()
service.Now = time.Now
service.RunAuthStatusCommand = func(context.Context, ProviderSpec, string) (AuthInfo, bool) {
authCalls.Add(1)
return AuthInfo{Status: AuthAuthenticated}, true
}
if _, err := service.List(context.Background(), ListInput{Providers: []string{"cursor", "codex"}}); err != nil {
t.Fatalf("first List() error = %v", err)
}
probesAfterFirst := authCalls.Load()
if probesAfterFirst != 2 {
t.Fatalf("first List() auth probes = %d, want 2", probesAfterFirst)
}
if _, err := service.List(context.Background(), ListInput{Providers: []string{"cursor"}}); err != nil {
t.Fatalf("second List() error = %v", err)
}
if got := authCalls.Load(); got != probesAfterFirst {
t.Fatalf("auth probes = %d, want cached %d", got, probesAfterFirst)
}
if _, err := service.List(context.Background(), ListInput{Providers: []string{"cursor"}, ForceRefresh: true}); err != nil {
t.Fatalf("forced List() error = %v", err)
}
if got := authCalls.Load(); got <= probesAfterFirst {
t.Fatalf("auth probes after force refresh = %d, want > %d", got, probesAfterFirst)
}
}
func TestProviderStatusCacheRefreshesRemoteAuthEvidenceEveryFifteenMinutes(t *testing.T) {
cachedAt := time.Unix(100, 0).UTC()
now := cachedAt.Add(14 * time.Minute)
service := Service{
Now: func() time.Time { return now },
RunOutcomes: NewRunOutcomeStore(),
}
spec := ProviderSpec{RemoteAuthProbe: providerregistry.RemoteAuthProbeDescriptor{
Kind: providerregistry.RemoteAuthProbeKindHTTPBearer,
}}
if !service.cachedProviderStatusStillValid(spec, cachedAt, "") {
t.Fatal("remote auth cache expired before fifteen minutes")
}
now = cachedAt.Add(15 * time.Minute)
if service.cachedProviderStatusStillValid(spec, cachedAt, "") {
t.Fatal("remote auth cache remained valid at fifteen minutes")
}
}
func TestForcedProviderStatusRefreshRechecksAuthButReusesStableCLIVersion(t *testing.T) {
tempDir := t.TempDir()
binaryPath := filepath.Join(tempDir, "cursor-agent")
versionCallsPath := filepath.Join(tempDir, "version-calls")
script := "#!/bin/sh\n" +
"if [ \"$1\" = \"--version\" ]; then\n" +
" echo call >> " + versionCallsPath + "\n" +
" echo 'cursor-agent 1.2.3'\n" +
"fi\n"
if err := os.WriteFile(binaryPath, []byte(script), 0o700); err != nil {
t.Fatalf("write cursor test CLI: %v", err)
}
var authCalls atomic.Int32
service := testService(func(string) (string, error) {
return binaryPath, nil
}, map[string]bool{})
service.StatusCache = NewProviderStatusCache()
service.CLIVersionCache = NewCLIVersionCache()
service.RunAuthStatusCommand = func(context.Context, ProviderSpec, string) (AuthInfo, bool) {
authCalls.Add(1)
return AuthInfo{Status: AuthAuthenticated}, true
}
for range 2 {
if _, err := service.List(context.Background(), ListInput{
Providers: []string{"cursor"},
ForceRefresh: true,
}); err != nil {
t.Fatalf("forced List() error = %v", err)
}
}
if got := authCalls.Load(); got != 2 {
t.Fatalf("auth checks = %d, want every forced refresh", got)
}
versionCalls, err := os.ReadFile(versionCallsPath)
if err != nil {
t.Fatalf("read version calls: %v", err)
}
if got := len(strings.Fields(string(versionCalls))); got != 1 {
t.Fatalf("version checks = %d, want stable binary checked once", got)
}
}
func TestProviderStatusCacheCollapsesConcurrentProviderProbes(t *testing.T) {
var authCalls atomic.Int32
service := testService(func(string) (string, error) {
return "/usr/bin/true", nil
}, map[string]bool{"/home/test/.cursor/cli-config.json": true})
service.StatusCache = NewProviderStatusCache()
service.Now = time.Now
service.RunAuthStatusCommand = func(context.Context, ProviderSpec, string) (AuthInfo, bool) {
authCalls.Add(1)
time.Sleep(30 * time.Millisecond)
return AuthInfo{Status: AuthAuthenticated}, true
}
start := make(chan struct{})
var wait sync.WaitGroup
for range 2 {
wait.Add(1)
go func() {
defer wait.Done()
<-start
if _, err := service.List(context.Background(), ListInput{Providers: []string{"cursor"}}); err != nil {
t.Errorf("List() error = %v", err)
}
}()
}
close(start)
wait.Wait()
if got := authCalls.Load(); got != 1 {
t.Fatalf("auth probe calls = %d, want 1", got)
}
}
func TestProviderStatusCacheUsesProbeCompletionTime(t *testing.T) {
times := []time.Time{
time.Unix(100, 0).UTC(),
time.Unix(101, 0).UTC(),
time.Unix(102, 0).UTC(),
time.Unix(103, 0).UTC(),
time.Unix(104, 0).UTC(),
}
var index atomic.Int32
service := testService(func(string) (string, error) {
return "", errors.New("not found")
}, map[string]bool{})
service.StatusCache = NewProviderStatusCache()
service.Now = func() time.Time {
i := int(index.Add(1) - 1)
if i >= len(times) {
return times[len(times)-1]
}
return times[i]
}
snapshot, err := service.List(context.Background(), ListInput{Providers: []string{"cursor"}})
if err != nil {
t.Fatalf("List() error = %v", err)
}
checkedAt := onlyStatus(t, snapshot).Availability.CheckedAt
if checkedAt == nil || !checkedAt.After(times[0]) {
t.Fatalf("checkedAt = %v, want after probe start %v", checkedAt, times[0])
}
}
func TestProviderStatusCacheInvalidatesWhenCredentialsChange(t *testing.T) {
var authCalls atomic.Int32
credentialModifiedAt := time.Unix(100, 0).UTC()
service := testService(func(string) (string, error) {
return "/usr/bin/true", nil
}, map[string]bool{"/home/test/.cursor/cli-config.json": true})
service.StatusCache = NewProviderStatusCache()
service.Now = time.Now
service.FileModTime = func(path string) (time.Time, bool) {
if path == "/home/test/.cursor/cli-config.json" {
return credentialModifiedAt, true
}
return time.Time{}, false
}
service.RunAuthStatusCommand = func(context.Context, ProviderSpec, string) (AuthInfo, bool) {
authCalls.Add(1)
return AuthInfo{Status: AuthAuthenticated}, true
}
for range 2 {
if _, err := service.List(context.Background(), ListInput{Providers: []string{"cursor"}}); err != nil {
t.Fatalf("List() error = %v", err)
}
}
if got := authCalls.Load(); got != 1 {
t.Fatalf("auth probes before credential change = %d, want 1", got)
}
credentialModifiedAt = credentialModifiedAt.Add(time.Second)
if _, err := service.List(context.Background(), ListInput{Providers: []string{"cursor"}}); err != nil {
t.Fatalf("List() after credential change error = %v", err)
}
if got := authCalls.Load(); got != 2 {
t.Fatalf("auth probes after credential change = %d, want 2", got)
}
}
func TestProviderStatusCacheInvalidatesForNewRuntimeAuthEvidence(t *testing.T) {
var authCalls atomic.Int32
service := testService(func(string) (string, error) {
return "/usr/bin/true", nil
}, map[string]bool{"/home/test/.cursor/cli-config.json": true})
service.StatusCache = NewProviderStatusCache()
service.RunOutcomes = NewRunOutcomeStore()
service.Now = time.Now
service.FileModTime = func(string) (time.Time, bool) {
return time.Unix(100, 0), true
}
service.RunAuthStatusCommand = func(context.Context, ProviderSpec, string) (AuthInfo, bool) {
authCalls.Add(1)
return AuthInfo{Status: AuthAuthenticated, AuthMethod: "oauth"}, true
}
configured, err := service.List(context.Background(), ListInput{Providers: []string{"cursor"}})
if err != nil {
t.Fatalf("initial List() error = %v", err)
}
if got := onlyStatus(t, configured).Auth.Status; got != AuthConfigured {
t.Fatalf("initial auth = %q, want configured", got)
}
service.RunOutcomes.RecordSuccess("cursor")
authenticated, err := service.List(context.Background(), ListInput{Providers: []string{"cursor"}})
if err != nil {
t.Fatalf("List() after runtime success error = %v", err)
}
if got := onlyStatus(t, authenticated).Auth.Status; got != AuthAuthenticated {
t.Fatalf("auth after runtime success = %q, want authenticated", got)
}
if got := authCalls.Load(); got != 2 {
t.Fatalf("auth probes after runtime evidence = %d, want cache invalidated", got)
}
}
func TestProviderStatusCacheDoesNotCacheFailedCodexRepairEvidence(t *testing.T) {
home := t.TempDir()
writeCodexBunInstall(t, home, codexBunReadyLauncher)
service := probeTestService(home)
service.StatusCache = NewProviderStatusCache()
var probes atomic.Int32
service.CodexProtocolProbe = func(context.Context, []string, []string) CodexProbeEvidence {
probes.Add(1)
return CodexProbeEvidence{CommandStarted: true, Category: "handshake_timeout"}
}
service.RunAuthStatusCommand = func(context.Context, ProviderSpec, string) (AuthInfo, bool) {
return AuthInfo{Status: AuthAuthenticated}, true
}
for range 2 {
if _, err := service.List(context.Background(), ListInput{Providers: []string{"codex"}}); err != nil {
t.Fatalf("List() error = %v", err)
}
}
if got := probes.Load(); got != 2 {
t.Fatalf("failed Codex probes = %d, want fresh probe for each List", got)
}
}