package tasks import ( "context" "testing" "time" "inbox/internal/base/timeutil" "inbox/internal/domain/lane" "inbox/internal/domain/task" "inbox/internal/domain/topic" "inbox/internal/domain/workspace" sqlitestore "inbox/internal/store/sqlite" ) func TestPatchDoesNotPartiallyPersistWhenDependencyReplaceFails(t *testing.T) { ctx := context.Background() clock := timeutil.FixedClock{Time: time.Date(2026, 3, 16, 14, 0, 0, 0, time.UTC)} store, err := sqlitestore.OpenInMemory(clock) if err != nil { t.Fatalf("OpenInMemory() error = %v", err) } defer store.Close() ws, chainRecord, taskRecord := seedTaskServiceGraph(t, ctx, store) _ = ws service := NewService(store, clock) newTitle := "Updated title should roll back" _, err = service.Patch(ctx, taskRecord.ID, Patch{ Title: &newTitle, Dependencies: &[]task.Dependency{ {TaskID: taskRecord.ID, DependsOnTaskID: "missing-task"}, }, }, "tester") if err == nil { t.Fatalf("expected Patch() error when dependency replace fails") } persisted, err := store.GetTask(ctx, taskRecord.ID) if err != nil { t.Fatalf("GetTask() error = %v", err) } if persisted.Title != taskRecord.Title { t.Fatalf("expected task title to remain %q, got %#v", taskRecord.Title, persisted) } deps, err := store.ListTaskDependencies(ctx, taskRecord.ID) if err != nil { t.Fatalf("ListTaskDependencies() error = %v", err) } if len(deps) != 0 { t.Fatalf("expected dependencies unchanged, got %#v", deps) } updatedTasks, err := store.ListTasksByLane(ctx, chainRecord.ID) if err != nil { t.Fatalf("ListTasksByLane() error = %v", err) } if len(updatedTasks) != 1 || updatedTasks[0].Title != taskRecord.Title { t.Fatalf("unexpected tasks after failed patch: %#v", updatedTasks) } } func seedTaskServiceGraph(t *testing.T, ctx context.Context, store *sqlitestore.Store) (workspace.Workspace, lane.Record, task.Record) { t.Helper() project, err := store.CreateProject(ctx, workspace.Project{ Slug: "demo", Name: "Demo", RootPath: t.TempDir(), DefaultBranch: "main", Status: "active", }) if err != nil { t.Fatalf("CreateProject() error = %v", err) } ws, err := store.CreateWorkspace(ctx, workspace.Workspace{ ProjectID: project.ID, Slug: "main", Name: "Main", RootPath: t.TempDir(), BaseBranch: "main", WorktreeBranch: "worktree/main", RuntimeBackend: "host", Status: "active", }) if err != nil { t.Fatalf("CreateWorkspace() error = %v", err) } topicRecord, err := store.CreateTopic(ctx, topic.Record{ WorkspaceID: ws.ID, Slug: "cleanup", Title: "Cleanup", Space: topic.SpaceWorkflow, Status: "execution", }) if err != nil { t.Fatalf("CreateTopic() error = %v", err) } chainRecord, err := store.CreateLane(ctx, lane.Record{ WorkspaceID: ws.ID, TopicID: topicRecord.ID, Name: "Main Chain", Slug: "main-chain", Status: lane.StatusDraft, BaseBranch: "main", BranchName: "lane/main/main-lane", WorktreePath: t.TempDir(), ContainerName: "lane-main-main-lane", CreatedByRoleName: "leader", }) if err != nil { t.Fatalf("CreateLane() error = %v", err) } taskRecord, err := store.CreateTask(ctx, task.Record{ WorkspaceID: ws.ID, TopicID: topicRecord.ID, LaneID: chainRecord.ID, Title: "Original title", BodyMarkdown: "Initial body.", Status: task.StatusDraft, TaskOrder: 1, Priority: 1, CreatedByRoleName: "leader", }, nil) if err != nil { t.Fatalf("CreateTask() error = %v", err) } return ws, chainRecord, taskRecord }