yao/agent/robot/executor/standard/host_test.go
Max c4696380f4 feat(robot): implement global phase agent resolution for improved agent configuration
- Introduced a new global phase agent resolver to streamline agent ID retrieval for various robot pipeline phases, enhancing flexibility in agent configuration.
- Updated existing phase agent retrieval logic to prioritize per-robot configurations, falling back to global settings when necessary.
- Enhanced error handling to provide clearer messages when no agent is configured for specific phases.
- Added tests to validate the new resolution logic and ensure proper functionality across different configurations.
2026-03-24 12:11:55 +08:00

231 lines
6.6 KiB
Go

package standard_test
import (
"context"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/yaoapp/yao/agent/robot/executor/standard"
robottypes "github.com/yaoapp/yao/agent/robot/types"
"github.com/yaoapp/yao/agent/testutils"
oauthtypes "github.com/yaoapp/yao/openapi/oauth/types"
)
func hostTestAuth() *oauthtypes.AuthorizedInfo {
return &oauthtypes.AuthorizedInfo{
UserID: "test-user-host",
TeamID: "test-team-host",
}
}
// H1: nil robot
func TestCallHostAgent_NilRobot(t *testing.T) {
e := standard.New()
ctx := robottypes.NewContext(context.Background(), nil)
_, err := e.CallHostAgent(ctx, nil, &robottypes.HostInput{Scenario: "assign"}, "chat-1")
require.Error(t, err)
assert.Contains(t, err.Error(), "robot cannot be nil")
}
// H2: no Host Agent configured
func TestCallHostAgent_NoHostAgent(t *testing.T) {
// Temporarily clear the global resolver so no fallback is available
orig := robottypes.GlobalPhaseAgentResolver
robottypes.GlobalPhaseAgentResolver = nil
defer func() { robottypes.GlobalPhaseAgentResolver = orig }()
e := standard.New()
ctx := robottypes.NewContext(context.Background(), nil)
t.Run("nil config", func(t *testing.T) {
robot := &robottypes.Robot{MemberID: "member-h2a"}
_, err := e.CallHostAgent(ctx, robot, &robottypes.HostInput{Scenario: "assign"}, "chat-1")
require.Error(t, err)
assert.Contains(t, err.Error(), "no Host Agent configured")
})
t.Run("nil resources", func(t *testing.T) {
robot := &robottypes.Robot{
MemberID: "member-h2b",
Config: &robottypes.Config{},
}
_, err := e.CallHostAgent(ctx, robot, &robottypes.HostInput{Scenario: "assign"}, "chat-1")
require.Error(t, err)
assert.Contains(t, err.Error(), "no Host Agent configured")
})
}
// H3: valid JSON response from Host Agent
func TestCallHostAgent_ValidJSONResponse(t *testing.T) {
if testing.Short() {
t.Skip("Requires assistant framework and LLM")
}
testutils.Prepare(t)
defer testutils.Clean(t)
e := standard.New()
ctx := robottypes.NewContext(context.Background(), hostTestAuth())
robot := &robottypes.Robot{
MemberID: "member-h3",
Config: &robottypes.Config{
Resources: &robottypes.Resources{
Phases: map[robottypes.Phase]string{
robottypes.PhaseHost: "tests.host-json",
},
},
},
}
input := &robottypes.HostInput{
Scenario: "assign",
Context: &robottypes.HostContext{
RobotStatus: &robottypes.RobotStatusSnapshot{ActiveCount: 0, MaxQuota: 10},
},
}
output, err := e.CallHostAgent(ctx, robot, input, "chat-h3")
require.NoError(t, err, "CallHostAgent should not error for valid JSON host agent")
require.NotNil(t, output, "output should not be nil")
assert.NotEmpty(t, output.Reply, "reply should not be empty")
assert.Equal(t, robottypes.HostActionConfirm, output.Action,
"action should be 'confirm' for the JSON host agent")
assert.False(t, output.WaitForMore, "wait_for_more should be false")
}
// H4: plain text response (non-JSON fallback)
func TestCallHostAgent_PlaintextFallback(t *testing.T) {
if testing.Short() {
t.Skip("Requires assistant framework and LLM")
}
testutils.Prepare(t)
defer testutils.Clean(t)
e := standard.New()
ctx := robottypes.NewContext(context.Background(), hostTestAuth())
robot := &robottypes.Robot{
MemberID: "member-h4",
Config: &robottypes.Config{
Resources: &robottypes.Resources{
Phases: map[robottypes.Phase]string{
robottypes.PhaseHost: "tests.host-plaintext",
},
},
},
}
input := &robottypes.HostInput{
Scenario: "assign",
Context: &robottypes.HostContext{
RobotStatus: &robottypes.RobotStatusSnapshot{ActiveCount: 0, MaxQuota: 10},
},
}
output, err := e.CallHostAgent(ctx, robot, input, "chat-h4")
require.NoError(t, err, "non-JSON response should fallback gracefully, not error")
require.NotNil(t, output, "output should not be nil")
assert.NotEmpty(t, output.Reply, "reply should contain the plaintext response")
assert.Equal(t, robottypes.HostActionConfirm, output.Action,
"action should fallback to 'confirm' for non-JSON response")
}
// H5: JSON with wrong structure (no action/reply fields)
func TestCallHostAgent_BadJSONStructureFallback(t *testing.T) {
if testing.Short() {
t.Skip("Requires assistant framework and LLM")
}
testutils.Prepare(t)
defer testutils.Clean(t)
e := standard.New()
ctx := robottypes.NewContext(context.Background(), hostTestAuth())
robot := &robottypes.Robot{
MemberID: "member-h5",
Config: &robottypes.Config{
Resources: &robottypes.Resources{
Phases: map[robottypes.Phase]string{
robottypes.PhaseHost: "tests.host-badjson",
},
},
},
}
input := &robottypes.HostInput{
Scenario: "assign",
Context: &robottypes.HostContext{
RobotStatus: &robottypes.RobotStatusSnapshot{ActiveCount: 0, MaxQuota: 10},
},
}
output, err := e.CallHostAgent(ctx, robot, input, "chat-h5")
require.NoError(t, err, "bad JSON structure should not error")
require.NotNil(t, output, "output should not be nil")
// The JSON is valid but has no action/reply fields.
// json.Unmarshal won't error — Action will be zero value ("").
// Verify the output is returned (either with empty action or fallback to confirm).
if output.Action == "" {
assert.Empty(t, output.Action,
"action should be empty when JSON has no action field")
} else {
assert.Equal(t, robottypes.HostActionConfirm, output.Action,
"action should be 'confirm' if fallback is triggered")
}
}
// H6: assistant not found
func TestCallHostAgent_AssistantNotFound(t *testing.T) {
if testing.Short() {
t.Skip("Requires assistant framework initialization")
}
testutils.Prepare(t)
defer testutils.Clean(t)
e := standard.New()
ctx := robottypes.NewContext(context.Background(), hostTestAuth())
robot := &robottypes.Robot{
MemberID: "member-h6",
Config: &robottypes.Config{
Resources: &robottypes.Resources{
Phases: map[robottypes.Phase]string{
robottypes.PhaseHost: "nonexistent-assistant",
},
},
},
}
input := &robottypes.HostInput{Scenario: "assign"}
_, err := e.CallHostAgent(ctx, robot, input, "chat-h6")
require.Error(t, err)
assert.Contains(t, err.Error(), "host agent")
}
// H7: input marshalling verification (pure unit test, no LLM needed)
func TestCallHostAgent_InputMarshalling(t *testing.T) {
input := &robottypes.HostInput{
Scenario: "clarify",
Context: &robottypes.HostContext{
RobotStatus: &robottypes.RobotStatusSnapshot{
ActiveCount: 2,
MaxQuota: 5,
},
AgentReply: "What format?",
},
}
assert.NotEmpty(t, input.Scenario)
assert.NotNil(t, input.Context)
assert.Equal(t, 2, input.Context.RobotStatus.ActiveCount)
}