Files
kandev/apps/backend/internal/task/service/service_tasks_test.go
T
2026-08-09 20:29:48 +01:00

267 lines
9.7 KiB
Go

package service
import (
"context"
"errors"
"os"
"path/filepath"
"testing"
"github.com/kandev/kandev/internal/task/models"
)
func TestDeriveProvisionalTaskTitle(t *testing.T) {
tests := []struct {
name string
description string
want string
}{
{name: "first six normalized words", description: " Ship\n this task with a concise title now ", want: "Ship this task with a concise"},
{name: "short prompt uses every word", description: " Fix login flow ", want: "Fix login flow"},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := deriveProvisionalTaskTitle(tt.description)
if err != nil {
t.Fatalf("derive provisional title: %v", err)
}
if got != tt.want {
t.Fatalf("title = %q, want %q", got, tt.want)
}
})
}
if _, err := deriveProvisionalTaskTitle(" \n\t "); !errors.Is(err, ErrAutoTitlePromptRequired) {
t.Fatalf("empty prompt error = %v, want %v", err, ErrAutoTitlePromptRequired)
}
}
func TestPrepareAutoTitleRejectsOfficeRequests(t *testing.T) {
err := prepareAutoTitle(&CreateTaskRequest{
ProjectID: "office-project",
AutoTitle: true,
Description: "Create an Office task",
})
if !errors.Is(err, ErrAutoTitleUnsupportedForOffice) {
t.Fatalf("prepare office auto title error = %v, want %v", err, ErrAutoTitleUnsupportedForOffice)
}
}
func TestCreateTaskAutoTitlePersistsPendingMarker(t *testing.T) {
svc, eventBus, repo := createTestService(t)
ctx := context.Background()
if err := repo.CreateWorkspace(ctx, &models.Workspace{ID: "ws-auto-title", Name: "Workspace"}); err != nil {
t.Fatalf("create workspace: %v", err)
}
if err := repo.CreateWorkflow(ctx, &models.Workflow{ID: "wf-auto-title", WorkspaceID: "ws-auto-title", Name: "Workflow"}); err != nil {
t.Fatalf("create workflow: %v", err)
}
taskResult, err := svc.CreateTask(ctx, &CreateTaskRequest{
WorkspaceID: "ws-auto-title",
WorkflowID: "wf-auto-title",
Description: " Add\n a better task title for this request ",
AutoTitle: true,
})
task := taskResult.Task
if err != nil {
t.Fatalf("create auto-titled task: %v", err)
}
if task.Title != "Add a better task title for" {
t.Fatalf("title = %q, want first six words", task.Title)
}
if !models.IsAgentTitlePending(task.Metadata) {
t.Fatalf("metadata = %#v, want pending agent title", task.Metadata)
}
if len(eventBus.GetPublishedEvents()) != 1 || eventBus.GetPublishedEvents()[0].Type != "task.created" {
t.Fatalf("events = %#v, want one task.created event", eventBus.GetPublishedEvents())
}
reloaded, err := svc.GetTask(ctx, task.ID)
if err != nil {
t.Fatalf("reload auto-titled task: %v", err)
}
if !models.IsAgentTitlePending(reloaded.Metadata) {
t.Fatalf("reloaded metadata = %#v, want pending marker", reloaded.Metadata)
}
}
func TestCreateTaskPersistsAutopilot(t *testing.T) {
svc, _, repo := createTestService(t)
ctx := context.Background()
if err := repo.CreateWorkspace(ctx, &models.Workspace{ID: "ws-autopilot-service", Name: "Workspace"}); err != nil {
t.Fatalf("create workspace: %v", err)
}
if err := repo.CreateWorkflow(ctx, &models.Workflow{ID: "wf-autopilot-service", WorkspaceID: "ws-autopilot-service", Name: "Workflow"}); err != nil {
t.Fatalf("create workflow: %v", err)
}
taskResult, err := svc.CreateTask(ctx, &CreateTaskRequest{
WorkspaceID: "ws-autopilot-service",
WorkflowID: "wf-autopilot-service",
Title: "Autopilot service task",
Autopilot: true,
})
if err != nil {
t.Fatalf("create autopilot task: %v", err)
}
task := taskResult.Task
if !task.Autopilot {
t.Fatal("created autopilot = false, want true")
}
reloaded, err := svc.GetTask(ctx, task.ID)
if err != nil {
t.Fatalf("reload autopilot task: %v", err)
}
if !reloaded.Autopilot {
t.Fatal("reloaded autopilot = false, want true")
}
}
func TestCreateTaskAutoTitleSupportsSubtasks(t *testing.T) {
svc, _, repo := createTestService(t)
ctx := context.Background()
if err := repo.CreateWorkspace(ctx, &models.Workspace{ID: "ws-auto-subtask", Name: "Workspace"}); err != nil {
t.Fatalf("create workspace: %v", err)
}
if err := repo.CreateWorkflow(ctx, &models.Workflow{ID: "wf-auto-subtask", WorkspaceID: "ws-auto-subtask", Name: "Workflow"}); err != nil {
t.Fatalf("create workflow: %v", err)
}
parentResult, err := svc.CreateTask(ctx, &CreateTaskRequest{
WorkspaceID: "ws-auto-subtask",
WorkflowID: "wf-auto-subtask",
Title: "Parent task",
})
parent := parentResult.Task
if err != nil {
t.Fatalf("create parent task: %v", err)
}
childResult, err := svc.CreateTask(ctx, &CreateTaskRequest{
WorkspaceID: "ws-auto-subtask",
WorkflowID: "wf-auto-subtask",
ParentID: parent.ID,
Description: "Investigate child behavior",
AutoTitle: true,
})
child := childResult.Task
if err != nil {
t.Fatalf("create auto-titled subtask: %v", err)
}
if child.ParentID != parent.ID || child.Title != "Investigate child behavior" || !models.IsAgentTitlePending(child.Metadata) {
t.Fatalf("child = %#v, want parent, title, and pending marker", child)
}
}
func TestSetPendingAgentTitleIsOneShotAndHumanRenameWins(t *testing.T) {
svc, eventBus, repo := createTestService(t)
ctx := context.Background()
if err := repo.CreateWorkspace(ctx, &models.Workspace{ID: "ws-agent-title", Name: "Workspace"}); err != nil {
t.Fatalf("create workspace: %v", err)
}
if err := repo.CreateWorkflow(ctx, &models.Workflow{ID: "wf-agent-title", WorkspaceID: "ws-agent-title", Name: "Workflow"}); err != nil {
t.Fatalf("create workflow: %v", err)
}
taskResult, err := svc.CreateTask(ctx, &CreateTaskRequest{
WorkspaceID: "ws-agent-title",
WorkflowID: "wf-agent-title",
Title: "Temporary prompt title",
Description: "Do the work",
Metadata: map[string]interface{}{models.MetaKeyAgentTitlePending: true, models.MetaKeyAgentTitleOwnerSessionID: "session-owner"},
})
task := taskResult.Task
if err != nil {
t.Fatalf("create pending task: %v", err)
}
eventBus.ClearEvents()
updated, accepted, reason, err := svc.SetPendingAgentTitle(ctx, task.ID, "session-owner", "Ship login fix")
if err != nil || !accepted {
t.Fatalf("set pending title = (%v, %v, %q), want accepted", updated, accepted, reason)
}
if updated.Title != "Ship login fix" || models.IsAgentTitlePending(updated.Metadata) || models.AgentTitleOwnerSessionID(updated.Metadata) != "" {
t.Fatalf("updated task = %#v, want resolved title and marker removed", updated)
}
if len(eventBus.GetPublishedEvents()) != 1 || eventBus.GetPublishedEvents()[0].Type != "task.updated" {
t.Fatalf("events = %#v, want one task.updated event", eventBus.GetPublishedEvents())
}
_, accepted, reason, err = svc.SetPendingAgentTitle(ctx, task.ID, "session-owner", "Late overwrite")
if err != nil || accepted || reason != "title_not_pending" {
t.Fatalf("second set = (accepted=%v, reason=%q, err=%v), want idempotent rejection", accepted, reason, err)
}
pendingTaskResult, err := svc.CreateTask(ctx, &CreateTaskRequest{
WorkspaceID: "ws-agent-title",
WorkflowID: "wf-agent-title",
Title: "Temporary again",
Description: "Do more work",
Metadata: map[string]interface{}{models.MetaKeyAgentTitlePending: true, models.MetaKeyAgentTitleOwnerSessionID: "session-owner"},
})
pendingTask := pendingTaskResult.Task
if err != nil {
t.Fatalf("create second pending task: %v", err)
}
humanTitle := "Human chosen title"
if _, err := svc.UpdateTask(ctx, pendingTask.ID, &UpdateTaskRequest{Title: &humanTitle}); err != nil {
t.Fatalf("human rename: %v", err)
}
updated, accepted, reason, err = svc.SetPendingAgentTitle(ctx, pendingTask.ID, "session-owner", "Agent overwrite")
if err != nil || accepted || reason != "title_not_pending" || updated.Title != humanTitle {
t.Fatalf("late agent rename = (%v, %v, %q, %v), want human title preserved", updated.Title, accepted, reason, err)
}
}
// TestPerformTaskCleanup_QuickChatDir verifies that performTaskCleanup removes
// quick-chat workspace directories for both ephemeral and non-ephemeral tasks,
// and that tasks with no directory on disk produce no error.
func TestPerformTaskCleanup_QuickChatDir(t *testing.T) {
svc, _, _ := createTestService(t)
ctx := context.Background()
quickChatDir := t.TempDir()
svc.SetQuickChatDir(quickChatDir)
makeSession := func(id string) *models.TaskSession {
return &models.TaskSession{ID: id}
}
t.Run("ephemeral task with dir — dir removed", func(t *testing.T) {
sessID := "sess-ephemeral"
dir := filepath.Join(quickChatDir, sessID)
if err := os.MkdirAll(dir, 0o755); err != nil {
t.Fatalf("mkdir: %v", err)
}
errs := svc.performTaskCleanup(ctx, "task-eph", []*models.TaskSession{makeSession(sessID)}, nil, nil, taskEnvironmentCleanup{}, nil)
if len(errs) != 0 {
t.Fatalf("unexpected errors: %v", errs)
}
if _, err := os.Stat(dir); !os.IsNotExist(err) {
t.Errorf("expected dir %s to be removed", dir)
}
})
t.Run("non-ephemeral task with dir — dir removed", func(t *testing.T) {
sessID := "sess-nonephemeral"
dir := filepath.Join(quickChatDir, sessID)
if err := os.MkdirAll(dir, 0o755); err != nil {
t.Fatalf("mkdir: %v", err)
}
errs := svc.performTaskCleanup(ctx, "task-noeph", []*models.TaskSession{makeSession(sessID)}, nil, nil, taskEnvironmentCleanup{}, nil)
if len(errs) != 0 {
t.Fatalf("unexpected errors: %v", errs)
}
if _, err := os.Stat(dir); !os.IsNotExist(err) {
t.Errorf("expected dir %s to be removed", dir)
}
})
t.Run("task with no dir on disk — no error", func(t *testing.T) {
sessID := "sess-nodir"
// Do not create the directory.
errs := svc.performTaskCleanup(ctx, "task-nodir", []*models.TaskSession{makeSession(sessID)}, nil, nil, taskEnvironmentCleanup{}, nil)
if len(errs) != 0 {
t.Fatalf("expected no errors when dir absent, got: %v", errs)
}
})
}