package dashboard import ( "context" "slices" "testing" "time" "inbox/internal/base/timeutil" "inbox/internal/domain/humantask" "inbox/internal/domain/message" "inbox/internal/domain/role" "inbox/internal/domain/taskgraph" "inbox/internal/domain/topic" "inbox/internal/domain/workflow" "inbox/internal/domain/workspace" sqlitestore "inbox/internal/store/sqlite" ) func TestTopicsReturnsWorkflowTopicSummariesOnly(t *testing.T) { ctx := context.Background() clock := timeutil.FixedClock{Time: time.Date(2026, 3, 16, 12, 0, 0, 0, time.UTC)} store := openDashboardTestStore(t, clock) ensureDashboardTestRole(t, ctx, store, role.Definition{ Name: "product", Title: "Product", ExecutorKind: role.ExecutorKindCodex, IsEnabled: true, }) ensureDashboardTestRole(t, ctx, store, role.Definition{ Name: "backend", Title: "Backend", ExecutorKind: role.ExecutorKindCodex, IsEnabled: true, }) ws := createDashboardTestWorkspace(t, ctx, store) alpha := createDashboardTestTopic(t, ctx, store, ws.ID, "alpha", topic.SpaceWorkflow) beta := createDashboardTestTopic(t, ctx, store, ws.ID, "beta", topic.SpaceWorkflow) createDashboardTestTopic(t, ctx, store, ws.ID, "clarify", topic.SpaceClarify) if _, err := store.CreateMessage(ctx, message.Record{ WorkspaceID: ws.ID, TopicID: beta.ID, FromRoleName: "product", ToExpr: "backend", Type: message.TypeChat, Stage: "review", BodyMarkdown: "Please review the beta flow.", CreatedAt: "2026-03-16T12:10:00Z", }); err != nil { t.Fatalf("CreateMessage(beta) error = %v", err) } if _, err := store.CreateWorkflowRun(ctx, workflow.Run{ WorkspaceID: ws.ID, TopicID: alpha.ID, RoleName: "backend", Stage: workflow.StageExecution, Mode: "once", Status: workflow.RunStatusSucceeded, StartedAt: "2026-03-16T12:20:00Z", CompletedAt: "2026-03-16T12:30:00Z", }); err != nil { t.Fatalf("CreateWorkflowRun(alpha) error = %v", err) } service := NewService(store) payload, err := service.Topics(ctx, ws) if err != nil { t.Fatalf("Topics() error = %v", err) } if len(payload.Topics) != 2 { t.Fatalf("expected 2 workflow topics, got %#v", payload.Topics) } if payload.Topics[0].Name != "alpha" || payload.Topics[0].LatestStage != string(workflow.StageExecution) { t.Fatalf("unexpected alpha topic payload: %#v", payload.Topics[0]) } if payload.Topics[1].Name != "beta" || payload.Topics[1].LatestStage != "review" || payload.Topics[1].MessageCount != 1 { t.Fatalf("unexpected beta topic payload: %#v", payload.Topics[1]) } } func TestWorkflowBoardAggregatesOnlyWorkflowTopicState(t *testing.T) { ctx := context.Background() clock := timeutil.FixedClock{Time: time.Date(2026, 3, 16, 12, 0, 0, 0, time.UTC)} store := openDashboardTestStore(t, clock) ensureDashboardTestRole(t, ctx, store, role.Definition{ Name: "product", Title: "Product", ExecutorKind: role.ExecutorKindCodex, IsEnabled: true, }) ensureDashboardTestRole(t, ctx, store, role.Definition{ Name: "backend", Title: "Backend", ExecutorKind: role.ExecutorKindCodex, IsEnabled: true, }) ensureDashboardTestRole(t, ctx, store, role.Definition{ Name: "approver", Title: "Approver", ExecutorKind: role.ExecutorKindHuman, IsEnabled: true, }) ws := createDashboardTestWorkspace(t, ctx, store) workflowTopic := createDashboardTestTopic(t, ctx, store, ws.ID, "signup", topic.SpaceWorkflow) clarifyTopic := createDashboardTestTopic(t, ctx, store, ws.ID, "signup-q", topic.SpaceClarify) workflowDelivery := createDashboardTestMessage(t, ctx, store, message.Record{ WorkspaceID: ws.ID, TopicID: workflowTopic.ID, FromRoleName: "product", ToExpr: "backend", Type: message.TypeChat, Stage: "execution", BodyMarkdown: "Build the signup workflow.", CreatedAt: "2026-03-16T12:05:00Z", }) createDashboardTestMessage(t, ctx, store, message.Record{ WorkspaceID: ws.ID, TopicID: clarifyTopic.ID, FromRoleName: "product", ToExpr: "backend", Type: message.TypeQuestion, Stage: "clarification", BodyMarkdown: "Clarify the signup workflow.", CreatedAt: "2026-03-16T12:06:00Z", }) createDashboardTestMessage(t, ctx, store, message.Record{ WorkspaceID: ws.ID, TopicID: workflowTopic.ID, FromRoleName: "product", ToExpr: "approver", Type: message.TypeQuestion, Stage: "review", BodyMarkdown: "Need human approval for signup.", CreatedAt: "2026-03-16T12:07:00Z", }) createDashboardTestMessage(t, ctx, store, message.Record{ WorkspaceID: ws.ID, TopicID: clarifyTopic.ID, FromRoleName: "product", ToExpr: "approver", Type: message.TypeQuestion, Stage: "clarification", BodyMarkdown: "Need human clarification.", CreatedAt: "2026-03-16T12:08:00Z", }) if _, err := store.CreateTaskGraphVersion(ctx, taskgraph.Record{ TopicID: workflowTopic.ID, Version: 1, Status: taskgraph.StatusDraft, PlanJSON: `{"tasks":[]}`, PlanSummaryMarkdown: "Plan the signup delivery in two steps.\n\n1. Build the form.\n2. Verify the flow.", CreatedByRoleName: "leader", CreatedAt: "2026-03-16T12:09:00Z", }); err != nil { t.Fatalf("CreateTaskGraphVersion() error = %v", err) } service := NewService(store) payload, err := service.WorkflowBoard(ctx, ws, workflowTopic.Slug) if err != nil { t.Fatalf("WorkflowBoard() error = %v", err) } if len(payload.Topics) != 1 || payload.Topics[0].Name != workflowTopic.Slug { t.Fatalf("expected only workflow topic in board summary, got %#v", payload.Topics) } if payload.ActiveTopic != workflowTopic.Slug || payload.Board == nil { t.Fatalf("expected active workflow board, got %#v", payload) } waitingRoles := payload.Topics[0].WaitingRoles if !slices.Equal(waitingRoles, []string{"approver", "backend"}) { t.Fatalf("unexpected waiting roles: %#v", waitingRoles) } if payload.Board.Topic.MessageCount != 2 { t.Fatalf("expected only workflow-topic messages on the board, got %#v", payload.Board.Topic) } if len(payload.Board.PendingHumanTasks) != 1 { t.Fatalf("expected only workflow-topic human tasks, got %#v", payload.Board.PendingHumanTasks) } if payload.Board.PendingHumanTasks[0].PromptBody != "Need human approval for signup." { t.Fatalf("unexpected human task payload: %#v", payload.Board.PendingHumanTasks[0]) } if payload.Board.Plan == nil || payload.Board.Plan.Status != string(taskgraph.StatusDraft) { t.Fatalf("expected workflow board plan payload, got %#v", payload.Board.Plan) } if payload.Board.Plan.SummaryMarkdown == "" { t.Fatalf("expected non-empty plan summary, got %#v", payload.Board.Plan) } if len(payload.Board.Links) != 2 { t.Fatalf("expected only workflow-topic links, got %#v", payload.Board.Links) } if payload.Board.Links[0].LastMessageAt < workflowDelivery.CreatedAt { t.Fatalf("unexpected link ordering: %#v", payload.Board.Links) } } func TestSpaceTopicsSortsByLatestMessageTime(t *testing.T) { ctx := context.Background() clock := timeutil.FixedClock{Time: time.Date(2026, 3, 16, 12, 0, 0, 0, time.UTC)} store := openDashboardTestStore(t, clock) ensureDashboardTestRole(t, ctx, store, role.Definition{ Name: "product", Title: "Product", ExecutorKind: role.ExecutorKindCodex, IsEnabled: true, }) ensureDashboardTestRole(t, ctx, store, role.Definition{ Name: "backend", Title: "Backend", ExecutorKind: role.ExecutorKindCodex, IsEnabled: true, }) ws := createDashboardTestWorkspace(t, ctx, store) olderTopic := createDashboardTestTopic(t, ctx, store, ws.ID, "older", topic.SpaceWorkflow) newerTopic := createDashboardTestTopic(t, ctx, store, ws.ID, "newer", topic.SpaceWorkflow) createDashboardTestMessage(t, ctx, store, message.Record{ ID: "z-older", WorkspaceID: ws.ID, TopicID: olderTopic.ID, FromRoleName: "product", ToExpr: "backend", Type: message.TypeChat, Stage: "execution", BodyMarkdown: "Older topic message.", CreatedAt: "2026-03-16T12:01:00Z", }) createDashboardTestMessage(t, ctx, store, message.Record{ ID: "a-newer", WorkspaceID: ws.ID, TopicID: newerTopic.ID, FromRoleName: "product", ToExpr: "backend", Type: message.TypeChat, Stage: "execution", BodyMarkdown: "Newer topic message.", CreatedAt: "2026-03-16T12:02:00Z", }) service := NewService(store) payload, err := service.SpaceTopics(ctx, ws, topic.SpaceWorkflow) if err != nil { t.Fatalf("SpaceTopics() error = %v", err) } if len(payload.Topics) != 2 { t.Fatalf("expected 2 topics, got %#v", payload.Topics) } if payload.Topics[0].Topic != "newer" || payload.Topics[1].Topic != "older" { t.Fatalf("expected topics sorted by latest message time, got %#v", payload.Topics) } } func openDashboardTestStore(t *testing.T, clock timeutil.FixedClock) *sqlitestore.Store { t.Helper() store, err := sqlitestore.OpenInMemory(clock) if err != nil { t.Fatalf("OpenInMemory() error = %v", err) } t.Cleanup(func() { store.Close() }) return store } func createDashboardTestWorkspace(t *testing.T, ctx context.Context, store *sqlitestore.Store) workspace.Workspace { 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) } return ws } func createDashboardTestTopic(t *testing.T, ctx context.Context, store *sqlitestore.Store, workspaceID, slug string, space topic.Space) topic.Record { t.Helper() record, err := store.CreateTopic(ctx, topic.Record{ WorkspaceID: workspaceID, Slug: slug, Title: slug, Space: space, Status: "execution", }) if err != nil { t.Fatalf("CreateTopic(%s) error = %v", slug, err) } return record } func createDashboardTestMessage(t *testing.T, ctx context.Context, store *sqlitestore.Store, value message.Record) message.Record { t.Helper() item, err := store.CreateMessage(ctx, value) if err != nil { t.Fatalf("CreateMessage(%s) error = %v", value.BodyMarkdown, err) } return item } func ensureDashboardTestRole(t *testing.T, ctx context.Context, store *sqlitestore.Store, definition role.Definition) { t.Helper() if _, err := store.UpsertRole(ctx, definition, "test"); err != nil { t.Fatalf("UpsertRole(%s) error = %v", definition.Name, err) } } var _ humantask.Record