130 lines
3.7 KiB
Go
130 lines
3.7 KiB
Go
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
|
|
}
|