Files
tasq/test/programmer_stats_test.dart
redz1029 cdfeb954fd feat(programmer): day-sheet model + stats, streak and badges
Add ProgrammerDaySheet model with DaySheetStatus/DaySheetSnapshot,
computeProgrammerStats with streak, first-pass rate, approved totals,
hasCleanWeek, and tiered badge progress via computeBadges. 10 tests pass.

Co-Authored-By: claude-flow <ruv@ruv.net>
2026-09-27 21:27:03 +08:00

265 lines
8.5 KiB
Dart

import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:tasq/models/programmer_day_sheet.model.dart';
import 'package:tasq/models/programmer_task.model.dart';
import 'package:tasq/utils/app_time.dart';
import 'package:tasq/utils/programmer_stats.dart';
void main() {
setUpAll(AppTime.initialize);
ProgrammerDaySheet makeSheet({
String id = 'id',
String programmerId = 'user1',
String status = 'pending',
required DateTime workDate,
int resubmissions = 0,
DaySheetSnapshot? approvedSnapshot,
}) {
return ProgrammerDaySheet(
id: id,
programmerId: programmerId,
status: status,
workDate: workDate,
resubmissions: resubmissions,
approvedSnapshot: approvedSnapshot,
createdAt: DateTime.utc(2026, 1, 1),
updatedAt: DateTime.utc(2026, 1, 1),
);
}
ProgrammerStats statsWith({
int completedTotal = 0,
int bugFixCompleted = 0,
Duration approvedTotal = Duration.zero,
Duration helpGivenTotal = Duration.zero,
Duration helpGivenThisMonth = Duration.zero,
Duration todayFocus = Duration.zero,
int completedThisWeek = 0,
int completedLastWeek = 0,
int currentStreak = 0,
int bestStreak = 0,
double? firstPassRate,
bool hasCleanWeek = false,
}) {
return ProgrammerStats(
todayFocus: todayFocus,
helpGivenThisMonth: helpGivenThisMonth,
approvedTotal: approvedTotal,
helpGivenTotal: helpGivenTotal,
completedThisWeek: completedThisWeek,
completedLastWeek: completedLastWeek,
currentStreak: currentStreak,
bestStreak: bestStreak,
completedTotal: completedTotal,
bugFixCompleted: bugFixCompleted,
firstPassRate: firstPassRate,
hasCleanWeek: hasCleanWeek,
);
}
test('fromMap parses work_date and snapshot', () {
final ts = DateTime.utc(2026, 9, 28).toIso8601String();
final m = <String, dynamic>{
'id': 'ds1',
'programmer_id': 'user1',
'status': 'approved',
'work_date': '2026-09-23',
'submitted_at': null,
'reviewed_at': null,
'auto_submitted': false,
'reviewed_by': null,
'resubmissions': 0,
'approved_snapshot': <String, dynamic>{
'v': 1,
'total_seconds': 3600,
'rows': <dynamic>[],
},
'created_at': ts,
'updated_at': ts,
};
final sheet = ProgrammerDaySheet.fromMap(m);
expect(sheet.workDate, DateTime.utc(2026, 9, 23));
expect(sheet.approvedSnapshot?.totalSeconds, 3600);
expect(sheet.isFirstPassApproved, isTrue);
});
group('computeStreak', () {
test('streak counts weekdays back from today, skipping weekends', () {
// today Mon Sep 28 (no sheet);
// sheets: Wed Sep 23 pending, Thu Sep 24 pending, Fri Sep 25 approved
final sheets = [
makeSheet(id: 'a', workDate: DateTime.utc(2026, 9, 23), status: 'pending'),
makeSheet(id: 'b', workDate: DateTime.utc(2026, 9, 24), status: 'pending'),
makeSheet(id: 'c', workDate: DateTime.utc(2026, 9, 25), status: 'approved'),
];
final result = computeStreak(
sheets: sheets,
leaveDays: {},
today: DateTime.utc(2026, 9, 28),
);
expect(result.current, 3);
});
test('a disapproved day breaks the streak', () {
// Fri Sep 25 disapproved -> streak breaks immediately after skipping today
final sheets = [
makeSheet(id: 'a', workDate: DateTime.utc(2026, 9, 25), status: 'disapproved'),
];
final result = computeStreak(
sheets: sheets,
leaveDays: {},
today: DateTime.utc(2026, 9, 28),
);
expect(result.current, 0);
});
test('approved leave days are neutral', () {
// Thu Sep 24 in leaveDays (no sheet); Wed Sep 23 approved, Fri Sep 25 approved;
// today Mon Sep 28 (no sheet) -> current 2
final sheets = [
makeSheet(id: 'a', workDate: DateTime.utc(2026, 9, 23), status: 'approved'),
makeSheet(id: 'b', workDate: DateTime.utc(2026, 9, 25), status: 'approved'),
];
final result = computeStreak(
sheets: sheets,
leaveDays: {DateTime.utc(2026, 9, 24)},
today: DateTime.utc(2026, 9, 28),
);
expect(result.current, 2);
});
test('best streak finds the longest earlier run', () {
// Earlier run of 4: Mon Sep 7, Tue Sep 8, Wed Sep 9, Thu Sep 10 all approved
// Then gap (Fri Sep 11 missing), later Fri Sep 25 approved (run=1)
// today Mon Sep 28 (no sheet) -> current=1, best=4
final sheets = [
makeSheet(id: 'a', workDate: DateTime.utc(2026, 9, 7), status: 'approved'),
makeSheet(id: 'b', workDate: DateTime.utc(2026, 9, 8), status: 'approved'),
makeSheet(id: 'c', workDate: DateTime.utc(2026, 9, 9), status: 'approved'),
makeSheet(id: 'd', workDate: DateTime.utc(2026, 9, 10), status: 'approved'),
makeSheet(id: 'e', workDate: DateTime.utc(2026, 9, 25), status: 'approved'),
];
final result = computeStreak(
sheets: sheets,
leaveDays: {},
today: DateTime.utc(2026, 9, 28),
);
expect(result.best, 4);
});
});
group('firstPassRate', () {
test('first-pass rate over 30 days', () {
// 2 approved(r=0), 1 approved(r=1), 1 disapproved -> 2 first-pass / 4 reviewed = 0.5
final now = DateTime.utc(2026, 9, 28);
final base = DateTime.utc(2026, 9, 20);
final sheets = [
makeSheet(id: 'a', workDate: base, status: 'approved', resubmissions: 0),
makeSheet(
id: 'b',
workDate: base.add(const Duration(days: 1)),
status: 'approved',
resubmissions: 0),
makeSheet(
id: 'c',
workDate: base.add(const Duration(days: 2)),
status: 'approved',
resubmissions: 1),
makeSheet(
id: 'd',
workDate: base.add(const Duration(days: 3)),
status: 'disapproved',
resubmissions: 0),
];
final stats = computeProgrammerStats(
userId: 'user1',
now: now,
tasks: [],
sheets: sheets,
leaveDays: {},
todayFocus: Duration.zero,
);
expect(stats.firstPassRate, closeTo(0.5, 0.001));
});
test('no reviewed sheets gives null first-pass rate', () {
final stats = computeProgrammerStats(
userId: 'user1',
now: DateTime.utc(2026, 9, 28),
tasks: [],
sheets: [
makeSheet(
id: 'a',
workDate: DateTime.utc(2026, 9, 20),
status: 'draft',
resubmissions: 0),
],
leaveDays: {},
todayFocus: Duration.zero,
);
expect(stats.firstPassRate, isNull);
});
});
test('completedThisWeek uses a Monday-start week', () {
// now = Mon Sep 28, 2026
// week Mon Sep 28 - Sun Oct 4; last week Mon Sep 21 - Sun Sep 27
final now = DateTime.utc(2026, 9, 28);
ProgrammerTask makeTask(String id, DateTime completedAt) {
return ProgrammerTask(
id: id,
title: 'Task $id',
category: ProgrammerTaskCategory.others,
status: ProgrammerTaskStatus.completed,
assigneeId: 'user1',
createdAt: DateTime.utc(2026, 1, 1),
updatedAt: DateTime.utc(2026, 1, 1),
completedAt: completedAt,
);
}
final tasks = [
makeTask('t1', DateTime.utc(2026, 9, 28)), // this week (Mon)
makeTask('t2', DateTime.utc(2026, 9, 29)), // this week (Tue)
makeTask('t3', DateTime.utc(2026, 9, 27)), // last week (Sun Sep 27)
makeTask('t4', DateTime.utc(2026, 9, 21)), // last week (Mon Sep 21)
];
final stats = computeProgrammerStats(
userId: 'user1',
now: now,
tasks: tasks,
sheets: [],
leaveDays: {},
todayFocus: Duration.zero,
);
expect(stats.completedThisWeek, 2);
expect(stats.completedLastWeek, 2);
});
group('computeBadges', () {
test('tiered badge shows highest earned tier and next target', () {
final b =
computeBadges(statsWith(completedTotal: 12), includeSheetBadges: false)
.firstWhere((b) => b.id == 'finisher');
expect(b.earned, isTrue);
expect(b.title, 'Finisher · 10');
expect(b.target, 50);
});
test('includeSheetBadges false hides sheet-based badges', () {
final ids =
computeBadges(statsWith(), includeSheetBadges: false)
.map((b) => b.id)
.toSet();
expect(
ids,
isNot(containsAll(
['deep_work', 'on_a_roll', 'team_player', 'clean_week'])));
});
});
}