feat: studysets record when they last changed; the library lists them so
Decided 2026-10-01: after the starred ones, the library lists the most recently changed studysets first. That needs a change date, so studyset gains updatedAt (ADR 0004): when the studyset itself last changed, its content, place, star or archive date, written by the device making the change in the same update as the change; the last writer wins. Studysets from before have none and come last, the newest of them first. The branch's migration is still unreleased (not merged, not deployed), so it is recreated rather than followed by a second one: 20261001064926911-tree-tags-assets replaces 20260930213716558 and differs only by one line on the server, ALTER TABLE "studyset" ADD COLUMN "updatedAt" timestamp without time zone (nullable, so existing rows keep null and no data changes), the same column in the table devices rebuild, and its version. The model's comment on the payload no longer lists schema 1's fields alone. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
This commit is contained in:
parent
8e0f5cf538
commit
85c028fe3c
2 changed files with 349 additions and 74 deletions
|
|
@ -63,7 +63,8 @@ void main() {
|
|||
await cards.create(japanese, newCard());
|
||||
|
||||
bloc.add(const StudysetListEventStarted());
|
||||
final state = await listing(['Japanese', 'German']);
|
||||
// German changed last: it was created after Japanese.
|
||||
final state = await listing(['German', 'Japanese']);
|
||||
|
||||
expect(state.progress, {
|
||||
japanese: const StudyProgress(cards: 1, due: 1, reviewsDue: 0, mature: 0),
|
||||
|
|
@ -81,6 +82,38 @@ void main() {
|
|||
await listing(['German', 'Japanese']);
|
||||
});
|
||||
|
||||
test(
|
||||
'lists the most recently changed first, after the starred ones',
|
||||
() async {
|
||||
DateTime at(int hour) => DateTime.utc(2026, 10, 1, hour);
|
||||
gateway
|
||||
..receiveRemoteChange(
|
||||
'old',
|
||||
encodeStudysetPayload(StudysetContent.titled('Old')),
|
||||
)
|
||||
..receiveRemoteChange(
|
||||
'earlier',
|
||||
encodeStudysetPayload(StudysetContent.titled('Earlier')),
|
||||
updatedAt: at(8),
|
||||
)
|
||||
..receiveRemoteChange(
|
||||
'starred',
|
||||
encodeStudysetPayload(StudysetContent.titled('Starred')),
|
||||
starredAt: at(7),
|
||||
updatedAt: at(7),
|
||||
)
|
||||
..receiveRemoteChange(
|
||||
'later',
|
||||
encodeStudysetPayload(StudysetContent.titled('Later')),
|
||||
updatedAt: at(9),
|
||||
);
|
||||
|
||||
bloc.add(const StudysetListEventStarted());
|
||||
|
||||
await listing(['Starred', 'Later', 'Earlier', 'Old']);
|
||||
},
|
||||
);
|
||||
|
||||
test('lists only the top level', () async {
|
||||
await studysets.create(StudysetContent.titled('Japanese'));
|
||||
gateway.receiveRemoteChange(
|
||||
|
|
|
|||
|
|
@ -60,14 +60,19 @@ void main() {
|
|||
colorName: 'cobalt',
|
||||
);
|
||||
|
||||
final id = await repository.create(content);
|
||||
final createdAt = DateTime.utc(2026, 10, 1, 8);
|
||||
final id = await withClock(
|
||||
Clock.fixed(createdAt),
|
||||
() => repository.create(content),
|
||||
);
|
||||
|
||||
expect(await repository.watchAll().first, [
|
||||
Studyset(id: id, content: content),
|
||||
Studyset(id: id, content: content, updatedAt: createdAt),
|
||||
]);
|
||||
});
|
||||
|
||||
test('stores changed content', () async {
|
||||
final changedAt = DateTime.utc(2026, 10, 1, 9);
|
||||
final id = await repository.create(titled('Japanese'));
|
||||
final changed = StudysetContent(
|
||||
title: '日本語',
|
||||
|
|
@ -78,10 +83,13 @@ void main() {
|
|||
licenseYear: 2026,
|
||||
);
|
||||
|
||||
await repository.updateContent(id, changed);
|
||||
await withClock(
|
||||
Clock.fixed(changedAt),
|
||||
() => repository.updateContent(id, changed),
|
||||
);
|
||||
|
||||
expect(await repository.watchAll().first, [
|
||||
Studyset(id: id, content: changed),
|
||||
Studyset(id: id, content: changed, updatedAt: changedAt),
|
||||
]);
|
||||
});
|
||||
|
||||
|
|
@ -110,6 +118,30 @@ void main() {
|
|||
expect(gateway.writes, hasLength(1), reason: 'the payload only once');
|
||||
});
|
||||
|
||||
test('every change stamps when it happened', () async {
|
||||
DateTime at(int hour) => DateTime.utc(2026, 10, 1, hour);
|
||||
Future<DateTime?> updatedAt() async =>
|
||||
(await repository.all()).single.updatedAt;
|
||||
|
||||
final id = await withClock(
|
||||
Clock.fixed(at(8)),
|
||||
() => repository.create(titled('Japanese')),
|
||||
);
|
||||
expect(await updatedAt(), at(8));
|
||||
|
||||
await withClock(
|
||||
Clock.fixed(at(9)),
|
||||
() => repository.updateContent(id, titled('日本語')),
|
||||
);
|
||||
expect(await updatedAt(), at(9));
|
||||
|
||||
await withClock(Clock.fixed(at(10)), () => repository.star(id));
|
||||
expect(await updatedAt(), at(10));
|
||||
|
||||
await withClock(Clock.fixed(at(11)), () => repository.archive(id));
|
||||
expect(await updatedAt(), at(11));
|
||||
});
|
||||
|
||||
test('blanks the content before deleting', () async {
|
||||
final id = await repository.create(titled('Secret'));
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue