Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 15 additions & 1 deletion src/tasks/taskApi.class.ts
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,19 @@ export class TaskApi {
// Initialize timeSpent if it doesn't exist (for older tasks)
if (task.timeSpent === undefined) task.timeSpent = 0;

// Auto-stop other tasks when starting a new one
if (newStatus === TaskStatus.InProgress) {
for (const t of tasks) {
if (t.status === TaskStatus.InProgress && t.externalId !== task.externalId && t.lastStartedAt) {
const started = new Date(t.lastStartedAt);
const duration = currentDate.getTime() - started.getTime();
t.timeSpent += duration;
t.lastStartedAt = undefined;
t.status = TaskStatus.Pending;
}
}
}

// Handle status transitions for time tracking
if (newStatus !== undefined && newStatus !== oldStatus) {
if (newStatus === TaskStatus.InProgress) {
Expand All @@ -127,7 +140,8 @@ export class TaskApi {
} else {
// Moving away from InProgress (to Done, Blocked, or Pending)
if (oldStatus === TaskStatus.InProgress && task.lastStartedAt) {
const duration = currentDate.getTime() - task.lastStartedAt.getTime();
const started = new Date(task.lastStartedAt);
const duration = currentDate.getTime() - started.getTime();
task.timeSpent += duration;
task.lastStartedAt = undefined;
}
Expand Down
100 changes: 100 additions & 0 deletions tests/tasks/taskApi.class.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -278,6 +278,106 @@ describe('TaskApi', () => {
expect(task.timeSpent).toBe(0);
expect(task.title).toBe('Updated Title');
});
it('should stop an InProgress task when another task starts as InProgress', async () => {
// Setup: Task 1 is InProgress
mockDb.data.tasks[0].status = TaskStatus.InProgress;
mockDb.data.tasks[0].lastStartedAt = new Date(new Date().getTime() - 60000); // 60s ago
mockDb.data.tasks[0].timeSpent = 0;

// Setup: Task 2 is Pending
mockDb.data.tasks[1] = {
_id: 'task-2-id',
externalId: 2,
title: 'Task 2',
importance: 3,
urgency: 3,
estimatedTime: 10,
status: TaskStatus.Pending,
createdAt: new Date(),
updatedAt: new Date(),
labels: [],
timeSpent: 0,
};

// Action: Start Task 2
await taskApi.update('2', { ...taskDto({ status: TaskStatus.InProgress }) });

// Assertions
const task1 = await taskApi.get('1');
const task2 = await taskApi.get('2');

// Task 1 should have stopped and accumulated time
expect(task1.status).toBe(TaskStatus.Pending);
expect(task1.timeSpent).toBeGreaterThan(55000);
expect(task1.lastStartedAt).toBeUndefined();

// Task 2 should now be InProgress
expect(task2.status).toBe(TaskStatus.InProgress);
expect(task2.lastStartedAt).toBeInstanceOf(Date);
});

it('should not stop the same task when starting it', async () => {
// Setup: Task 1 is InProgress
mockDb.data.tasks[0].status = TaskStatus.InProgress;
mockDb.data.tasks[0].lastStartedAt = new Date(new Date().getTime() - 60000); // 60s ago
mockDb.data.tasks[0].timeSpent = 5000;

// Action: Start Task 1 again (redundant)
await taskApi.update('1', { ...taskDto({ status: TaskStatus.InProgress }) });

const task1 = await taskApi.get('1');
expect(task1.status).toBe(TaskStatus.InProgress);
expect(task1.timeSpent).toBe(5000); // Should not have changed
});

it('should stop an InProgress task when another starts, even when lastStartedAt is a string (loaded from disk)', async () => {
// Setup: Task 1 is InProgress with lastStartedAt as ISO string (simulating read from JSON)
const dateStr = new Date(new Date().getTime() - 60000).toISOString();
mockDb.data.tasks[0].status = TaskStatus.InProgress;
mockDb.data.tasks[0].lastStartedAt = dateStr as any; // String instead of Date
mockDb.data.tasks[0].timeSpent = 0;

// Setup: Task 2 is Pending
mockDb.data.tasks[1] = {
_id: 'task-2-id',
externalId: 2,
title: 'Task 2',
importance: 3,
urgency: 3,
estimatedTime: 10,
status: TaskStatus.Pending,
createdAt: new Date(),
updatedAt: new Date(),
labels: [],
timeSpent: 0,
};

// Action: Start Task 2
await taskApi.update('2', { ...taskDto({ status: TaskStatus.InProgress }) });

// Assertions
const task1 = await taskApi.get('1');
expect(task1.status).toBe(TaskStatus.Pending);
expect(task1.timeSpent).toBeGreaterThan(55000);
expect(task1.lastStartedAt).toBeUndefined();
});

it('should accumulate time when moving away from InProgress, even when lastStartedAt is a string', async () => {
// Setup: Task is InProgress with lastStartedAt as ISO string
const dateStr = new Date(new Date().getTime() - 120000).toISOString();
mockDb.data.tasks[0].status = TaskStatus.InProgress;
mockDb.data.tasks[0].lastStartedAt = dateStr as any;
mockDb.data.tasks[0].timeSpent = 0;

// Action: Move to Pending
const updatedTask = await taskApi.update('1', { ...taskDto({ status: TaskStatus.Pending }) });

// Should have accumulated ~120s without crashing
expect(updatedTask.timeSpent).toBeGreaterThan(118000);
expect(updatedTask.timeSpent).toBeLessThan(122000);
expect(updatedTask.lastStartedAt).toBeUndefined();
});

});
});

Expand Down