Files
tasq/test/programmer_daily_time_test.dart
T
redz1029 f071cc722d fix(programmer): reassignment while paused must not open new segment
Capture whether segment was running before reassignment, and only reopen
for new assignee if it was. A paused task that gets reassigned should not
start accruing time for the new assignee until explicitly resumed.

Also add test: reassignment while paused does not open segment.

Co-Authored-By: claude-flow <ruv@ruv.net>
2026-09-27 20:57:55 +08:00

268 lines
8.7 KiB
Dart

import 'package:flutter_test/flutter_test.dart';
import 'package:tasq/models/programmer_task.model.dart';
import 'package:tasq/models/programmer_task_activity_log.model.dart';
import 'package:tasq/utils/app_time.dart';
import 'package:tasq/utils/programmer_daily_time.dart';
void main() {
setUpAll(AppTime.initialize);
/// Helper to create a ProgrammerTaskActivityLog at Manila time d h:m
ProgrammerTaskActivityLog _ev(
String action,
int d,
int h, [
int m = 0,
Map<String, dynamic>? meta,
]) {
return ProgrammerTaskActivityLog(
id: 'ev-$d-$h-$m',
taskId: 'task-1',
actorId: 'actor',
actionType: action,
meta: meta,
createdAt: AppTime.fromComponents(
year: 2026,
month: 9,
day: d,
hour: h,
minute: m,
),
);
}
/// Helper to create a ProgrammerTask
ProgrammerTask _task({
String status = ProgrammerTaskStatus.inProgress,
String? creatorId = 'a',
String? assigneeId = 'a',
DateTime? startedAt,
DateTime? completedAt,
DateTime? cancelledAt,
}) {
return ProgrammerTask(
id: 'task-1',
title: 'Test Task',
category: ProgrammerTaskCategory.softwareDevelopment,
status: status,
creatorId: creatorId,
assigneeId: assigneeId,
createdAt: AppTime.fromComponents(year: 2026, month: 9, day: 27),
startedAt: startedAt,
completedAt: completedAt,
cancelledAt: cancelledAt,
updatedAt: AppTime.fromComponents(year: 2026, month: 9, day: 27),
);
}
test('manilaDay ignores device time zone', () {
// Sep 26 4:30 PM UTC = Sep 27 12:30 AM Manila time
expect(
manilaDay(DateTime.parse('2026-09-26T16:30:00Z')),
DateTime.utc(2026, 9, 27),
);
// Sep 26 3:59 PM UTC = Sep 26 11:59 PM Manila time
expect(
manilaDay(DateTime.parse('2026-09-26T15:59:00Z')),
DateTime.utc(2026, 9, 26),
);
});
test('day key round-trip', () {
expect(formatDayKey(parseDayKey('2026-09-27')), '2026-09-27');
expect(formatDayKey(parseDayKey('2025-01-05')), '2025-01-05');
});
test('single day with a pause', () {
final task = _task(
status: ProgrammerTaskStatus.completed,
creatorId: 'a',
assigneeId: 'a',
startedAt: AppTime.fromComponents(year: 2026, month: 9, day: 27, hour: 9),
completedAt:
AppTime.fromComponents(year: 2026, month: 9, day: 27, hour: 15),
);
final logs = [
_ev('started', 27, 9),
_ev('paused', 27, 11),
_ev('resumed', 27, 13),
_ev('completed', 27, 15),
];
final now =
AppTime.fromComponents(year: 2026, month: 9, day: 27, hour: 20);
final result = dailySecondsByAssignee(task: task, logs: logs, now: now);
expect(result['a'], {DateTime.utc(2026, 9, 27): 4 * 3600});
});
test('running across Manila midnight splits by day', () {
final task = _task(
status: ProgrammerTaskStatus.inProgress,
creatorId: 'a',
assigneeId: 'a',
startedAt: AppTime.fromComponents(year: 2026, month: 9, day: 26, hour: 22),
);
final logs = [
_ev('started', 26, 22),
];
final now =
AppTime.fromComponents(year: 2026, month: 9, day: 27, hour: 2);
final result = dailySecondsByAssignee(task: task, logs: logs, now: now);
expect(result['a']![DateTime.utc(2026, 9, 26)], 2 * 3600);
expect(result['a']![DateTime.utc(2026, 9, 27)], 2 * 3600);
});
test('reassignment credits each assignee for their own span', () {
final task = _task(
status: ProgrammerTaskStatus.completed,
creatorId: 'a',
assigneeId: 'b',
startedAt: AppTime.fromComponents(year: 2026, month: 9, day: 27, hour: 9),
completedAt:
AppTime.fromComponents(year: 2026, month: 9, day: 27, hour: 12),
);
final logs = [
_ev('started', 27, 9),
_ev('reassigned', 27, 10, 0, {'assignee_id': 'b'}),
_ev('completed', 27, 12),
];
final now =
AppTime.fromComponents(year: 2026, month: 9, day: 27, hour: 20);
final result = dailySecondsByAssignee(task: task, logs: logs, now: now);
expect(result['a']![DateTime.utc(2026, 9, 27)], 3600);
expect(result['b']![DateTime.utc(2026, 9, 27)], 2 * 3600);
});
test('initial assignee comes from the first assigned event', () {
final task = _task(
status: ProgrammerTaskStatus.completed,
creatorId: 'boss',
assigneeId: 'a',
startedAt: AppTime.fromComponents(year: 2026, month: 9, day: 27, hour: 9),
completedAt:
AppTime.fromComponents(year: 2026, month: 9, day: 27, hour: 10),
);
final logs = [
_ev('assigned', 27, 8, 0, {'assignee_id': 'a'}),
_ev('started', 27, 9),
_ev('completed', 27, 10),
];
final now =
AppTime.fromComponents(year: 2026, month: 9, day: 27, hour: 20);
final result = dailySecondsByAssignee(task: task, logs: logs, now: now);
expect(result.keys.toList(), ['a']);
expect(result['a']![DateTime.utc(2026, 9, 27)], 3600);
});
test('adjustment subtracts on the day created and clamps at zero', () {
// Case 1: adjustment {seconds:1800} at 10:00
final task1 = _task(
status: ProgrammerTaskStatus.completed,
creatorId: 'a',
assigneeId: 'a',
startedAt: AppTime.fromComponents(year: 2026, month: 9, day: 27, hour: 9),
completedAt:
AppTime.fromComponents(year: 2026, month: 9, day: 27, hour: 11),
);
final logs1 = [
_ev('started', 27, 9),
_ev('adjustment', 27, 10, 0, {'seconds': 1800}),
_ev('completed', 27, 11),
];
final now =
AppTime.fromComponents(year: 2026, month: 9, day: 27, hour: 20);
final result1 =
dailySecondsByAssignee(task: task1, logs: logs1, now: now);
expect(result1['a']![DateTime.utc(2026, 9, 27)], 5400); // 7200 - 1800
// Case 2: adjustment {seconds: 99999} -> clamps at 0
final task2 = _task(
status: ProgrammerTaskStatus.completed,
creatorId: 'a',
assigneeId: 'a',
startedAt: AppTime.fromComponents(year: 2026, month: 9, day: 27, hour: 9),
completedAt:
AppTime.fromComponents(year: 2026, month: 9, day: 27, hour: 11),
);
final logs2 = [
_ev('started', 27, 9),
_ev('adjustment', 27, 10, 0, {'seconds': 99999}),
_ev('completed', 27, 11),
];
final result2 =
dailySecondsByAssignee(task: task2, logs: logs2, now: now);
expect(result2['a']![DateTime.utc(2026, 9, 27)], 0); // clamped
});
test('falls back to task.startedAt when there is no started log', () {
final task = _task(
status: ProgrammerTaskStatus.completed,
creatorId: 'a',
assigneeId: 'a',
startedAt: AppTime.fromComponents(year: 2026, month: 9, day: 27, hour: 9),
completedAt:
AppTime.fromComponents(year: 2026, month: 9, day: 27, hour: 10),
);
final logs = [
_ev('completed', 27, 10),
];
final now =
AppTime.fromComponents(year: 2026, month: 9, day: 27, hour: 20);
final result = dailySecondsByAssignee(task: task, logs: logs, now: now);
expect(result['a']![DateTime.utc(2026, 9, 27)], 3600);
});
test('a paused in-progress task stops accruing at the pause', () {
final task = _task(
status: ProgrammerTaskStatus.inProgress,
creatorId: 'a',
assigneeId: 'a',
startedAt: AppTime.fromComponents(year: 2026, month: 9, day: 27, hour: 9),
);
final logs = [
_ev('started', 27, 9),
_ev('paused', 27, 10),
];
final now =
AppTime.fromComponents(year: 2026, month: 9, day: 27, hour: 15);
final result = dailySecondsByAssignee(task: task, logs: logs, now: now);
expect(result['a']![DateTime.utc(2026, 9, 27)], 3600); // not 6 hours
});
test('reassignment while paused does not open a new segment for the new assignee', () {
// task: started 09:00, paused 10:00, reassigned {assignee_id:'b'} 11:00, completed 12:00 (all Sep 27)
// expected: a -> 3600s (09-10), b -> 0 (no interval opened after paused reassignment)
final task = _task(
status: ProgrammerTaskStatus.completed,
creatorId: 'a',
assigneeId: 'b',
startedAt: AppTime.fromComponents(year: 2026, month: 9, day: 27, hour: 9),
completedAt:
AppTime.fromComponents(year: 2026, month: 9, day: 27, hour: 12),
);
final logs = [
_ev('started', 27, 9),
_ev('paused', 27, 10),
_ev('reassigned', 27, 11, 0, {'assignee_id': 'b'}),
_ev('completed', 27, 12),
];
final now =
AppTime.fromComponents(year: 2026, month: 9, day: 27, hour: 20);
final result = dailySecondsByAssignee(task: task, logs: logs, now: now);
expect(result['a']![DateTime.utc(2026, 9, 27)], 3600); // a worked 09-10
// b should have no time, or null entry
expect(
result['b']?[DateTime.utc(2026, 9, 27)] ?? 0,
0,
);
});
}