feat(explore): the device tells the instance whether to count the person, and what their copies come from (ADR 0019, D3)

Count me stays in the person's settings, which follow them to every device
and are now encrypted; after each sync the device mirrors it to the
instance (`learningCount.setCounted`) and, only while it is on, reports
which publications their copies come from, topics with them
(`reportCopies`). Taken back, the instance forgets the copies. Only people
with an identity report, as only they are counted.

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-21 21:40:27 +02:00
commit 2f169ae899
Signed by: raisondetredev
GPG key ID: 07F6FCF405488B64

View file

@ -0,0 +1,90 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:halkyon_learn_flutter/src/core/content/published_ref.dart';
import 'package:halkyon_learn_flutter/src/core/content/rich_content.dart';
import 'package:halkyon_learn_flutter/src/feature/explore/data/learning_counting.dart';
import 'package:halkyon_learn_flutter/src/feature/settings/data/settings_repository.dart';
import 'package:halkyon_learn_flutter/src/feature/studyset/data/studyset_payload.dart';
import 'package:halkyon_learn_flutter/src/feature/studyset/data/studyset_repository.dart';
import 'package:halkyon_learn_flutter/src/feature/studyset/model/studyset_content.dart';
import '../../../support/fake_learning_count_gateway.dart';
import '../../../support/fake_settings_gateway.dart';
import '../../../support/fake_studyset_gateway.dart';
void main() {
late FakeLearningCountGateway instance;
late FakeStudysetGateway studysetRows;
late SettingsRepository settings;
late LearningCounting counting;
setUp(() {
instance = FakeLearningCountGateway();
studysetRows = FakeStudysetGateway();
settings = SettingsRepository(FakeSettingsGateway());
final studysets = StudysetRepository(studysetRows);
counting = LearningCounting(instance, settings, studysets);
addTearDown(() async {
await counting.dispose();
await studysets.dispose();
await settings.dispose();
});
});
/// A studyset copied from [publication] (or the person's own without),
/// inside [parent].
void studyset(String id, {String? publication, String? parent}) =>
studysetRows.receiveRemoteChange(
id,
encodeStudysetPayload(
StudysetContent(
title: id,
description: RichContent.empty(),
published: publication == null
? null
: PublishedRef(publication: publication, item: id, version: 1),
),
),
parentId: parent,
);
test('while the person chose to be counted, the instance counts them and '
'learns which publications their copies come from', () async {
studyset('japanese', publication: 'pub-ja');
studyset('kanji', publication: 'pub-ja', parent: 'japanese');
studyset('greek', publication: 'pub-el');
studyset('mine');
await settings.update(
(settings) => settings.copyWith(countLearning: true),
);
await counting.report();
expect(instance.isCounted, isTrue);
expect(instance.reported, {
'pub-ja': {'japanese', 'kanji'},
'pub-el': {'greek'},
});
});
test('without the choice, nothing about copies reaches the instance, and '
'a choice taken back forgets them there', () async {
studyset('japanese', publication: 'pub-ja');
await counting.report();
expect(instance.isCounted, isFalse);
expect(instance.reported, isNull);
await settings.update(
(settings) => settings.copyWith(countLearning: true),
);
await counting.report();
expect(instance.reported, isNotNull);
await settings.update(
(settings) => settings.copyWith(countLearning: false),
);
await counting.report();
expect(instance.isCounted, isFalse);
expect(instance.reported, isNull);
});
}