diff --git a/apps/backend/internal/analytics/repository/sqlite/repository_test.go b/apps/backend/internal/analytics/repository/sqlite/repository_test.go index b9afaa6b2f..61d828955f 100644 --- a/apps/backend/internal/analytics/repository/sqlite/repository_test.go +++ b/apps/backend/internal/analytics/repository/sqlite/repository_test.go @@ -424,13 +424,19 @@ func TestGetCompletedTaskActivity_ExcludesEphemeralTasks(t *testing.T) { execOrFatal(t, dbConn, `INSERT INTO boards (id, workspace_id, name, created_at, updated_at) VALUES ('board-1', 'ws-1', 'Board', ?, ?)`, nowStr, nowStr) execOrFatal(t, dbConn, `INSERT INTO workflow_steps (id, workflow_id, name, position, created_at, updated_at) VALUES ('step-done', 'board-1', 'Done', 1, ?, ?)`, nowStr, nowStr) - // Regular completed task - execOrFatal(t, dbConn, `INSERT INTO tasks (id, workspace_id, board_id, workflow_step_id, title, is_ephemeral, archived_at, created_at, updated_at) VALUES ('task-regular', 'ws-1', 'board-1', 'step-done', 'Regular', 0, ?, ?, ?)`, nowStr, nowStr, nowStr) + // Completed task on last step with session completed_at (NOT archived — isolates session path) + execOrFatal(t, dbConn, `INSERT INTO tasks (id, workspace_id, board_id, workflow_step_id, title, is_ephemeral, created_at, updated_at) VALUES ('task-regular', 'ws-1', 'board-1', 'step-done', 'Regular', 0, ?, ?)`, nowStr, nowStr) + // Archived task WITHOUT session completed_at — should still count using archived_at + execOrFatal(t, dbConn, `INSERT INTO tasks (id, workspace_id, board_id, workflow_step_id, title, is_ephemeral, archived_at, created_at, updated_at) VALUES ('task-archived-no-session', 'ws-1', 'board-1', 'step-done', 'Archived No Session', 0, ?, ?, ?)`, nowStr, nowStr, nowStr) + // Task on last step with NO archived_at and NO session completed_at — should NOT count + execOrFatal(t, dbConn, `INSERT INTO tasks (id, workspace_id, board_id, workflow_step_id, title, is_ephemeral, created_at, updated_at) VALUES ('task-last-step-no-dates', 'ws-1', 'board-1', 'step-done', 'No Dates', 0, ?, ?)`, nowStr, nowStr) // Ephemeral completed task execOrFatal(t, dbConn, `INSERT INTO tasks (id, workspace_id, board_id, workflow_step_id, title, is_ephemeral, archived_at, created_at, updated_at) VALUES ('task-ephemeral', 'ws-1', 'board-1', '', 'Quick Chat', 1, ?, ?, ?)`, nowStr, nowStr, nowStr) execOrFatal(t, dbConn, `INSERT INTO task_sessions (id, task_id, agent_profile_id, state, started_at, completed_at, updated_at) VALUES ('sess-regular', 'task-regular', 'agent-1', 'COMPLETED', ?, ?, ?)`, nowStr, nowStr, nowStr) execOrFatal(t, dbConn, `INSERT INTO task_sessions (id, task_id, agent_profile_id, state, started_at, completed_at, updated_at) VALUES ('sess-ephemeral', 'task-ephemeral', 'agent-1', 'COMPLETED', ?, ?, ?)`, nowStr, nowStr, nowStr) + // Session for archived task with NO completed_at + execOrFatal(t, dbConn, `INSERT INTO task_sessions (id, task_id, agent_profile_id, state, started_at, updated_at) VALUES ('sess-archived', 'task-archived-no-session', 'agent-1', 'CANCELLED', ?, ?)`, nowStr, nowStr) results, err := repo.GetCompletedTaskActivity(ctx, "ws-1", 7) if err != nil { @@ -441,8 +447,10 @@ func TestGetCompletedTaskActivity_ExcludesEphemeralTasks(t *testing.T) { for _, r := range results { totalCompleted += r.CompletedTasks } - if totalCompleted != 1 { - t.Errorf("expected 1 completed task (ephemeral excluded), got %d", totalCompleted) + // Should count: task-regular (via session completed_at) + task-archived-no-session (via archived_at) + // Should NOT count: task-last-step-no-dates (neither date), task-ephemeral (ephemeral) + if totalCompleted != 2 { + t.Errorf("expected 2 completed tasks (1 via session + 1 via archived_at), got %d", totalCompleted) } } diff --git a/apps/backend/internal/analytics/repository/sqlite/stats.go b/apps/backend/internal/analytics/repository/sqlite/stats.go index 79ba93a3f1..348f5be2e7 100644 --- a/apps/backend/internal/analytics/repository/sqlite/stats.go +++ b/apps/backend/internal/analytics/repository/sqlite/stats.go @@ -260,7 +260,7 @@ func (r *Repository) GetCompletedTaskActivity(ctx context.Context, workspaceID s dateStart := dialect.DateNowMinusDays(drv, "?") datePlus := dialect.DatePlusOneDay(drv, "date") curDate := dialect.CurrentDate(drv) - dateOfCompleted := dialect.DateOf(drv, "ts.completed_at") + dateOfCompleted := dialect.DateOf(drv, "COALESCE(ts.completed_at, t.archived_at)") query := fmt.Sprintf(` WITH RECURSIVE dates(date) AS ( @@ -274,13 +274,14 @@ func (r *Repository) GetCompletedTaskActivity(ctx context.Context, workspaceID s SELECT %s as activity_date, COUNT(DISTINCT t.id) as completed_tasks FROM tasks t LEFT JOIN workflow_steps ws ON ws.id = t.workflow_step_id - JOIN ( + LEFT JOIN ( SELECT task_id, MAX(completed_at) as completed_at FROM task_sessions WHERE completed_at IS NOT NULL GROUP BY task_id ) ts ON ts.task_id = t.id WHERE t.workspace_id = ? AND t.is_ephemeral = 0 AND (t.archived_at IS NOT NULL OR ws.position = (SELECT MAX(ws2.position) FROM workflow_steps ws2 WHERE ws2.workflow_id = ws.workflow_id)) + AND COALESCE(ts.completed_at, t.archived_at) IS NOT NULL GROUP BY %s ) activity ON activity.activity_date = d.date ORDER BY d.date ASC