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:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user