chore(repo): reinitialize repository
This commit is contained in:
@@ -0,0 +1,167 @@
|
||||
package tasks
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"inbox/internal/base/timeutil"
|
||||
"inbox/internal/domain/task"
|
||||
)
|
||||
|
||||
type Repository interface {
|
||||
CreateTask(ctx context.Context, value task.Record, dependencies []task.Dependency) (task.Record, error)
|
||||
GetTask(ctx context.Context, taskID string) (task.Record, error)
|
||||
ListTasksByTopic(ctx context.Context, topicID string) ([]task.Record, error)
|
||||
ListTasksByLane(ctx context.Context, laneID string) ([]task.Record, error)
|
||||
UpdateTaskWithDependencies(ctx context.Context, value task.Record, dependencies *[]task.Dependency) (task.Record, error)
|
||||
ListTaskDependencies(ctx context.Context, taskID string) ([]task.Dependency, error)
|
||||
AppendTaskEvent(ctx context.Context, value task.Event) (task.Event, error)
|
||||
ListTaskEvents(ctx context.Context, taskID string) ([]task.Event, error)
|
||||
}
|
||||
|
||||
type Service struct {
|
||||
repo Repository
|
||||
clock timeutil.Clock
|
||||
}
|
||||
|
||||
type CreateInput struct {
|
||||
Task task.Record
|
||||
Dependencies []task.Dependency
|
||||
}
|
||||
|
||||
type Patch struct {
|
||||
Title *string
|
||||
BodyMarkdown *string
|
||||
AcceptanceMarkdown *string
|
||||
Kind *task.Kind
|
||||
Deliverables *[]string
|
||||
BatchKey *string
|
||||
Status *task.Status
|
||||
Priority *int
|
||||
TaskOrder *int
|
||||
BlockingReasonMarkdown *string
|
||||
ResultSummaryMarkdown *string
|
||||
AssignedRunID *string
|
||||
StartedAt *string
|
||||
CompletedAt *string
|
||||
Dependencies *[]task.Dependency
|
||||
}
|
||||
|
||||
func NewService(repo Repository, clock timeutil.Clock) *Service {
|
||||
if clock == nil {
|
||||
clock = timeutil.SystemClock{}
|
||||
}
|
||||
return &Service{repo: repo, clock: clock}
|
||||
}
|
||||
|
||||
func (s *Service) Create(ctx context.Context, input CreateInput) (task.Record, error) {
|
||||
input.Task = task.NormalizeRecord(input.Task)
|
||||
if input.Task.Status == "" {
|
||||
input.Task.Status = task.StatusDraft
|
||||
}
|
||||
item, err := s.repo.CreateTask(ctx, input.Task, input.Dependencies)
|
||||
if err != nil {
|
||||
return task.Record{}, err
|
||||
}
|
||||
_, _ = s.repo.AppendTaskEvent(ctx, task.Event{
|
||||
TaskID: item.ID,
|
||||
EventType: "created",
|
||||
BodyMarkdown: item.BodyMarkdown,
|
||||
CreatedByRoleName: item.CreatedByRoleName,
|
||||
CreatedAt: item.CreatedAt,
|
||||
})
|
||||
return item, nil
|
||||
}
|
||||
|
||||
func (s *Service) Get(ctx context.Context, taskID string) (task.Record, error) {
|
||||
return s.repo.GetTask(ctx, taskID)
|
||||
}
|
||||
|
||||
func (s *Service) ListByTopic(ctx context.Context, topicID string) ([]task.Record, error) {
|
||||
return s.repo.ListTasksByTopic(ctx, topicID)
|
||||
}
|
||||
|
||||
func (s *Service) ListByLane(ctx context.Context, laneID string) ([]task.Record, error) {
|
||||
return s.repo.ListTasksByLane(ctx, laneID)
|
||||
}
|
||||
|
||||
func (s *Service) Dependencies(ctx context.Context, taskID string) ([]task.Dependency, error) {
|
||||
return s.repo.ListTaskDependencies(ctx, taskID)
|
||||
}
|
||||
|
||||
func (s *Service) Events(ctx context.Context, taskID string) ([]task.Event, error) {
|
||||
return s.repo.ListTaskEvents(ctx, taskID)
|
||||
}
|
||||
|
||||
func (s *Service) Patch(ctx context.Context, taskID string, patch Patch, changedBy string) (task.Record, error) {
|
||||
current, err := s.repo.GetTask(ctx, taskID)
|
||||
if err != nil {
|
||||
return task.Record{}, err
|
||||
}
|
||||
current = task.NormalizeRecord(current)
|
||||
if patch.Title != nil {
|
||||
current.Title = *patch.Title
|
||||
}
|
||||
if patch.BodyMarkdown != nil {
|
||||
current.BodyMarkdown = *patch.BodyMarkdown
|
||||
}
|
||||
if patch.AcceptanceMarkdown != nil {
|
||||
current.AcceptanceMarkdown = *patch.AcceptanceMarkdown
|
||||
}
|
||||
if patch.Kind != nil {
|
||||
current.Kind = *patch.Kind
|
||||
}
|
||||
if patch.Deliverables != nil {
|
||||
current.Deliverables = append([]string(nil), (*patch.Deliverables)...)
|
||||
}
|
||||
if patch.BatchKey != nil {
|
||||
current.BatchKey = *patch.BatchKey
|
||||
}
|
||||
if patch.Status != nil {
|
||||
current.Status = *patch.Status
|
||||
if current.Status == task.StatusRunning && current.StartedAt == "" {
|
||||
current.StartedAt = timeutil.FormatRFC3339(s.clock.Now())
|
||||
}
|
||||
if (current.Status == task.StatusSucceeded || current.Status == task.StatusFailed || current.Status == task.StatusCancelled) && current.CompletedAt == "" {
|
||||
current.CompletedAt = timeutil.FormatRFC3339(s.clock.Now())
|
||||
}
|
||||
}
|
||||
if patch.Priority != nil {
|
||||
current.Priority = *patch.Priority
|
||||
}
|
||||
if patch.TaskOrder != nil {
|
||||
current.TaskOrder = *patch.TaskOrder
|
||||
}
|
||||
if patch.BlockingReasonMarkdown != nil {
|
||||
current.BlockingReasonMarkdown = *patch.BlockingReasonMarkdown
|
||||
}
|
||||
if patch.ResultSummaryMarkdown != nil {
|
||||
current.ResultSummaryMarkdown = *patch.ResultSummaryMarkdown
|
||||
}
|
||||
if patch.AssignedRunID != nil {
|
||||
current.AssignedRunID = *patch.AssignedRunID
|
||||
}
|
||||
if patch.StartedAt != nil {
|
||||
current.StartedAt = *patch.StartedAt
|
||||
}
|
||||
if patch.CompletedAt != nil {
|
||||
current.CompletedAt = *patch.CompletedAt
|
||||
}
|
||||
item, err := s.repo.UpdateTaskWithDependencies(ctx, current, patch.Dependencies)
|
||||
if err != nil {
|
||||
return task.Record{}, err
|
||||
}
|
||||
if changedBy != "" {
|
||||
_, _ = s.repo.AppendTaskEvent(ctx, task.Event{
|
||||
TaskID: item.ID,
|
||||
EventType: "updated",
|
||||
BodyMarkdown: item.ResultSummaryMarkdown,
|
||||
CreatedByRoleName: changedBy,
|
||||
CreatedAt: timeutil.FormatRFC3339(s.clock.Now()),
|
||||
})
|
||||
}
|
||||
return item, nil
|
||||
}
|
||||
|
||||
func (s *Service) AppendEvent(ctx context.Context, value task.Event) (task.Event, error) {
|
||||
return s.repo.AppendTaskEvent(ctx, value)
|
||||
}
|
||||
@@ -0,0 +1,129 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user