chore: Halkyon Learn moves into apps/learn

Its server, client, app and curator, and the tools that write its
assets (maps, Anki fixtures, icons), as they are; paths are fixed in
the commits that follow.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
This commit is contained in:
Tizian 「ティツィアーン」 2026-10-19 20:08:06 +02:00
commit 649499be23
Signed by: raisondetredev
GPG key ID: 07F6FCF405488B64

View file

@ -0,0 +1,128 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:halkyon_learn_flutter/src/feature/home/model/study_activity.dart';
import 'package:halkyon_learn_flutter/src/feature/home/model/study_goal.dart';
void main() {
// A Wednesday, in the afternoon, local time.
final now = DateTime(2026, 10, 7, 15);
DateTime day(int d) => DateTime(2026, 10, d);
DateTime at(int d, [int hour = 10]) => DateTime(2026, 10, d, hour);
StudyActivity studied(List<int> days) =>
StudyActivity.of(ratings: [for (final d in days) at(d)], written: []);
test('counts ratings and cards written by the local day they fell on', () {
final activity = StudyActivity.of(
ratings: [at(5, 9), at(5, 23)],
written: [at(6)],
);
expect(activity.on(day(5)), 2);
expect(activity.on(day(6)), 1);
expect(activity.studiedOn(day(4)), isFalse);
});
test('starts on the first day anything happened, imports included, which '
"don't count as studying", () {
final activity = StudyActivity.of(
ratings: [at(6)],
written: [at(5)],
imported: [at(4, 23)],
);
expect(activity.start, day(4));
expect(activity.studiedOn(day(4)), isFalse);
expect(StudyActivity.none.start, isNull);
});
group('The streak', () {
test('runs up to today once studied today', () {
expect(studyStreak(studied([5, 6, 7]), {}, now), 3);
});
test('runs up to yesterday while today is still open', () {
expect(studyStreak(studied([5, 6]), {}, now), 2);
});
test('is broken by a day without studying or resting', () {
expect(studyStreak(studied([4, 6, 7]), {}, now), 2);
expect(studyStreak(studied([4, 5]), {}, now), 0);
});
test('keeps going over a rest day, which counts', () {
expect(studyStreak(studied([4, 6, 7]), {day(5)}, now), 4);
});
});
test('a week runs Monday to Sunday, each day as it stands', () {
final week = weekOf(day(7), studied([5, 7]), {day(6)}, now);
expect(week.map((each) => each.day.day), [5, 6, 7, 8, 9, 10, 11]);
expect(week.map((each) => each.state), [
DayState.studied,
DayState.rested,
DayState.studied,
DayState.upcoming,
DayState.upcoming,
DayState.upcoming,
DayState.upcoming,
]);
expect(
weekOf(day(7), studied([5]), {}, now).take(3).map((each) => each.state),
[DayState.studied, DayState.missed, DayState.today],
);
});
test('days before the first activity have nothing to miss; a rest day '
'still shows', () {
expect(
weekOf(day(7), studied([6]), {}, now).take(3).map((each) => each.state),
[DayState.notStarted, DayState.studied, DayState.today],
);
expect(
weekOf(day(7), studied([]), {day(5)}, now).take(3).map((e) => e.state),
[DayState.rested, DayState.notStarted, DayState.today],
);
});
test('heat starts at one card and darkens in coarse steps', () {
expect([0, 1, 4, 5, 9, 10, 19, 20].map(heatLevel), [
0,
1,
1,
2,
2,
3,
3,
4,
]);
});
test('cards fall due on their day; overdue ones today', () {
expect(dueByDay([at(1), at(7), at(9), at(9, 20)], day(7)), {
day(7): 2,
day(9): 2,
});
});
test("a goal's progress counts the ratings of its day or week", () {
final ratings = [at(4), at(5), at(6), at(7), at(7, 14)];
expect(
goalProgress(
const StudyGoal(period: GoalPeriod.day, target: 20),
ratings,
now,
),
2,
);
expect(
goalProgress(
const StudyGoal(period: GoalPeriod.week, target: 20),
ratings,
now,
),
4,
);
});
}