212 lines
6.5 KiB
Go
212 lines
6.5 KiB
Go
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])
|
|
}
|
|
}
|