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 }