337 lines
11 KiB
Go
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
|