Files
ai-workflow/inbox/internal/app/dashboard/service_test.go
T

337 lines
11 KiB
Go

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