halkyon/halkyon_learn_flutter/test/support/fake_feedback_gateways.dart
Tizian 「ティツィアーン」 9accc95df2
feat(feedback): reports and suggestions on published studysets, gathered by the instance
Stage 4 of the public tier (ADR 0005, rule 6), its server and the app's
data. A learner writes a report or a suggestion on a published studyset,
a card, note or step of it, in their own synced space, so it works
offline; replies and the author's decisions (closed, hidden, accepted,
declined) likewise, in the writer's or the author's space. The instance
gathers them: public to everyone signed in, but for what the author
hides; for the author's inbox and the writer's. What counts is decided
as rows are read, since a modified app could write anything: threads
from people with an identity and a public profile the author hasn't
blocked, replies from the writer and the author alone, decisions from
the author alone, no suggestion under "no derivatives". The author gets
the files a suggestion names from its writer's, and the next release
credits the writers of the suggestions accepted since the last
(Co-authored-by). Deleting a person deletes their blocks and what they
read; what they wrote goes with their space.

In the app, the repository writes reports and suggestions (a card, note
or step as the learner has it, without what is theirs alone, with the
files it names) and takes an accepted suggestion into the author's own
card, note or step.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MMm7usAo6q9ZZRoPkWjoxi
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-10 09:46:42 +02:00

116 lines
3.2 KiB
Dart

import 'dart:convert';
import 'dart:typed_data';
import 'package:halkyon_learn_flutter/src/feature/feedback/data/feedback_gateway.dart';
import 'package:halkyon_learn_flutter/src/feature/feedback/model/feedback.dart';
/// A [FeedbackWriteGateway] in memory: what was written, in order.
class FakeFeedbackWriteGateway() implements FeedbackWriteGateway {
/// The threads written: their published studyset, kind and payload JSON.
final threads =
<
({String publicationId, FeedbackKind kind, Map<String, Object?> json})
>[];
/// The replies written, by thread.
final messages = <({String threadId, Map<String, Object?> json})>[];
/// The decisions written, by thread.
final decisions = <({String threadId, FeedbackDecision decision})>[];
@override
Future<String> insertThread({
required String publicationId,
required FeedbackKind kind,
required ByteData payload,
}) async {
threads.add((
publicationId: publicationId,
kind: kind,
json: _json(payload),
));
return 'thread-${threads.length}';
}
@override
Future<void> insertMessage(String threadId, ByteData payload) async =>
messages.add((threadId: threadId, json: _json(payload)));
@override
Future<void> insertDecision(
String threadId,
FeedbackDecision decision,
) async => decisions.add((threadId: threadId, decision: decision));
static Map<String, Object?> _json(ByteData payload) =>
jsonDecode(utf8.decode(Uint8List.sublistView(payload)))
as Map<String, Object?>;
}
/// A [FeedbackServerGateway] in memory: [threads] as the instance gathers
/// them (all public, for the inbox too), [files] by SHA-256.
class FakeFeedbackServerGateway({
final List<FeedbackThread> threads = const [],
final Map<String, Uint8List> files = const {},
}) implements FeedbackServerGateway {
late var _threads = [...threads];
/// The threads read, and the people blocked.
final seen = <String>[];
final blocked = <String>[];
/// When set, every request fails with it.
Exception? fail;
/// Replaces what the instance gathers.
set gathered(List<FeedbackThread> threads) => _threads = [...threads];
Future<void> _maybeFail() async {
if (fail case final error?) {
throw error;
}
}
@override
Future<List<FeedbackThread>> threadsOf(String publicId) async {
await _maybeFail();
return [
for (final thread in _threads)
if (thread.publicId == publicId) thread,
];
}
@override
Future<List<FeedbackThread>> inbox() async {
await _maybeFail();
return [
for (final thread in _threads)
if (thread.mine || thread.forMe) thread,
];
}
@override
Future<int> unseen() async =>
(await inbox()).where((thread) => thread.unseen).length;
@override
Future<void> markSeen(String threadId) async {
await _maybeFail();
seen.add(threadId);
}
@override
Future<void> block(String handle) async => blocked.add(handle);
@override
Future<void> unblock(String handle) async => blocked.remove(handle);
@override
Future<List<String>> blockedPeople() async => [...blocked];
@override
Future<Uint8List> file(String threadId, String sha256) async {
await _maybeFail();
return files[sha256]!;
}
}