Files
kandev/apps/backend/internal/task/service/service_test.go
T
2026-01-16 01:59:00 +00:00

636 lines
19 KiB
Go

package service
import (
"context"
"sync"
"testing"
"time"
"github.com/kandev/kandev/internal/agent/worktree"
"github.com/kandev/kandev/internal/common/logger"
"github.com/kandev/kandev/internal/events/bus"
"github.com/kandev/kandev/internal/task/models"
"github.com/kandev/kandev/internal/task/repository"
v1 "github.com/kandev/kandev/pkg/api/v1"
)
// MockEventBus implements bus.EventBus for testing
type MockEventBus struct {
mu sync.Mutex
publishedEvents []*bus.Event
closed bool
}
func NewMockEventBus() *MockEventBus {
return &MockEventBus{
publishedEvents: make([]*bus.Event, 0),
}
}
func (m *MockEventBus) Publish(ctx context.Context, subject string, event *bus.Event) error {
m.mu.Lock()
defer m.mu.Unlock()
m.publishedEvents = append(m.publishedEvents, event)
return nil
}
func (m *MockEventBus) Subscribe(subject string, handler bus.EventHandler) (bus.Subscription, error) {
return nil, nil
}
func (m *MockEventBus) QueueSubscribe(subject, queue string, handler bus.EventHandler) (bus.Subscription, error) {
return nil, nil
}
func (m *MockEventBus) Request(ctx context.Context, subject string, event *bus.Event, timeout time.Duration) (*bus.Event, error) {
return nil, nil
}
func (m *MockEventBus) Close() {
m.mu.Lock()
defer m.mu.Unlock()
m.closed = true
}
func (m *MockEventBus) IsConnected() bool {
return !m.closed
}
func (m *MockEventBus) GetPublishedEvents() []*bus.Event {
m.mu.Lock()
defer m.mu.Unlock()
return m.publishedEvents
}
func (m *MockEventBus) ClearEvents() {
m.mu.Lock()
defer m.mu.Unlock()
m.publishedEvents = make([]*bus.Event, 0)
}
func createTestService(t *testing.T) (*Service, *MockEventBus, repository.Repository) {
t.Helper()
tmpDir := t.TempDir()
repo, err := repository.NewSQLiteRepository(tmpDir + "/test.db")
if err != nil {
t.Fatalf("failed to create test repository: %v", err)
}
if _, err := worktree.NewSQLiteStore(repo.DB()); err != nil {
t.Fatalf("failed to init worktree store: %v", err)
}
t.Cleanup(func() {
if err := repo.Close(); err != nil {
t.Errorf("failed to close repo: %v", err)
}
})
eventBus := NewMockEventBus()
log, _ := logger.NewLogger(logger.LoggingConfig{Level: "error", Format: "json", OutputPath: "stdout"})
svc := NewService(repo, eventBus, log, RepositoryDiscoveryConfig{})
return svc, eventBus, repo
}
// Task tests
func TestService_CreateTask(t *testing.T) {
svc, eventBus, repo := createTestService(t)
ctx := context.Background()
// Create board and column first
_ = repo.CreateWorkspace(ctx, &models.Workspace{ID: "ws-1", Name: "Workspace"})
board := &models.Board{ID: "board-123", WorkspaceID: "ws-1", Name: "Test Board"}
_ = repo.CreateBoard(ctx, board)
column := &models.Column{ID: "col-123", BoardID: "board-123", Name: "To Do", State: v1.TaskStateTODO}
_ = repo.CreateColumn(ctx, column)
repository := &models.Repository{ID: "repo-123", WorkspaceID: "ws-1", Name: "Test Repo"}
_ = repo.CreateRepository(ctx, repository)
req := &CreateTaskRequest{
WorkspaceID: "ws-1",
BoardID: "board-123",
ColumnID: "col-123",
Title: "Test Task",
Description: "A test task",
Priority: 5,
Repositories: []TaskRepositoryInput{
{
RepositoryID: "repo-123",
BaseBranch: "main",
},
},
}
task, err := svc.CreateTask(ctx, req)
if err != nil {
t.Fatalf("failed to create task: %v", err)
}
if task.ID == "" {
t.Error("expected task ID to be set")
}
if task.Title != "Test Task" {
t.Errorf("expected title 'Test Task', got %s", task.Title)
}
if task.State != v1.TaskStateCreated {
t.Errorf("expected state CREATED, got %s", task.State)
}
// Check event was published
events := eventBus.GetPublishedEvents()
if len(events) != 1 {
t.Errorf("expected 1 event, got %d", len(events))
}
if events[0].Type != "task.created" {
t.Errorf("expected event type 'task.created', got %s", events[0].Type)
}
}
func TestService_GetTask(t *testing.T) {
svc, _, repo := createTestService(t)
ctx := context.Background()
// Create required entities
setupTestTask(t, repo)
retrieved, err := svc.GetTask(ctx, "task-123")
if err != nil {
t.Fatalf("failed to get task: %v", err)
}
if retrieved.Title != "Test Task" {
t.Errorf("expected title 'Test Task', got %s", retrieved.Title)
}
}
func TestService_GetTaskNotFound(t *testing.T) {
svc, _, _ := createTestService(t)
ctx := context.Background()
_, err := svc.GetTask(ctx, "nonexistent")
if err == nil {
t.Error("expected error for nonexistent task")
}
}
func TestService_UpdateTask(t *testing.T) {
svc, eventBus, repo := createTestService(t)
ctx := context.Background()
_ = repo.CreateWorkspace(ctx, &models.Workspace{ID: "ws-1", Name: "Workspace"})
_ = repo.CreateBoard(ctx, &models.Board{ID: "board-123", WorkspaceID: "ws-1", Name: "Board"})
_ = repo.CreateColumn(ctx, &models.Column{ID: "col-123", BoardID: "board-123", Name: "Column", State: v1.TaskStateTODO})
_ = repo.CreateTask(ctx, &models.Task{ID: "task-123", WorkspaceID: "ws-1", BoardID: "board-123", ColumnID: "col-123", Title: "Original"})
eventBus.ClearEvents()
newTitle := "Updated Title"
req := &UpdateTaskRequest{Title: &newTitle}
updated, err := svc.UpdateTask(ctx, "task-123", req)
if err != nil {
t.Fatalf("failed to update task: %v", err)
}
if updated.Title != "Updated Title" {
t.Errorf("expected title 'Updated Title', got %s", updated.Title)
}
// Check event was published
events := eventBus.GetPublishedEvents()
if len(events) != 1 {
t.Errorf("expected 1 event, got %d", len(events))
}
if events[0].Type != "task.updated" {
t.Errorf("expected event type 'task.updated', got %s", events[0].Type)
}
}
func TestService_DeleteTask(t *testing.T) {
svc, eventBus, repo := createTestService(t)
ctx := context.Background()
_ = repo.CreateWorkspace(ctx, &models.Workspace{ID: "ws-1", Name: "Workspace"})
_ = repo.CreateBoard(ctx, &models.Board{ID: "board-123", WorkspaceID: "ws-1", Name: "Board"})
_ = repo.CreateColumn(ctx, &models.Column{ID: "col-123", BoardID: "board-123", Name: "Column", State: v1.TaskStateTODO})
_ = repo.CreateTask(ctx, &models.Task{ID: "task-123", WorkspaceID: "ws-1", BoardID: "board-123", ColumnID: "col-123", Title: "Test"})
eventBus.ClearEvents()
err := svc.DeleteTask(ctx, "task-123")
if err != nil {
t.Fatalf("failed to delete task: %v", err)
}
// Verify task is deleted
_, err = svc.GetTask(ctx, "task-123")
if err == nil {
t.Error("expected task to be deleted")
}
// Check event was published
events := eventBus.GetPublishedEvents()
if len(events) != 1 {
t.Errorf("expected 1 event, got %d", len(events))
}
}
func TestService_ListTasks(t *testing.T) {
svc, _, repo := createTestService(t)
ctx := context.Background()
_ = repo.CreateWorkspace(ctx, &models.Workspace{ID: "ws-1", Name: "Workspace"})
_ = repo.CreateBoard(ctx, &models.Board{ID: "board-123", WorkspaceID: "ws-1", Name: "Board"})
_ = repo.CreateColumn(ctx, &models.Column{ID: "col-123", BoardID: "board-123", Name: "Column", State: v1.TaskStateTODO})
_ = repo.CreateTask(ctx, &models.Task{ID: "task-1", WorkspaceID: "ws-1", BoardID: "board-123", ColumnID: "col-123", Title: "Task 1"})
_ = repo.CreateTask(ctx, &models.Task{ID: "task-2", WorkspaceID: "ws-1", BoardID: "board-123", ColumnID: "col-123", Title: "Task 2"})
tasks, err := svc.ListTasks(ctx, "board-123")
if err != nil {
t.Fatalf("failed to list tasks: %v", err)
}
if len(tasks) != 2 {
t.Errorf("expected 2 tasks, got %d", len(tasks))
}
}
func TestService_UpdateTaskState(t *testing.T) {
svc, eventBus, repo := createTestService(t)
ctx := context.Background()
_ = repo.CreateWorkspace(ctx, &models.Workspace{ID: "ws-1", Name: "Workspace"})
_ = repo.CreateBoard(ctx, &models.Board{ID: "board-123", WorkspaceID: "ws-1", Name: "Board"})
_ = repo.CreateColumn(ctx, &models.Column{ID: "col-123", BoardID: "board-123", Name: "Column", State: v1.TaskStateTODO})
task := &models.Task{ID: "task-123", WorkspaceID: "ws-1", BoardID: "board-123", ColumnID: "col-123", Title: "Test", State: v1.TaskStateTODO}
_ = repo.CreateTask(ctx, task)
eventBus.ClearEvents()
updated, err := svc.UpdateTaskState(ctx, "task-123", v1.TaskStateInProgress)
if err != nil {
t.Fatalf("failed to update task state: %v", err)
}
if updated.State != v1.TaskStateInProgress {
t.Errorf("expected state IN_PROGRESS, got %s", updated.State)
}
// Check event was published
events := eventBus.GetPublishedEvents()
if len(events) != 1 {
t.Errorf("expected 1 event, got %d", len(events))
}
if events[0].Type != "task.state_changed" {
t.Errorf("expected event type 'task.state_changed', got %s", events[0].Type)
}
}
func TestService_MoveTask(t *testing.T) {
svc, eventBus, repo := createTestService(t)
ctx := context.Background()
_ = repo.CreateWorkspace(ctx, &models.Workspace{ID: "ws-1", Name: "Workspace"})
_ = repo.CreateBoard(ctx, &models.Board{ID: "board-123", WorkspaceID: "ws-1", Name: "Board"})
// Create source column
_ = repo.CreateColumn(ctx, &models.Column{ID: "col-123", BoardID: "board-123", Name: "Todo", State: v1.TaskStateTODO})
// Create destination column with different state
_ = repo.CreateColumn(ctx, &models.Column{ID: "col-done", BoardID: "board-123", Name: "Done", State: v1.TaskStateCompleted})
task := &models.Task{ID: "task-123", WorkspaceID: "ws-1", BoardID: "board-123", ColumnID: "col-123", Title: "Test", State: v1.TaskStateTODO}
_ = repo.CreateTask(ctx, task)
eventBus.ClearEvents()
moved, err := svc.MoveTask(ctx, "task-123", "board-123", "col-done", 0)
if err != nil {
t.Fatalf("failed to move task: %v", err)
}
if moved.ColumnID != "col-done" {
t.Errorf("expected column 'col-done', got %s", moved.ColumnID)
}
if moved.State != v1.TaskStateCompleted {
t.Errorf("expected state COMPLETED, got %s", moved.State)
}
}
// Board tests
func TestService_CreateBoard(t *testing.T) {
svc, _, repo := createTestService(t)
ctx := context.Background()
_ = repo.CreateWorkspace(ctx, &models.Workspace{ID: "ws-1", Name: "Workspace"})
req := &CreateBoardRequest{
WorkspaceID: "ws-1",
Name: "Test Board",
Description: "A test board",
}
board, err := svc.CreateBoard(ctx, req)
if err != nil {
t.Fatalf("failed to create board: %v", err)
}
if board.ID == "" {
t.Error("expected board ID to be set")
}
if board.Name != "Test Board" {
t.Errorf("expected name 'Test Board', got %s", board.Name)
}
}
func TestService_GetBoard(t *testing.T) {
svc, _, repo := createTestService(t)
ctx := context.Background()
_ = repo.CreateWorkspace(ctx, &models.Workspace{ID: "ws-1", Name: "Workspace"})
board := &models.Board{ID: "board-123", WorkspaceID: "ws-1", Name: "Test Board"}
_ = repo.CreateBoard(ctx, board)
retrieved, err := svc.GetBoard(ctx, "board-123")
if err != nil {
t.Fatalf("failed to get board: %v", err)
}
if retrieved.Name != "Test Board" {
t.Errorf("expected name 'Test Board', got %s", retrieved.Name)
}
}
func TestService_UpdateBoard(t *testing.T) {
svc, _, repo := createTestService(t)
ctx := context.Background()
_ = repo.CreateWorkspace(ctx, &models.Workspace{ID: "ws-1", Name: "Workspace"})
board := &models.Board{ID: "board-123", WorkspaceID: "ws-1", Name: "Original"}
_ = repo.CreateBoard(ctx, board)
newName := "Updated"
req := &UpdateBoardRequest{Name: &newName}
updated, err := svc.UpdateBoard(ctx, "board-123", req)
if err != nil {
t.Fatalf("failed to update board: %v", err)
}
if updated.Name != "Updated" {
t.Errorf("expected name 'Updated', got %s", updated.Name)
}
}
func TestService_DeleteBoard(t *testing.T) {
svc, _, repo := createTestService(t)
ctx := context.Background()
_ = repo.CreateWorkspace(ctx, &models.Workspace{ID: "ws-1", Name: "Workspace"})
board := &models.Board{ID: "board-123", WorkspaceID: "ws-1", Name: "Test Board"}
_ = repo.CreateBoard(ctx, board)
err := svc.DeleteBoard(ctx, "board-123")
if err != nil {
t.Fatalf("failed to delete board: %v", err)
}
_, err = svc.GetBoard(ctx, "board-123")
if err == nil {
t.Error("expected board to be deleted")
}
}
func TestService_ListBoards(t *testing.T) {
svc, _, repo := createTestService(t)
ctx := context.Background()
_ = repo.CreateWorkspace(ctx, &models.Workspace{ID: "ws-1", Name: "Workspace"})
_ = repo.CreateBoard(ctx, &models.Board{ID: "board-1", WorkspaceID: "ws-1", Name: "Board 1"})
_ = repo.CreateBoard(ctx, &models.Board{ID: "board-2", WorkspaceID: "ws-1", Name: "Board 2"})
boards, err := svc.ListBoards(ctx, "ws-1")
if err != nil {
t.Fatalf("failed to list boards: %v", err)
}
if len(boards) != 2 {
t.Errorf("expected 2 boards, got %d", len(boards))
}
}
// Column tests
func TestService_CreateColumn(t *testing.T) {
svc, _, repo := createTestService(t)
ctx := context.Background()
_ = repo.CreateWorkspace(ctx, &models.Workspace{ID: "ws-1", Name: "Workspace"})
board := &models.Board{ID: "board-123", WorkspaceID: "ws-1", Name: "Test Board"}
_ = repo.CreateBoard(ctx, board)
req := &CreateColumnRequest{
BoardID: "board-123",
Name: "To Do",
Position: 0,
State: v1.TaskStateTODO,
}
column, err := svc.CreateColumn(ctx, req)
if err != nil {
t.Fatalf("failed to create column: %v", err)
}
if column.ID == "" {
t.Error("expected column ID to be set")
}
if column.Name != "To Do" {
t.Errorf("expected name 'To Do', got %s", column.Name)
}
}
func TestService_GetColumn(t *testing.T) {
svc, _, repo := createTestService(t)
ctx := context.Background()
_ = repo.CreateWorkspace(ctx, &models.Workspace{ID: "ws-1", Name: "Workspace"})
_ = repo.CreateBoard(ctx, &models.Board{ID: "board-123", WorkspaceID: "ws-1", Name: "Board"})
column := &models.Column{ID: "col-123", BoardID: "board-123", Name: "To Do"}
_ = repo.CreateColumn(ctx, column)
retrieved, err := svc.GetColumn(ctx, "col-123")
if err != nil {
t.Fatalf("failed to get column: %v", err)
}
if retrieved.Name != "To Do" {
t.Errorf("expected name 'To Do', got %s", retrieved.Name)
}
}
func TestService_ListColumns(t *testing.T) {
svc, _, repo := createTestService(t)
ctx := context.Background()
_ = repo.CreateWorkspace(ctx, &models.Workspace{ID: "ws-1", Name: "Workspace"})
_ = repo.CreateBoard(ctx, &models.Board{ID: "board-123", WorkspaceID: "ws-1", Name: "Board"})
_ = repo.CreateColumn(ctx, &models.Column{ID: "col-1", BoardID: "board-123", Name: "To Do"})
_ = repo.CreateColumn(ctx, &models.Column{ID: "col-2", BoardID: "board-123", Name: "Done"})
columns, err := svc.ListColumns(ctx, "board-123")
if err != nil {
t.Fatalf("failed to list columns: %v", err)
}
if len(columns) != 2 {
t.Errorf("expected 2 columns, got %d", len(columns))
}
}
// Message tests
func setupTestTask(t *testing.T, repo repository.Repository) {
t.Helper()
ctx := context.Background()
_ = repo.CreateWorkspace(ctx, &models.Workspace{ID: "ws-1", Name: "Workspace"})
_ = repo.CreateBoard(ctx, &models.Board{ID: "board-123", WorkspaceID: "ws-1", Name: "Board"})
_ = repo.CreateColumn(ctx, &models.Column{ID: "col-123", BoardID: "board-123", Name: "Column", State: v1.TaskStateTODO})
_ = repo.CreateTask(ctx, &models.Task{ID: "task-123", WorkspaceID: "ws-1", BoardID: "board-123", ColumnID: "col-123", Title: "Test Task"})
}
func setupTestSession(t *testing.T, repo repository.Repository) string {
t.Helper()
ctx := context.Background()
session := &models.TaskSession{
ID: "session-123",
TaskID: "task-123",
AgentProfileID: "profile-123",
State: models.TaskSessionStateStarting,
}
_ = repo.CreateTaskSession(ctx, session)
return session.ID
}
func TestService_CreateMessage(t *testing.T) {
svc, eventBus, repo := createTestService(t)
ctx := context.Background()
// Create a task first
setupTestTask(t, repo)
sessionID := setupTestSession(t, repo)
eventBus.ClearEvents()
req := &CreateMessageRequest{
TaskSessionID: sessionID,
Content: "This is a test comment",
AuthorType: "user",
AuthorID: "user-123",
}
comment, err := svc.CreateMessage(ctx, req)
if err != nil {
t.Fatalf("failed to create comment: %v", err)
}
if comment.ID == "" {
t.Error("expected comment ID to be set")
}
if comment.Content != "This is a test comment" {
t.Errorf("expected content 'This is a test comment', got %s", comment.Content)
}
if comment.AuthorType != models.MessageAuthorUser {
t.Errorf("expected author type 'user', got %s", comment.AuthorType)
}
// Check event was published
events := eventBus.GetPublishedEvents()
if len(events) != 1 {
t.Errorf("expected 1 event, got %d", len(events))
}
if events[0].Type != "message.added" {
t.Errorf("expected event type 'message.added', got %s", events[0].Type)
}
}
func TestService_CreateAgentMessage(t *testing.T) {
svc, _, repo := createTestService(t)
ctx := context.Background()
setupTestTask(t, repo)
sessionID := setupTestSession(t, repo)
req := &CreateMessageRequest{
TaskSessionID: sessionID,
Content: "What should I do next?",
AuthorType: "agent",
AuthorID: "agent-123",
RequestsInput: true,
}
comment, err := svc.CreateMessage(ctx, req)
if err != nil {
t.Fatalf("failed to create comment: %v", err)
}
if comment.AuthorType != models.MessageAuthorAgent {
t.Errorf("expected author type 'agent', got %s", comment.AuthorType)
}
if !comment.RequestsInput {
t.Error("expected RequestsInput to be true")
}
}
func TestService_CreateMessageSessionNotFound(t *testing.T) {
svc, _, _ := createTestService(t)
ctx := context.Background()
req := &CreateMessageRequest{
TaskSessionID: "nonexistent",
Content: "Test comment",
}
_, err := svc.CreateMessage(ctx, req)
if err == nil {
t.Error("expected error for nonexistent task")
}
}
func TestService_GetMessage(t *testing.T) {
svc, _, repo := createTestService(t)
ctx := context.Background()
setupTestTask(t, repo)
sessionID := setupTestSession(t, repo)
comment := &models.Message{ID: "comment-123", TaskSessionID: sessionID, TaskID: "task-123", AuthorType: models.MessageAuthorUser, Content: "Test"}
_ = repo.CreateMessage(ctx, comment)
retrieved, err := svc.GetMessage(ctx, "comment-123")
if err != nil {
t.Fatalf("failed to get comment: %v", err)
}
if retrieved.Content != "Test" {
t.Errorf("expected content 'Test', got %s", retrieved.Content)
}
}
func TestService_ListMessages(t *testing.T) {
svc, _, repo := createTestService(t)
ctx := context.Background()
setupTestTask(t, repo)
sessionID := setupTestSession(t, repo)
_ = repo.CreateMessage(ctx, &models.Message{ID: "comment-1", TaskSessionID: sessionID, TaskID: "task-123", AuthorType: models.MessageAuthorUser, Content: "Comment 1"})
_ = repo.CreateMessage(ctx, &models.Message{ID: "comment-2", TaskSessionID: sessionID, TaskID: "task-123", AuthorType: models.MessageAuthorAgent, Content: "Comment 2"})
comments, err := svc.ListMessages(ctx, sessionID)
if err != nil {
t.Fatalf("failed to list comments: %v", err)
}
if len(comments) != 2 {
t.Errorf("expected 2 comments, got %d", len(comments))
}
}
func TestService_DeleteMessage(t *testing.T) {
svc, _, repo := createTestService(t)
ctx := context.Background()
setupTestTask(t, repo)
sessionID := setupTestSession(t, repo)
comment := &models.Message{ID: "comment-123", TaskSessionID: sessionID, TaskID: "task-123", AuthorType: models.MessageAuthorUser, Content: "Test"}
_ = repo.CreateMessage(ctx, comment)
err := svc.DeleteMessage(ctx, "comment-123")
if err != nil {
t.Fatalf("failed to delete comment: %v", err)
}
_, err = svc.GetMessage(ctx, "comment-123")
if err == nil {
t.Error("expected comment to be deleted")
}
}