Files
tutti/packages/agent/daemon/runtime/standard_acp_setup_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

369 lines
14 KiB
Go

package agentruntime
import (
"context"
"encoding/json"
"errors"
"strings"
"testing"
"time"
)
func TestRunStandardACPSetupAuthenticatesWithFreshAdvertisedMethod(t *testing.T) {
t.Parallel()
transport := newStandardACPTransport("Example Agent", "setup-session")
transport.conn.authMethods = []map[string]any{{
"id": "oauth-personal", "name": "Log in with Google", "description": "Google account",
}}
transport.conn.requireAuthentication = true
transport.conn.authenticateResult = map[string]any{
"_meta": map[string]any{
"codebuddy.ai/userinfo": map[string]any{
"userId": "user-1", "userName": "Ryan", "userNickname": "Rhinoc", "enterpriseName": "Tutti",
},
},
}
result, err := runStandardACPSetupTest(t, transport, "oauth-personal")
if err != nil {
t.Fatal(err)
}
if result.Status != StandardACPSetupReady || len(result.AuthMethods) != 1 || result.AuthMethods[0].ID != "oauth-personal" {
t.Fatalf("setup result = %#v", result)
}
if result.Account == nil || result.Account.ID != "user-1" || result.Account.DisplayName != "Rhinoc" ||
result.Account.AuthMethodID != "oauth-personal" || result.Account.Organization != "Tutti" {
t.Fatalf("authenticated account = %#v", result.Account)
}
if got := transport.conn.authenticatedMethodID(); got != "oauth-personal" {
t.Fatalf("authenticated method id = %q", got)
}
if containsString(transport.specs[0].Env, "NO_BROWSER=1") {
t.Fatal("interactive authenticate must allow the runtime to open a browser")
}
}
func TestRunStandardACPSetupRejectsUnadvertisedMethod(t *testing.T) {
t.Parallel()
transport := newStandardACPTransport("Example Agent", "setup-session")
transport.conn.authMethods = []map[string]any{{"id": "oauth", "name": "OAuth"}}
_, err := runStandardACPSetupTest(t, transport, "attacker-method")
if !errors.Is(err, ErrACPAuthMethodUnavailable) {
t.Fatalf("error = %v, want ErrACPAuthMethodUnavailable", err)
}
if got := transport.conn.authenticatedMethodID(); got != "" {
t.Fatalf("authenticated method id = %q", got)
}
}
func TestRunStandardACPSetupPreservesExplicitAuthenticationFailureWithoutTextInference(t *testing.T) {
t.Parallel()
transport := newStandardACPTransport("Example Agent", "setup-session")
transport.conn.authMethods = []map[string]any{{"id": "oauth", "name": "OAuth"}}
transport.conn.authenticateError = &acpError{
Code: -32000,
Message: "This account is not supported by this client",
}
result, err := runStandardACPSetupTest(t, transport, "oauth")
if err == nil || !strings.Contains(err.Error(), "This account is not supported by this client") {
t.Fatalf("error = %v, want provider authentication failure", err)
}
if result.Status != StandardACPSetupAuthRequired || len(result.AuthMethods) != 1 {
t.Fatalf("setup result = %#v", result)
}
}
func TestRunStandardACPSetupRejectsTerminalMethodWithoutAuthenticate(t *testing.T) {
t.Parallel()
transport := newStandardACPTransport("Example Agent", "setup-session")
transport.conn.authMethods = []map[string]any{{
"id": "login", "name": "Login with Example account",
"description": "Run `example login` in a terminal",
"type": "terminal", "args": []any{"login"},
}}
transport.conn.requireAuthentication = true
result, err := runStandardACPSetupTest(t, transport, "login")
if !errors.Is(err, ErrACPAuthMethodTerminal) {
t.Fatalf("error = %v, want ErrACPAuthMethodTerminal", err)
}
if result.Status != StandardACPSetupAuthRequired || len(result.AuthMethods) != 1 {
t.Fatalf("setup result = %#v", result)
}
method := result.AuthMethods[0]
if method.Type != "terminal" || len(method.Args) != 1 || method.Args[0] != "login" {
t.Fatalf("terminal auth method = %#v", method)
}
if got := transport.conn.authenticatedMethodID(); got != "" {
t.Fatalf("authenticated method id = %q, want no ACP authenticate call", got)
}
}
func TestRunStandardACPSetupParsesAuthMethodTypeAndArgs(t *testing.T) {
t.Parallel()
initializeResult := json.RawMessage(`{"authMethods":[
{"id":"browser","name":"Browser","description":"d"},
{"id":"login","name":"Terminal login","type":"terminal","args":["login","--device"]},
{"id":"bad","name":"Bad","type":"terminal","args":["", "ok"]}
]}`)
methods := parseStandardACPAuthMethods(initializeResult)
if len(methods) != 3 {
t.Fatalf("auth methods = %#v", methods)
}
if methods[0].Type != "" || methods[0].Args != nil {
t.Fatalf("browser method = %#v", methods[0])
}
if methods[1].Type != "terminal" || strings.Join(methods[1].Args, " ") != "login --device" {
t.Fatalf("terminal method = %#v", methods[1])
}
if methods[2].Type != "terminal" || methods[2].Args != nil {
t.Fatalf("method with invalid args = %#v", methods[2])
}
}
func TestRunStandardACPSetupParsesAuthMethodTerminalMeta(t *testing.T) {
t.Parallel()
// Kimi Code declares the terminal login metadata inside the ACP _meta
// extension, not as top-level type/args fields.
initializeResult := json.RawMessage(`{"authMethods":[
{"id":"login","name":"Login with Kimi account","description":"Run ` + "`kimi login`" + ` in a terminal",
"_meta":{"terminal-auth":{"command":"/opt/kimi/bin/kimi","args":["login"],"label":"Kimi Code Login","env":{},"type":"terminal"}}},
{"id":"both","name":"Both","type":"browser","args":["--top"],
"_meta":{"terminal-auth":{"type":"terminal","args":["login"]}}},
{"id":"weird","name":"Weird","_meta":{"other":{"type":"terminal"}}}
]}`)
methods := parseStandardACPAuthMethods(initializeResult)
if len(methods) != 3 {
t.Fatalf("auth methods = %#v", methods)
}
if methods[0].Type != "terminal" || len(methods[0].Args) != 1 || methods[0].Args[0] != "login" {
t.Fatalf("meta terminal method = %#v", methods[0])
}
if methods[1].Type != "browser" || strings.Join(methods[1].Args, " ") != "--top" {
t.Fatalf("top-level fields must win over _meta = %#v", methods[1])
}
if methods[2].Type != "" || methods[2].Args != nil {
t.Fatalf("unrelated _meta must be ignored = %#v", methods[2])
}
}
func TestRunStandardACPSetupRejectsTerminalMetaMethodWithoutAuthenticate(t *testing.T) {
t.Parallel()
transport := newStandardACPTransport("Example Agent", "setup-session")
transport.conn.authMethods = []map[string]any{{
"id": "login", "name": "Login with Example account",
"description": "Run `example login` in a terminal",
"_meta": map[string]any{
"terminal-auth": map[string]any{"type": "terminal", "args": []any{"login"}},
},
}}
transport.conn.requireAuthentication = true
result, err := runStandardACPSetupTest(t, transport, "login")
if !errors.Is(err, ErrACPAuthMethodTerminal) {
t.Fatalf("error = %v, want ErrACPAuthMethodTerminal", err)
}
if result.Status != StandardACPSetupAuthRequired || len(result.AuthMethods) != 1 {
t.Fatalf("setup result = %#v", result)
}
if got := transport.conn.authenticatedMethodID(); got != "" {
t.Fatalf("authenticated method id = %q, want no ACP authenticate call", got)
}
}
func TestRunStandardACPSetupFlagsSessionWithoutUsableModel(t *testing.T) {
t.Parallel()
// Kimi Code with a saved OAuth token but an unseeded model config:
// session/new succeeds yet advertises zero models, and every prompt would
// fail. The probe must keep the target in auth_required so the gate
// re-offers the terminal login that seeds the config.
transport := newStandardACPTransport("Example Agent", "setup-session")
transport.conn.authMethods = []map[string]any{{
"id": "login", "name": "Login with Example account",
"_meta": map[string]any{
"terminal-auth": map[string]any{"type": "terminal", "args": []any{"login"}},
},
}}
transport.conn.models = map[string]any{"availableModels": []any{}, "currentModelId": ""}
result, err := runStandardACPSetupTest(t, transport, "")
if err != nil {
t.Fatalf("probe without method must not fail hard: %v", err)
}
if result.Status != StandardACPSetupAuthRequired || len(result.AuthMethods) != 1 {
t.Fatalf("setup result = %#v", result)
}
if method := result.AuthMethods[0]; method.Type != "terminal" || len(method.Args) != 1 || method.Args[0] != "login" {
t.Fatalf("terminal auth method = %#v", method)
}
if transport.conn.lastNewSessionParams == nil {
t.Fatal("probe must verify readiness through session/new")
}
}
func TestRunStandardACPSetupReadyWhenTerminalMethodRemainsAdvertised(t *testing.T) {
t.Parallel()
// Kimi Code continues advertising its terminal login method after setup.
// The method catalog is not an authentication verdict: a usable session/new
// result remains the authoritative ready signal.
transport := newStandardACPTransport("Example Agent", "setup-session")
transport.conn.authMethods = []map[string]any{{
"id": "login", "name": "Login with Example account",
"type": "terminal", "args": []any{"login"},
}}
transport.conn.models = map[string]any{
"availableModels": []any{map[string]any{"modelId": "example-model", "name": "Example Model"}},
"currentModelId": "example-model",
}
result, err := runStandardACPSetupTest(t, transport, "")
if err != nil {
t.Fatal(err)
}
if result.Status != StandardACPSetupReady || len(result.AuthMethods) != 1 {
t.Fatalf("setup result = %#v", result)
}
if transport.conn.lastNewSessionParams == nil {
t.Fatal("probe must verify readiness through session/new")
}
if got := transport.conn.authenticatedMethodID(); got != "" {
t.Fatalf("authenticated method id = %q, want no ACP authenticate call", got)
}
}
func TestStandardACPSetupSessionNewAuthCompatibilityIsExact(t *testing.T) {
authErr := &acpCallError{Method: acpMethodNewSession, Err: acpError{
Code: -32000, Message: "authentication required",
}}
if !standardACPSetupSessionNewAuthRejected(authErr) {
t.Fatal("exact setup authentication rejection was not recognized")
}
providerErr := &acpCallError{Method: acpMethodNewSession, Err: acpError{
Code: -32000, Message: "server overloaded; relay returned 524",
}}
if standardACPSetupSessionNewAuthRejected(providerErr) {
t.Fatal("unrelated provider failure must not become setup auth_required")
}
}
func TestRunStandardACPSetupOffersTerminalConfigurationForMissingProvider(t *testing.T) {
t.Parallel()
transport := newStandardACPTransport("Example Agent", "setup-session")
transport.conn.authMethods = []map[string]any{{
"id": "agent-setup", "name": "Configure Example Agent",
"type": "terminal", "args": []any{"--setup"},
}}
transport.conn.newSessionError = &acpError{
Code: -32603,
Message: "Internal error",
Data: json.RawMessage(`{"details":"No LLM provider configured. Run example setup for first-time configuration."}`),
}
result, err := runStandardACPSetupTest(t, transport, "")
if err != nil {
t.Fatalf("probe without method must expose configuration instead of failing: %v", err)
}
if result.Status != StandardACPSetupAuthRequired || len(result.AuthMethods) != 1 {
t.Fatalf("setup result = %#v", result)
}
if method := result.AuthMethods[0]; method.ID != "agent-setup" || method.Type != "terminal" ||
len(method.Args) != 1 || method.Args[0] != "--setup" {
t.Fatalf("terminal configuration method = %#v", method)
}
if transport.conn.lastNewSessionParams == nil {
t.Fatal("probe must inspect session/new before classifying missing configuration")
}
}
func TestStandardACPSetupMissingProviderRequiresAdvertisedTerminalConfiguration(t *testing.T) {
t.Parallel()
err := errors.New("No LLM provider configured")
if standardACPSetupNeedsConfiguration(err, nil) {
t.Fatal("missing provider without a terminal configuration method must remain a runtime failure")
}
if standardACPSetupNeedsConfiguration(errors.New("provider request failed"), []StandardACPAuthMethod{{
ID: "setup", Type: "terminal", Args: []string{"--setup"},
}}) {
t.Fatal("unrelated provider failures must remain runtime failures")
}
}
func TestStandardACPSetupSessionNewTimeoutWithTerminalConfigurationRequiresAuth(t *testing.T) {
t.Parallel()
err := &acpCallTimeoutError{Method: acpMethodNewSession, Timeout: time.Minute}
methods := []StandardACPAuthMethod{{ID: "anthropic", Name: "Anthropic"}, {
ID: "hermes-setup", Name: "Configure Hermes", Type: "terminal", Args: []string{"--setup"},
}}
if !standardACPSetupSessionNewTimedOut(err) {
t.Fatal("session/new timeout was not recognized")
}
if !standardACPSetupHasInteractiveAuth(methods) {
t.Fatal("terminal setup method was not recognized")
}
}
func TestRunStandardACPSetupReadyWithSeededModels(t *testing.T) {
t.Parallel()
transport := newStandardACPTransport("Example Agent", "setup-session")
transport.conn.models = map[string]any{
"availableModels": []any{map[string]any{"modelId": "example-model", "name": "Example Model"}},
"currentModelId": "example-model",
}
result, err := runStandardACPSetupTest(t, transport, "")
if err != nil {
t.Fatal(err)
}
if result.Status != StandardACPSetupReady {
t.Fatalf("setup result = %#v", result)
}
}
func TestACPSessionHasNoUsableModel(t *testing.T) {
t.Parallel()
cases := []struct {
name string
raw string
want bool
}{
{"empty list and no current model", `{"models":{"availableModels":[],"currentModelId":""}}`, true},
{"empty config option model selector", `{"configOptions":[{"id":"model","category":"model","currentValue":"","options":[]}]}`, true},
{"empty categorized model selector", `{"configOptions":[{"id":"model_choice","category":"model","currentValue":null,"options":[]}]}`, true},
{"populated config option model selector", `{"configOptions":[{"id":"model","currentValue":"m","options":[{"name":"M","value":"m"}]}]}`, false},
{"empty options with current config model", `{"configOptions":[{"id":"model","currentValue":"m","options":[]}]}`, false},
{"unrelated empty config option", `{"configOptions":[{"id":"sandbox","currentValue":"","options":[]}]}`, false},
{"populated list", `{"models":{"availableModels":[{"modelId":"m"}],"currentModelId":"m"}}`, false},
{"empty list but current model set", `{"models":{"availableModels":[],"currentModelId":"m"}}`, false},
{"models state without list", `{"models":{"currentModelId":"m"}}`, false},
{"no models state", `{"sessionId":"s"}`, false},
{"null models state", `{"models":null}`, false},
}
for _, tc := range cases {
if got := acpSessionHasNoUsableModel(json.RawMessage(tc.raw)); got != tc.want {
t.Fatalf("%s: acpSessionHasNoUsableModel = %v, want %v", tc.name, got, tc.want)
}
}
}
func runStandardACPSetupTest(t *testing.T, transport *standardACPTransport, methodID string) (StandardACPSetupResult, error) {
t.Helper()
return RunStandardACPSetup(
context.Background(),
StandardACPAdapterConfig{Provider: "acp:example", Name: "example-acp", Command: []string{"example", "--acp"}},
transport,
LegacyHostMetadata(),
standardTestSession("acp:example"),
methodID,
)
}