package httpapi import ( "context" "encoding/json" "net/http" "net/http/httptest" "testing" "time" "inbox/internal/base/timeutil" "inbox/internal/domain/lane" "inbox/internal/domain/lanesync" "inbox/internal/domain/task" "inbox/internal/domain/topic" "inbox/internal/domain/workspace" sqlitestore "inbox/internal/store/sqlite" ) func TestDashboardWorkflowBoardIncludesLanesAndTasks(t *testing.T) { clock := timeutil.FixedClock{Time: time.Date(2026, 3, 16, 11, 0, 0, 0, time.UTC)} store, err := sqlitestore.OpenInMemory(clock) if err != nil { t.Fatalf("OpenInMemory() error = %v", err) } defer store.Close() ctx := context.Background() 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) } record, err := store.CreateTopic(ctx, topic.Record{ WorkspaceID: ws.ID, Slug: "signup-flow", Title: "Signup Flow", Space: topic.SpaceWorkflow, Status: "execution", }) if err != nil { t.Fatalf("CreateTopic() error = %v", err) } laneRecord, err := store.CreateLane(ctx, lane.Record{ WorkspaceID: ws.ID, TopicID: record.ID, Name: "UI Chain", Slug: "ui-chain", Status: lane.StatusRunning, BaseBranch: "main", BranchName: "lane/main/ui-lane", HeadCommit: "abc123def4567890", WorktreePath: "/tmp/ui-chain", ContainerName: "lane-main-ui-lane", CreatedByRoleName: "leader", }) if err != nil { t.Fatalf("CreateLane() error = %v", err) } upstreamLane, err := store.CreateLane(ctx, lane.Record{ WorkspaceID: ws.ID, TopicID: record.ID, Name: "Foundation", Slug: "foundation", Status: lane.StatusSucceeded, BaseBranch: "main", BranchName: "lane/main/foundation", HeadCommit: "1234567890abcdef", WorktreePath: "/tmp/foundation", ContainerName: "lane-main-foundation", CreatedByRoleName: "leader", }) if err != nil { t.Fatalf("CreateLane(upstream) error = %v", err) } rootTask, err := store.CreateTask(ctx, task.Record{ WorkspaceID: ws.ID, TopicID: record.ID, LaneID: laneRecord.ID, Title: "Build UI", BodyMarkdown: "Implement the UI lane.", Status: task.StatusRunning, TaskOrder: 1, Priority: 10, CreatedByRoleName: "leader", }, nil) if err != nil { t.Fatalf("CreateTask(root) error = %v", err) } if _, err := store.CreateTask(ctx, task.Record{ WorkspaceID: ws.ID, TopicID: record.ID, LaneID: laneRecord.ID, Title: "Verify UI", BodyMarkdown: "Verify the UI lane.", Status: task.StatusDraft, TaskOrder: 2, Priority: 5, CreatedByRoleName: "leader", }, []task.Dependency{{DependsOnTaskID: rootTask.ID}}); err != nil { t.Fatalf("CreateTask(dependent) error = %v", err) } if _, err := store.CreateLaneSync(ctx, lanesync.Record{ WorkspaceID: ws.ID, TopicID: record.ID, DownstreamLaneID: laneRecord.ID, UpstreamLaneID: upstreamLane.ID, TaskID: rootTask.ID, UpstreamCommit: "1234567890abcdef", MergeCommit: "fedcba0987654321", Status: lanesync.StatusApplied, }); err != nil { t.Fatalf("CreateLaneSync() error = %v", err) } handler := NewHandler(store, clock) req := httptest.NewRequest(http.MethodGet, "/api/v2/dashboard/workflow/board?workspace="+ws.Slug+"&topic="+record.Slug, nil) rec := httptest.NewRecorder() handler.ServeHTTP(rec, req) if rec.Code != http.StatusOK { t.Fatalf("GET workflow board status = %d body=%s", rec.Code, rec.Body.String()) } var payload struct { Board struct { Lanes []struct { Name string `json:"name"` Status string `json:"status"` HeadCommit string `json:"head_commit"` LastSync *struct { Status string `json:"status"` UpstreamLaneID string `json:"upstream_lane_id"` } `json:"last_sync"` SyncHistory []struct { Status string `json:"status"` } `json:"sync_history"` } `json:"lanes"` Tasks []struct { Title string `json:"title"` Status string `json:"status"` LaneID string `json:"lane_id"` Dependencies []struct { DependsOnTaskID string `json:"depends_on_task_id"` } `json:"dependencies"` } `json:"tasks"` } `json:"board"` } if err := json.Unmarshal(rec.Body.Bytes(), &payload); err != nil { t.Fatalf("decode workflow board: %v", err) } if len(payload.Board.Lanes) != 2 { t.Fatalf("unexpected lanes payload: %#v", payload.Board.Lanes) } var uiLane *struct { Name string `json:"name"` Status string `json:"status"` HeadCommit string `json:"head_commit"` LastSync *struct { Status string `json:"status"` UpstreamLaneID string `json:"upstream_lane_id"` } `json:"last_sync"` SyncHistory []struct { Status string `json:"status"` } `json:"sync_history"` } for idx := range payload.Board.Lanes { if payload.Board.Lanes[idx].Name == "UI Chain" { uiLane = &payload.Board.Lanes[idx] break } } if uiLane == nil { t.Fatalf("expected UI Chain in lanes payload, got %#v", payload.Board.Lanes) } if uiLane.HeadCommit != "abc123def4567890" { t.Fatalf("expected head commit in lanes payload, got %#v", uiLane) } if uiLane.LastSync == nil || uiLane.LastSync.Status != "applied" || uiLane.LastSync.UpstreamLaneID != upstreamLane.ID { t.Fatalf("expected latest lane sync in payload, got %#v", uiLane) } if len(uiLane.SyncHistory) != 1 { t.Fatalf("expected sync history in payload, got %#v", uiLane) } if len(payload.Board.Tasks) != 2 { t.Fatalf("unexpected tasks payload: %#v", payload.Board.Tasks) } if payload.Board.Tasks[0].LaneID != laneRecord.ID { t.Fatalf("expected lane id on task payload, got %#v", payload.Board.Tasks[0]) } if payload.Board.Tasks[1].Dependencies[0].DependsOnTaskID != rootTask.ID { t.Fatalf("expected dependency on root task, got %#v", payload.Board.Tasks[1]) } }