yao/agent/robot/api/interact_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

171 lines
5.2 KiB
Go

package api
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/yaoapp/yao/agent/robot/types"
)
// AI1-AI3: Interact routing
func TestInteract(t *testing.T) {
t.Run("empty member_id returns error", func(t *testing.T) {
ctx := types.NewContext(nil, nil)
_, err := Interact(ctx, "", &InteractRequest{Message: "test"})
assert.Error(t, err)
assert.Contains(t, err.Error(), "member_id is required")
})
t.Run("nil request returns error", func(t *testing.T) {
ctx := types.NewContext(nil, nil)
_, err := Interact(ctx, "member-1", nil)
assert.Error(t, err)
assert.Contains(t, err.Error(), "interact request is required")
})
t.Run("no manager and no execution_id returns error", func(t *testing.T) {
ctx := types.NewContext(nil, nil)
_, err := Interact(ctx, "member-1", &InteractRequest{Message: "test"})
assert.Error(t, err)
assert.Contains(t, err.Error(), "execution_id is required")
})
}
// AI6: Reply shortcut
func TestReply(t *testing.T) {
t.Run("empty member_id returns error", func(t *testing.T) {
ctx := types.NewContext(nil, nil)
_, err := Reply(ctx, "", "exec-1", "task-1", "hello")
assert.Error(t, err)
assert.Contains(t, err.Error(), "member_id is required")
})
t.Run("routes through Interact", func(t *testing.T) {
ctx := types.NewContext(nil, nil)
// legacyResume accesses the DB model which panics if not initialized.
// Verify the routing reaches legacyResume by catching the expected panic.
assert.Panics(t, func() {
Reply(ctx, "member-1", "exec-1", "task-1", "hello")
}, "should reach legacyResume which requires DB model")
})
}
// AI7: Confirm shortcut
func TestConfirm(t *testing.T) {
t.Run("empty member_id returns error", func(t *testing.T) {
ctx := types.NewContext(nil, nil)
_, err := Confirm(ctx, "", "exec-1", "yes")
assert.Error(t, err)
assert.Contains(t, err.Error(), "member_id is required")
})
t.Run("routes through Interact", func(t *testing.T) {
ctx := types.NewContext(nil, nil)
assert.Panics(t, func() {
Confirm(ctx, "member-1", "exec-1", "yes")
}, "should reach legacyResume which requires DB model")
})
}
// AI8-AI9: CancelExecution
func TestCancelExecution(t *testing.T) {
t.Run("no manager returns error", func(t *testing.T) {
ctx := types.NewContext(nil, nil)
err := CancelExecution(ctx, "exec-1")
assert.Error(t, err)
assert.Contains(t, err.Error(), "cancel not available")
})
}
// AI10-AI12: legacyResume
func TestLegacyResume(t *testing.T) {
t.Run("non-existent execution panics without DB", func(t *testing.T) {
ctx := types.NewContext(nil, nil)
assert.Panics(t, func() {
legacyResume(ctx, &InteractRequest{
ExecutionID: "nonexistent-exec",
Message: "test",
})
}, "should panic because DB model is not initialized")
})
}
// AI1: managerInteract delegates correctly
func TestManagerInteract(t *testing.T) {
t.Run("converts request fields correctly", func(t *testing.T) {
// This would require a running Manager; test the field mapping logic
req := &InteractRequest{
ExecutionID: "exec-ai1",
TaskID: "task-ai1",
Source: types.InteractSourceUI,
Message: "do it",
Action: "confirm",
}
// Verify InteractRequest has all expected fields
assert.Equal(t, "exec-ai1", req.ExecutionID)
assert.Equal(t, "task-ai1", req.TaskID)
assert.Equal(t, types.InteractSourceUI, req.Source)
assert.Equal(t, "do it", req.Message)
assert.Equal(t, "confirm", req.Action)
})
}
// AI2: Interact with execution_id and no manager falls back to legacy
func TestInteractLegacyFallback(t *testing.T) {
t.Run("with execution_id delegates to legacyResume", func(t *testing.T) {
ctx := types.NewContext(nil, nil)
assert.Panics(t, func() {
Interact(ctx, "member-1", &InteractRequest{
ExecutionID: "exec-1",
Message: "resume this",
})
}, "should reach legacyResume which requires DB model")
})
}
// Test InteractResult field mapping
func TestInteractResultFields(t *testing.T) {
result := &InteractResult{
ExecutionID: "exec-test",
Status: "confirmed",
Message: "Done",
ChatID: "chat-test",
Reply: "I'll do it",
WaitForMore: true,
}
assert.Equal(t, "exec-test", result.ExecutionID)
assert.Equal(t, "confirmed", result.Status)
assert.Equal(t, "Done", result.Message)
assert.Equal(t, "chat-test", result.ChatID)
assert.Equal(t, "I'll do it", result.Reply)
assert.True(t, result.WaitForMore)
// Verify zero-value result
empty := &InteractResult{}
assert.Empty(t, empty.ExecutionID)
assert.Empty(t, empty.Status)
assert.False(t, empty.WaitForMore)
}
// Test that legacyResume returns "waiting" on ErrExecutionSuspended
func TestLegacyResumeStatusMapping(t *testing.T) {
// ErrExecutionSuspended handling is tested via the suspend E2E tests.
// Here we verify the InteractResult field structure.
result := &InteractResult{
ExecutionID: "exec-lr",
Status: "waiting",
Message: "Execution suspended again: needs more input",
}
assert.Equal(t, "waiting", result.Status)
assert.Contains(t, result.Message, "suspended")
resultOK := &InteractResult{
ExecutionID: "exec-lr2",
Status: "resumed",
Message: "Execution resumed and completed successfully",
}
require.Equal(t, "resumed", resultOK.Status)
}