feat(app): reviews know how a card was studied; progress can start afresh
Each review records its mode (ADR 0004, rule 3): front to back, back to front with a schedule of its own, or crammed outside the schedule. A card's schedule replays only the reviews of one mode. Resetting progress appends a 'reset' entry per card, no migration (the column is text): schedules replay what follows it, and the history stays for the streak and the heatmap. Reviews are inserted in batches. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
This commit is contained in:
parent
8cf7749ed6
commit
baace1b6e9
2 changed files with 214 additions and 69 deletions
|
|
@ -2,6 +2,7 @@ import 'package:flutter_test/flutter_test.dart';
|
|||
import 'package:halkyon_learn_flutter/src/feature/study/data/review_repository.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/study/model/card_schedule.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/study/model/rating.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/study/model/review.dart';
|
||||
|
||||
import '../../../support/fake_review_gateway.dart';
|
||||
|
||||
|
|
@ -66,14 +67,77 @@ void main() {
|
|||
expect(schedules['new']!.isNew, isTrue);
|
||||
});
|
||||
|
||||
test('skips a rating this app does not know', () async {
|
||||
await gateway.insert(
|
||||
cardId: 'card',
|
||||
rating: 9,
|
||||
reviewedAt: DateTime.utc(2026, 10, 5),
|
||||
durationMs: 0,
|
||||
);
|
||||
test('skips a rating or a mode this app does not know', () async {
|
||||
await gateway.insert([
|
||||
(
|
||||
id: '',
|
||||
cardId: 'card',
|
||||
rating: 9,
|
||||
reviewedAt: DateTime.utc(2026, 10, 5),
|
||||
durationMs: 0,
|
||||
mode: 'review',
|
||||
),
|
||||
(
|
||||
id: '',
|
||||
cardId: 'card',
|
||||
rating: 3,
|
||||
reviewedAt: DateTime.utc(2026, 10, 5),
|
||||
durationMs: 0,
|
||||
mode: 'telepathy',
|
||||
),
|
||||
]);
|
||||
|
||||
expect(await reviews.reviewsOf({'card'}), isEmpty);
|
||||
});
|
||||
|
||||
group('Modes', () {
|
||||
final day = DateTime.utc(2026, 10, 5, 9);
|
||||
|
||||
Future<void> rate(String card, Rating rating, int hours, StudyMode mode) =>
|
||||
reviews.record(
|
||||
cardId: card,
|
||||
rating: rating,
|
||||
reviewedAt: day.add(Duration(hours: hours)),
|
||||
duration: Duration.zero,
|
||||
mode: mode,
|
||||
);
|
||||
|
||||
test('studying back to front has a schedule of its own; cramming moves '
|
||||
'none', () async {
|
||||
await rate('card', Rating.good, 0, StudyMode.review);
|
||||
await rate('card', Rating.again, 1, StudyMode.reverse);
|
||||
await rate('card', Rating.easy, 2, StudyMode.cram);
|
||||
|
||||
final forward = (await reviews.schedulesOf(['card']))['card']!;
|
||||
final backward = (await reviews.schedulesOf([
|
||||
'card',
|
||||
], mode: StudyMode.reverse))['card']!;
|
||||
|
||||
expect(forward.reviewCount, 1);
|
||||
expect(backward.reviewCount, 1);
|
||||
expect(backward.isLearning, isTrue);
|
||||
});
|
||||
|
||||
test('a fresh start makes cards new again in both directions, and '
|
||||
'keeps their reviews', () async {
|
||||
await rate('card', Rating.good, 0, StudyMode.review);
|
||||
await rate('card', Rating.good, 1, StudyMode.reverse);
|
||||
await reviews.reset(['card'], day.add(const Duration(hours: 2)));
|
||||
|
||||
expect((await reviews.schedulesOf(['card']))['card']!.isNew, isTrue);
|
||||
expect(
|
||||
(await reviews.schedulesOf([
|
||||
'card',
|
||||
], mode: StudyMode.reverse))['card']!.isNew,
|
||||
isTrue,
|
||||
);
|
||||
expect(await reviews.reviewsOf({'card'}), hasLength(3));
|
||||
|
||||
await rate('card', Rating.good, 3, StudyMode.review);
|
||||
expect(
|
||||
(await reviews.schedulesOf(['card']))['card']!.reviewCount,
|
||||
1,
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,19 +14,17 @@ class FakeReviewGateway() implements ReviewGateway {
|
|||
];
|
||||
|
||||
@override
|
||||
Future<void> insert({
|
||||
required String cardId,
|
||||
required int rating,
|
||||
required DateTime reviewedAt,
|
||||
required int durationMs,
|
||||
}) async {
|
||||
rows.add((
|
||||
id: 'review-${(_nextId++).toString().padLeft(4, '0')}',
|
||||
cardId: cardId,
|
||||
rating: rating,
|
||||
reviewedAt: reviewedAt,
|
||||
durationMs: durationMs,
|
||||
));
|
||||
Future<void> insert(Iterable<ReviewRow> reviews) async {
|
||||
for (final review in reviews) {
|
||||
rows.add((
|
||||
id: 'review-${(_nextId++).toString().padLeft(4, '0')}',
|
||||
cardId: review.cardId,
|
||||
rating: review.rating,
|
||||
reviewedAt: review.reviewedAt,
|
||||
durationMs: review.durationMs,
|
||||
mode: review.mode,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
|
|
|
|||
Loading…
Reference in a new issue