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) }