Files
tasq/test/programmer_task_ledger_test.dart
redz1029 b307f66f2c Add PM extension (Projects, Work Logs, Comments) + Quill rich-text UI
Projects: Brick-backed model with list/detail screens and nav entry.
Work Logs: per-contributor time ledger, assignee running-clock +
helper logged minutes with Hours/Minutes input; pause-after-save and
helper-deduct prompts.
Comments: threaded comments with file attachments and Quill composer.
Detail screen: tabbed layout (Work Log / Comments / Activity),
editable title + description via edit dialog, Quill-rendered
descriptions with legacy plain-text fallback.
Create dialog: fixed-width (480px max), Quill description editor.
Shared QuillFieldEditor + QuillReadOnly widgets extracted for reuse.
RLS: hardened activity-log actor binding, immutable creator_id/
created_at triggers, SECURITY INVOKER visibility helper,
append-only work-log and scoped comment policies.
Brick migration for project_id FK on programmer_tasks.

Co-Authored-By: claude-flow <ruv@ruv.net>
2026-09-27 16:06:26 +08:00

103 lines
3.4 KiB
Dart

import 'package:flutter_test/flutter_test.dart';
import 'package:tasq/models/programmer_task_work_log.model.dart';
import 'package:tasq/utils/app_time.dart';
import 'package:tasq/utils/programmer_task_ledger.dart';
import 'package:tasq/utils/task_duration.dart';
ProgrammerTaskWorkLog _log(String author, int? minutes) => ProgrammerTaskWorkLog(
id: 'l-$author-$minutes',
taskId: 't1',
authorId: author,
description: 'work',
minutes: minutes,
createdAt: DateTime.utc(2026, 1, 1),
);
void main() {
setUpAll(AppTime.initialize);
group('computeTaskLedger', () {
test('assignee only (no work logs)', () {
final ledger = computeTaskLedger(
assigneeId: 'a',
assigneeDuration: const Duration(hours: 1),
workLogs: const [],
);
expect(ledger.assignee?.duration, const Duration(hours: 1));
expect(ledger.helpers, isEmpty);
expect(ledger.total, const Duration(hours: 1));
});
test('sums helper minutes per person; assignee notes excluded', () {
final ledger = computeTaskLedger(
assigneeId: 'a',
assigneeDuration: const Duration(minutes: 60),
workLogs: [
_log('h1', 30),
_log('h1', 15),
_log('h2', 20),
_log('a', null), // assignee progress note — no minutes, excluded
_log('a', 99), // even if an assignee row had minutes, exclude it
],
);
expect(ledger.assignee?.duration, const Duration(minutes: 60));
final byUser = {for (final e in ledger.helpers) e.userId: e.duration};
expect(byUser['h1'], const Duration(minutes: 45));
expect(byUser['h2'], const Duration(minutes: 20));
expect(byUser.containsKey('a'), isFalse);
expect(ledger.total, const Duration(minutes: 60 + 45 + 20));
});
test('helpers sorted by descending duration', () {
final ledger = computeTaskLedger(
assigneeId: 'a',
assigneeDuration: Duration.zero,
workLogs: [_log('h1', 10), _log('h2', 40)],
);
expect(ledger.helpers.first.userId, 'h2');
});
test('no assignee id → assignee entry is null', () {
final ledger = computeTaskLedger(
assigneeId: null,
assigneeDuration: const Duration(hours: 5),
workLogs: [_log('h1', 30)],
);
expect(ledger.assignee, isNull);
expect(ledger.total, const Duration(minutes: 30));
});
});
group('computeEffectiveDuration adjustment', () {
final base = DateTime.utc(2026, 1, 1, 9);
test('subtracts the adjustment', () {
final worked = computeEffectiveDuration(
fallbackStartedAt: base,
endAt: base.add(const Duration(hours: 2)),
events: const [],
adjustment: const Duration(minutes: 30),
);
expect(worked, const Duration(hours: 1, minutes: 30));
});
test('clamps to zero when the adjustment exceeds worked time', () {
final worked = computeEffectiveDuration(
fallbackStartedAt: base,
endAt: base.add(const Duration(minutes: 20)),
events: const [],
adjustment: const Duration(hours: 1),
);
expect(worked, Duration.zero);
});
test('default adjustment is zero (existing behavior unchanged)', () {
final worked = computeEffectiveDuration(
fallbackStartedAt: base,
endAt: base.add(const Duration(hours: 1)),
events: const [],
);
expect(worked, const Duration(hours: 1));
});
});
}