f071cc722d
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>
268 lines
8.7 KiB
Dart
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,
|
|
);
|
|
});
|
|
}
|