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,119 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:halkyon_learn_flutter/src/core/content/payload.dart';
import 'package:halkyon_learn_flutter/src/feature/settings/data/settings_payload.dart';
import 'package:halkyon_learn_flutter/src/feature/settings/data/settings_repository.dart';
import 'package:halkyon_learn_flutter/src/feature/settings/model/person_settings.dart';
import '../../../support/fake_settings_gateway.dart';
void main() {
late FakeSettingsGateway gateway;
late SettingsRepository repository;
setUp(() {
gateway = FakeSettingsGateway();
repository = SettingsRepository(gateway);
});
tearDown(() => repository.dispose());
const verb = (id: 'tag-1', name: 'verb');
const noun = (id: 'tag-2', name: 'noun');
test('a person without settings has the defaults, and nothing is '
'stored for reading them', () async {
expect(await repository.read(), const PersonSettings());
expect(gateway.writes, isEmpty);
});
test('stores a change in one row, made by the first change', () async {
await repository.update((s) => s.withStudyTags('japanese', [verb]));
await repository.update((s) => s.withStudyTags('german', [noun]));
expect(
await repository.read(),
const PersonSettings(
studyTags: {
'japanese': [verb],
'german': [noun],
},
),
);
expect({for (final (id, _) in gateway.writes) id}, hasLength(1));
});
test('two rows from two devices fold into the one with the smaller id, '
"with the other's settings added where it has none", () async {
await repository.update((s) => s.withStudyTags('japanese', [verb]));
gateway.receiveRemoteRow(
'settings-000',
encodeSettingsPayload(
const PersonSettings(
studyTags: {
'japanese': [noun],
'german': [noun],
},
),
),
);
final settings = await repository.read();
expect(
settings,
const PersonSettings(
studyTags: {
'japanese': [noun],
'german': [noun],
},
),
);
expect(gateway.deleted, ['settings-001']);
// Blanked before deleted.
expect(gateway.writes.last, ('settings-001', redactedPayload));
});
test('leaves settings of a newer app version alone, and keeps its own '
'beside them', () async {
gateway.receiveRemoteRow(
'settings-000',
encodePayload(settingsSchemaVersion + 1, {'studyTags': {}}),
);
await repository.update((s) => s.withStudyTags('japanese', [verb]));
expect(
(await repository.read()).studyTagsOf('japanese'),
[verb],
);
expect(gateway.deleted, isEmpty);
expect(gateway.writes.single.$1, isNot('settings-000'));
});
test('follows changes on this device and from others', () async {
final watched = repository.watch();
final expectation = expectLater(
watched.map((s) => s.studyTags.keys.toList()),
emitsInOrder([
<String>[],
['japanese'],
['japanese', 'german'],
]),
);
await Future<void>.delayed(Duration.zero);
await repository.update((s) => s.withStudyTags('japanese', [verb]));
final id = gateway.writes.single.$1;
gateway.receiveRemoteRow(
id,
encodeSettingsPayload(
const PersonSettings(
studyTags: {
'japanese': [verb],
'german': [noun],
},
),
),
);
await expectation;
});
}