fix: include archived tasks in completed tasks over time chart (#668)

* fix: include archived tasks in completed tasks over time chart

Archived tasks without a session completed_at were excluded from the
Completed Tasks Over Time chart. Use COALESCE(session.completed_at,
task.archived_at) as the completion date so archived tasks fall back
to their archive timestamp.

* fix: address review feedback - isolate completion paths in test

- Remove archived_at from task-regular so it only counts via session
  completed_at (CodeRabbit nitpick)
- Add task-last-step-no-dates to verify COALESCE IS NOT NULL filter
  excludes tasks with neither date (Claude suggestion)

---------

Co-authored-by: Kandev Agent <agent@kandev.dev>
This commit is contained in:
João Salavisa
2026-04-21 01:36:04 +01:00
committed by GitHub
parent 5bb10190c2
commit 88f69df702
2 changed files with 15 additions and 6 deletions
@@ -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)
}
}
@@ -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