yao/agent/robot/executor/standard/host_test.go
Max bc4787f857 Update executor to support V2 execution model and enhance event handling
- Implement V2 execution model in the standard executor, simplifying task execution to a single call without validation loops.
- Introduce support for resuming suspended executions, allowing for human input during task processing.
- Enhance event handling by pushing task completion and failure events to the event bus for better tracking and integration.
- Update tests to reflect changes in execution flow and ensure robust handling of task statuses and results.
2026-02-25 18:40:48 +08:00

226 lines
6.3 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) {
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)
}