- 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.
368 lines
11 KiB
Go
368 lines
11 KiB
Go
package standard_test
|
|
|
|
import (
|
|
"context"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
"github.com/yaoapp/yao/agent/robot/executor/standard"
|
|
"github.com/yaoapp/yao/agent/robot/types"
|
|
"github.com/yaoapp/yao/agent/testutils"
|
|
)
|
|
|
|
// ============================================================================
|
|
// P0 Inspiration Phase Tests
|
|
// ============================================================================
|
|
|
|
func TestRunInspirationBasic(t *testing.T) {
|
|
if testing.Short() {
|
|
t.Skip("Skipping integration test")
|
|
}
|
|
|
|
testutils.Prepare(t)
|
|
defer testutils.Clean(t)
|
|
|
|
ctx := types.NewContext(context.Background(), testAuth())
|
|
|
|
t.Run("generates inspiration report with clock context", func(t *testing.T) {
|
|
// Create robot with inspiration agent configured
|
|
robot := createTestRobot(t, "robot.inspiration")
|
|
|
|
// Create executor and execution
|
|
exec := createTestExecution(robot, types.TriggerClock)
|
|
|
|
// Run inspiration phase
|
|
e := standard.New()
|
|
err := e.RunInspiration(ctx, exec, nil)
|
|
|
|
require.NoError(t, err)
|
|
require.NotNil(t, exec.Inspiration)
|
|
assert.NotEmpty(t, exec.Inspiration.Content)
|
|
assert.NotNil(t, exec.Inspiration.Clock)
|
|
})
|
|
|
|
t.Run("includes expected markdown sections", func(t *testing.T) {
|
|
robot := createTestRobot(t, "robot.inspiration")
|
|
exec := createTestExecution(robot, types.TriggerClock)
|
|
|
|
e := standard.New()
|
|
err := e.RunInspiration(ctx, exec, nil)
|
|
|
|
require.NoError(t, err)
|
|
content := exec.Inspiration.Content
|
|
|
|
// Verify expected sections in markdown output
|
|
// Note: LLM output is non-deterministic, so we check for likely sections
|
|
hasSection := strings.Contains(content, "##") ||
|
|
strings.Contains(content, "Summary") ||
|
|
strings.Contains(content, "Highlight") ||
|
|
strings.Contains(content, "Recommend")
|
|
|
|
assert.True(t, hasSection, "should contain markdown sections, got: %s", content)
|
|
})
|
|
}
|
|
|
|
func TestRunInspirationClockContext(t *testing.T) {
|
|
if testing.Short() {
|
|
t.Skip("Skipping integration test")
|
|
}
|
|
|
|
testutils.Prepare(t)
|
|
defer testutils.Clean(t)
|
|
|
|
ctx := types.NewContext(context.Background(), testAuth())
|
|
|
|
t.Run("uses clock from trigger input", func(t *testing.T) {
|
|
robot := createTestRobot(t, "robot.inspiration")
|
|
exec := createTestExecution(robot, types.TriggerClock)
|
|
|
|
// Set specific clock context
|
|
specificTime := time.Date(2024, 12, 31, 17, 0, 0, 0, time.UTC)
|
|
exec.Input = &types.TriggerInput{
|
|
Clock: types.NewClockContext(specificTime, "UTC"),
|
|
}
|
|
|
|
e := standard.New()
|
|
err := e.RunInspiration(ctx, exec, nil)
|
|
|
|
require.NoError(t, err)
|
|
require.NotNil(t, exec.Inspiration.Clock)
|
|
|
|
// Clock should match input
|
|
assert.Equal(t, specificTime.Year(), exec.Inspiration.Clock.Year)
|
|
assert.Equal(t, int(specificTime.Month()), exec.Inspiration.Clock.Month)
|
|
assert.Equal(t, specificTime.Day(), exec.Inspiration.Clock.DayOfMonth)
|
|
})
|
|
|
|
t.Run("creates clock context when not provided", func(t *testing.T) {
|
|
robot := createTestRobot(t, "robot.inspiration")
|
|
exec := createTestExecution(robot, types.TriggerClock)
|
|
exec.Input = nil // No input
|
|
|
|
e := standard.New()
|
|
err := e.RunInspiration(ctx, exec, nil)
|
|
|
|
require.NoError(t, err)
|
|
require.NotNil(t, exec.Inspiration.Clock)
|
|
|
|
// Clock should be current time (approximately)
|
|
now := time.Now()
|
|
assert.Equal(t, now.Year(), exec.Inspiration.Clock.Year)
|
|
})
|
|
}
|
|
|
|
func TestRunInspirationRobotIdentity(t *testing.T) {
|
|
if testing.Short() {
|
|
t.Skip("Skipping integration test")
|
|
}
|
|
|
|
testutils.Prepare(t)
|
|
defer testutils.Clean(t)
|
|
|
|
ctx := types.NewContext(context.Background(), testAuth())
|
|
|
|
t.Run("robot identity influences output", func(t *testing.T) {
|
|
// Create robot with specific identity
|
|
robot := &types.Robot{
|
|
MemberID: "test-robot-1",
|
|
TeamID: "test-team-1",
|
|
DisplayName: "Sales Assistant",
|
|
Config: &types.Config{
|
|
Identity: &types.Identity{
|
|
Role: "Sales Assistant",
|
|
Duties: []string{"Track sales metrics", "Prepare weekly reports"},
|
|
Rules: []string{"Focus on actionable insights"},
|
|
},
|
|
Resources: &types.Resources{
|
|
Phases: map[types.Phase]string{
|
|
types.PhaseInspiration: "robot.inspiration",
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
exec := createTestExecution(robot, types.TriggerClock)
|
|
|
|
e := standard.New()
|
|
err := e.RunInspiration(ctx, exec, nil)
|
|
|
|
require.NoError(t, err)
|
|
assert.NotEmpty(t, exec.Inspiration.Content)
|
|
|
|
// The content should be influenced by robot identity
|
|
// (exact content varies due to LLM non-determinism)
|
|
})
|
|
}
|
|
|
|
func TestRunInspirationErrorHandling(t *testing.T) {
|
|
if testing.Short() {
|
|
t.Skip("Skipping integration test")
|
|
}
|
|
|
|
testutils.Prepare(t)
|
|
defer testutils.Clean(t)
|
|
|
|
ctx := types.NewContext(context.Background(), testAuth())
|
|
|
|
t.Run("returns error when robot is nil", func(t *testing.T) {
|
|
exec := &types.Execution{
|
|
ID: "test-exec-1",
|
|
TriggerType: types.TriggerClock,
|
|
}
|
|
// Don't set robot
|
|
|
|
e := standard.New()
|
|
err := e.RunInspiration(ctx, exec, nil)
|
|
|
|
assert.Error(t, err)
|
|
assert.Contains(t, err.Error(), "robot not found")
|
|
})
|
|
|
|
t.Run("returns error when agent not found", func(t *testing.T) {
|
|
robot := &types.Robot{
|
|
MemberID: "test-robot-1",
|
|
TeamID: "test-team-1",
|
|
Config: &types.Config{
|
|
Identity: &types.Identity{Role: "Test"},
|
|
Resources: &types.Resources{
|
|
Phases: map[types.Phase]string{
|
|
types.PhaseInspiration: "non.existent.agent",
|
|
},
|
|
},
|
|
},
|
|
}
|
|
exec := createTestExecution(robot, types.TriggerClock)
|
|
|
|
e := standard.New()
|
|
err := e.RunInspiration(ctx, exec, nil)
|
|
|
|
// Real AgentCaller returns error for non-existent agent
|
|
assert.Error(t, err)
|
|
assert.Contains(t, err.Error(), "call failed")
|
|
})
|
|
}
|
|
|
|
func TestRunInspirationWithDefaultAgent(t *testing.T) {
|
|
if testing.Short() {
|
|
t.Skip("Skipping integration test")
|
|
}
|
|
|
|
testutils.Prepare(t)
|
|
defer testutils.Clean(t)
|
|
|
|
ctx := types.NewContext(context.Background(), testAuth())
|
|
|
|
t.Run("uses global Uses config when per-robot resources not set", func(t *testing.T) {
|
|
robot := &types.Robot{
|
|
MemberID: "test-robot-1",
|
|
TeamID: "test-team-1",
|
|
Config: &types.Config{
|
|
Identity: &types.Identity{Role: "Test Robot"},
|
|
// No Resources configured — falls back to global uses.inspiration
|
|
},
|
|
}
|
|
exec := createTestExecution(robot, types.TriggerClock)
|
|
|
|
e := standard.New()
|
|
err := e.RunInspiration(ctx, exec, nil)
|
|
|
|
// With global Uses configured (agent.yml: uses.inspiration = "robot.inspiration"),
|
|
// the call should succeed. Without global config, it would error.
|
|
if err != nil {
|
|
assert.Contains(t, err.Error(), "call failed")
|
|
}
|
|
})
|
|
}
|
|
|
|
// ============================================================================
|
|
// InputFormatter Tests for P0
|
|
// ============================================================================
|
|
|
|
func TestInputFormatterClockContext(t *testing.T) {
|
|
t.Run("formats clock context correctly", func(t *testing.T) {
|
|
formatter := standard.NewInputFormatter()
|
|
|
|
// Create a specific clock context
|
|
clock := types.NewClockContext(
|
|
time.Date(2024, 12, 31, 17, 30, 0, 0, time.UTC),
|
|
"UTC",
|
|
)
|
|
|
|
robot := &types.Robot{
|
|
Config: &types.Config{
|
|
Identity: &types.Identity{
|
|
Role: "Sales Assistant",
|
|
Duties: []string{"Track metrics", "Send reports"},
|
|
},
|
|
},
|
|
}
|
|
|
|
content := formatter.FormatClockContext(clock, robot)
|
|
|
|
// Verify time context
|
|
assert.Contains(t, content, "Current Time Context")
|
|
assert.Contains(t, content, "2024")
|
|
assert.Contains(t, content, "12")
|
|
assert.Contains(t, content, "31")
|
|
assert.Contains(t, content, "Tuesday") // Dec 31, 2024 is Tuesday
|
|
|
|
// Verify robot identity
|
|
assert.Contains(t, content, "Robot Identity")
|
|
assert.Contains(t, content, "Sales Assistant")
|
|
assert.Contains(t, content, "Track metrics")
|
|
})
|
|
|
|
t.Run("handles nil clock", func(t *testing.T) {
|
|
formatter := standard.NewInputFormatter()
|
|
content := formatter.FormatClockContext(nil, nil)
|
|
assert.Empty(t, content)
|
|
})
|
|
|
|
t.Run("handles nil robot", func(t *testing.T) {
|
|
formatter := standard.NewInputFormatter()
|
|
clock := types.NewClockContext(time.Now(), "")
|
|
content := formatter.FormatClockContext(clock, nil)
|
|
|
|
// Should have time context but no robot identity
|
|
assert.Contains(t, content, "Current Time Context")
|
|
assert.NotContains(t, content, "Robot Identity")
|
|
})
|
|
|
|
t.Run("includes time markers", func(t *testing.T) {
|
|
formatter := standard.NewInputFormatter()
|
|
|
|
// Create a weekend + month start clock context
|
|
// Jan 1, 2028 is Saturday (weekend + month start)
|
|
clock := types.NewClockContext(
|
|
time.Date(2028, 1, 1, 10, 0, 0, 0, time.UTC),
|
|
"UTC",
|
|
)
|
|
|
|
content := formatter.FormatClockContext(clock, nil)
|
|
|
|
assert.Contains(t, content, "Weekend")
|
|
assert.Contains(t, content, "Month Start")
|
|
})
|
|
}
|
|
|
|
// ============================================================================
|
|
// Helper Functions
|
|
// ============================================================================
|
|
|
|
// createTestRobot creates a test robot with specified inspiration agent
|
|
// Includes available expert agents so the Inspiration Agent knows what resources are available
|
|
//
|
|
// Note: The agent IDs listed in Resources.Agents must exist in yao-dev-app/assistants/experts/
|
|
// Current available experts: data-analyst, summarizer, text-writer, web-reader
|
|
func createTestRobot(t *testing.T, agentID string) *types.Robot {
|
|
t.Helper()
|
|
return &types.Robot{
|
|
MemberID: "test-robot-1",
|
|
TeamID: "test-team-1",
|
|
DisplayName: "Test Robot",
|
|
Config: &types.Config{
|
|
Identity: &types.Identity{
|
|
Role: "Test Assistant",
|
|
Duties: []string{"Testing", "Data Analysis", "Report Generation"},
|
|
},
|
|
Resources: &types.Resources{
|
|
Phases: map[types.Phase]string{
|
|
types.PhaseInspiration: agentID,
|
|
},
|
|
// Available expert agents that can be delegated to
|
|
// These IDs correspond to assistants in yao-dev-app/assistants/experts/
|
|
Agents: []string{
|
|
"experts.data-analyst", // Data analysis and insights
|
|
"experts.summarizer", // Content summarization
|
|
"experts.text-writer", // Report and document generation
|
|
"experts.web-reader", // Web content extraction
|
|
},
|
|
},
|
|
// Knowledge base collections (if any)
|
|
KB: &types.KB{
|
|
Collections: []string{"test-knowledge"},
|
|
},
|
|
},
|
|
}
|
|
}
|
|
|
|
// createTestExecution creates a test execution for a robot
|
|
func createTestExecution(robot *types.Robot, trigger types.TriggerType) *types.Execution {
|
|
exec := &types.Execution{
|
|
ID: "test-exec-1",
|
|
MemberID: robot.MemberID,
|
|
TeamID: robot.TeamID,
|
|
TriggerType: trigger,
|
|
StartTime: time.Now(),
|
|
Status: types.ExecRunning,
|
|
Phase: types.PhaseInspiration,
|
|
Input: &types.TriggerInput{
|
|
Clock: types.NewClockContext(time.Now(), ""),
|
|
},
|
|
}
|
|
exec.SetRobot(robot)
|
|
return exec
|
|
}
|