yao/agent/robot/executor/standard/executor_test.go
Max 4f9238ac95 Add chat metadata persistence and enhance executor goals injection tests
- Implement TestEnsureChatMetadata to verify that metadata, including robot_id, is correctly persisted in chat records.
- Update EnsureChat method to store metadata from the context when creating chat records.
- Introduce TestExecutorGoalsInjection to validate that pre-confirmed goals are injected into executions from TriggerInput.Data.
- Enhance executor logic to handle goal injection and persistence, ensuring accurate execution titles.
- Modify chat filtering to support chat_id_prefix for improved chat retrieval based on robot identifiers.
2026-02-28 13:57:45 +08:00

281 lines
9.4 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"
"github.com/yaoapp/yao/agent/robot/executor/types"
"github.com/yaoapp/yao/agent/robot/store"
robottypes "github.com/yaoapp/yao/agent/robot/types"
"github.com/yaoapp/yao/agent/testutils"
oauthTypes "github.com/yaoapp/yao/openapi/oauth/types"
)
// ============================================================================
// Executor Persistence Integration Tests
// ============================================================================
func TestExecutorPersistence(t *testing.T) {
if testing.Short() {
t.Skip("Skipping integration test")
}
testutils.Prepare(t)
defer testutils.Clean(t)
t.Run("persists_execution_record_on_start", func(t *testing.T) {
ctx := robottypes.NewContext(context.Background(), &oauthTypes.AuthorizedInfo{
UserID: "user_persist_001",
TeamID: "team_persist_001",
})
robot := createPersistenceTestRobot("member_persist_001", "team_persist_001")
// Create executor with persistence enabled
e := standard.NewWithConfig(types.Config{
SkipPersistence: false,
})
// Execute with simulated failure to ensure we get a result
exec, err := e.Execute(ctx, robot, robottypes.TriggerHuman, "simulate_failure")
require.NoError(t, err)
require.NotNil(t, exec)
// Verify execution record was persisted
s := store.NewExecutionStore()
record, err := s.Get(context.Background(), exec.ID)
require.NoError(t, err)
require.NotNil(t, record)
assert.Equal(t, exec.ID, record.ExecutionID)
assert.Equal(t, "member_persist_001", record.MemberID)
assert.Equal(t, "team_persist_001", record.TeamID)
assert.Equal(t, robottypes.TriggerHuman, record.TriggerType)
assert.Equal(t, robottypes.ExecFailed, record.Status)
assert.Equal(t, "simulated failure", record.Error)
// Cleanup
_ = s.Delete(context.Background(), exec.ID)
})
t.Run("persists_failed_status_with_error", func(t *testing.T) {
ctx := robottypes.NewContext(context.Background(), &oauthTypes.AuthorizedInfo{
UserID: "user_persist_002",
TeamID: "team_persist_002",
})
robot := createPersistenceTestRobot("member_persist_002", "team_persist_002")
e := standard.NewWithConfig(types.Config{
SkipPersistence: false,
})
// Execute with simulated failure
exec, err := e.Execute(ctx, robot, robottypes.TriggerHuman, "simulate_failure")
require.NoError(t, err)
require.NotNil(t, exec)
// Verify the record has failed status with error message
s := store.NewExecutionStore()
record, err := s.Get(context.Background(), exec.ID)
require.NoError(t, err)
require.NotNil(t, record)
assert.Equal(t, robottypes.ExecFailed, record.Status)
assert.Equal(t, "simulated failure", record.Error)
assert.NotNil(t, record.StartTime)
// Cleanup
_ = s.Delete(context.Background(), exec.ID)
})
t.Run("skips_persistence_when_disabled", func(t *testing.T) {
ctx := robottypes.NewContext(context.Background(), &oauthTypes.AuthorizedInfo{
UserID: "user_persist_003",
TeamID: "team_persist_003",
})
robot := createPersistenceTestRobot("member_persist_003", "team_persist_003")
// Create executor with persistence disabled
e := standard.NewWithConfig(types.Config{
SkipPersistence: true,
})
exec, err := e.Execute(ctx, robot, robottypes.TriggerHuman, "simulate_failure")
require.NoError(t, err)
require.NotNil(t, exec)
// Verify no record was created
s := store.NewExecutionStore()
record, err := s.Get(context.Background(), exec.ID)
require.NoError(t, err)
assert.Nil(t, record) // Should not exist
})
}
// ============================================================================
// Goals Injection Tests (Host Agent confirmed goals)
// ============================================================================
// TestExecutorGoalsInjection verifies that when TriggerHuman is used with
// pre-confirmed goals (from Host Agent via /v1/agent/robots/:id/execute),
// the goals are injected directly into exec.Goals before RunGoals runs,
// and are persisted (title updated) so the task list shows the correct title.
func TestExecutorGoalsInjection(t *testing.T) {
if testing.Short() {
t.Skip("Skipping integration test")
}
testutils.Prepare(t)
defer testutils.Clean(t)
t.Run("goals_injected_from_trigger_input_data", func(t *testing.T) {
ctx := robottypes.NewContext(context.Background(), &oauthTypes.AuthorizedInfo{
UserID: "user_goals_inject_001",
TeamID: "team_goals_inject_001",
})
robot := createPersistenceTestRobot("member_goals_inject_001", "team_goals_inject_001")
e := standard.NewWithConfig(types.Config{
SkipPersistence: false,
})
// Simulate Host Agent confirmed goals passed via TriggerInput.Data
triggerInput := &robottypes.TriggerInput{
Data: map[string]interface{}{
"goals": "Create a mecha image with sci-fi style",
"chat_id": "robot_member_goals_inject_001_1234567890",
},
}
exec, err := e.Execute(ctx, robot, robottypes.TriggerHuman, triggerInput)
require.NoError(t, err)
require.NotNil(t, exec)
// Goals should be injected from TriggerInput.Data
require.NotNil(t, exec.Goals, "Goals should be injected from TriggerInput.Data")
assert.Equal(t, "Create a mecha image with sci-fi style", exec.Goals.Content,
"Goals content should match the pre-confirmed goals")
// Verify goals were persisted to the store
s := store.NewExecutionStore()
record, err := s.Get(context.Background(), exec.ID)
require.NoError(t, err)
require.NotNil(t, record)
// Execution name should reflect the goals (not "Preparing...")
assert.NotEmpty(t, exec.Name, "Execution name should be set from goals")
assert.NotEqual(t, "Preparing...", exec.Name, "Name should not be the default placeholder")
// Cleanup
_ = s.Delete(context.Background(), exec.ID)
t.Logf("✓ Goals injected from TriggerInput.Data: goals=%q, name=%q",
exec.Goals.Content, exec.Name)
})
t.Run("empty_goals_falls_through_to_goals_agent", func(t *testing.T) {
// When TriggerInput.Data["goals"] is an empty string, the executor
// does NOT inject pre-confirmed goals and falls through to RunGoals.
// RunGoals will call the Goals Agent (LLM), which may succeed or fail
// depending on the environment. We only verify that the executor returns
// without a panic and that no pre-confirmed goals were force-injected.
//
// This test requires a running AI environment; skip in short mode.
if testing.Short() {
t.Skip("Skipping: requires LLM for RunGoals fallback")
}
ctx := robottypes.NewContext(context.Background(), &oauthTypes.AuthorizedInfo{
UserID: "user_goals_empty_002",
TeamID: "team_goals_empty_002",
})
robot := createPersistenceTestRobot("member_goals_empty_002", "team_goals_empty_002")
e := standard.NewWithConfig(types.Config{
SkipPersistence: true,
})
triggerInput := &robottypes.TriggerInput{
Data: map[string]interface{}{
"goals": "", // empty — should not be injected as pre-confirmed
},
}
exec, err := e.Execute(ctx, robot, robottypes.TriggerHuman, triggerInput)
require.NoError(t, err)
require.NotNil(t, exec)
// If Goals was set, it came from the Goals Agent, NOT from the empty string injection.
// Either nil (agent skipped) or non-nil (agent ran) is acceptable.
if exec.Goals != nil {
assert.NotEmpty(t, exec.Goals.Content,
"If Goals Agent ran, content should be non-empty")
}
t.Logf("✓ Empty goals falls through to Goals Agent (goals=%v)", exec.Goals != nil)
})
t.Run("no_trigger_input_uses_normal_flow", func(t *testing.T) {
ctx := robottypes.NewContext(context.Background(), &oauthTypes.AuthorizedInfo{
UserID: "user_goals_normal_001",
TeamID: "team_goals_normal_001",
})
robot := createPersistenceTestRobot("member_goals_normal_001", "team_goals_normal_001")
e := standard.NewWithConfig(types.Config{
SkipPersistence: true,
})
// No TriggerInput — simulate plain string fallback (old API usage)
exec, err := e.Execute(ctx, robot, robottypes.TriggerHuman, "simulate_failure")
require.NoError(t, err)
require.NotNil(t, exec)
// Goals nil is expected — RunGoals would normally call the LLM
assert.Nil(t, exec.Goals, "Without pre-confirmed goals, Goals should remain nil")
t.Logf("✓ Normal flow (no pre-confirmed goals) proceeds without injection")
})
}
// ============================================================================
// Helper Functions
// ============================================================================
func createPersistenceTestRobot(memberID, teamID string) *robottypes.Robot {
return &robottypes.Robot{
MemberID: memberID,
TeamID: teamID,
DisplayName: "Persistence Test Robot",
Status: robottypes.RobotIdle,
AutonomousMode: true,
Config: &robottypes.Config{
Identity: &robottypes.Identity{
Role: "Test Robot",
Duties: []string{"Testing persistence"},
},
Quota: &robottypes.Quota{
Max: 5,
Queue: 10,
},
Triggers: &robottypes.Triggers{
Intervene: &robottypes.TriggerSwitch{Enabled: true},
},
Resources: &robottypes.Resources{
Phases: map[robottypes.Phase]string{
robottypes.PhaseInspiration: "robot.inspiration",
robottypes.PhaseGoals: "robot.goals",
robottypes.PhaseTasks: "robot.tasks",
robottypes.PhaseRun: "robot.validation",
"validation": "robot.validation",
},
Agents: []string{"experts.text-writer", "experts.data-analyst"},
},
},
}
}