halkyon/halkyon_learn_flutter/test/support/test_dependencies.dart
Tizian 「ティツィアーン」 8dde0fa42a
feat(assistant): a question opens in the AI assistant chosen, Proton's Lumo by default
Questions asked from the app went to Claude alone. Settings › Assistant
now chooses where they open (decided 2026-10-14): Lumo, the default, by
its guest chat with the question in the address's fragment (Lumo's
`prefill`, which needs no account and reaches no server; the address for
one's own account loses the question when nobody is signed in), ChatGPT,
Claude, Mistral, Perplexity, Duck.ai, Grok, Kagi Assistant, HuggingChat,
or an address of one's own with %s. Each link fills the question in for
the person to send (Perplexity answers at once, Grok asks first); Gemini,
which has no such link, and Copilot, whose stopped working, are left out.
The field names the assistant ("Ask Lumo…"). A device that sent questions
to Claude goes on asking an assistant, now Lumo. Connecting Claude to read
studysets stays as it was.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-14 14:35:35 +02:00

369 lines
17 KiB
Dart

import 'dart:io';
import 'dart:typed_data';
import 'package:halkyon_learn_flutter/src/core/asset/data/asset_file_cache.dart';
import 'package:halkyon_learn_flutter/src/core/asset/data/asset_gateway.dart';
import 'package:halkyon_learn_flutter/src/core/asset/data/asset_repository.dart';
import 'package:halkyon_learn_flutter/src/core/asset/data/asset_server_gateway.dart';
import 'package:halkyon_learn_flutter/src/core/asset/data/sound_player.dart';
import 'package:halkyon_learn_flutter/src/core/dependencies/dependencies.dart';
import 'package:halkyon_learn_flutter/src/core/dependencies/person_dependencies.dart';
import 'package:halkyon_learn_flutter/src/core/device_settings/data/device_settings_store.dart';
import 'package:halkyon_learn_flutter/src/core/device_settings/model/device_settings.dart';
import 'package:halkyon_learn_flutter/src/core/files/file_picker.dart';
import 'package:halkyon_learn_flutter/src/core/files/file_saver.dart';
import 'package:halkyon_learn_flutter/src/core/sync/pending_changes_store.dart';
import 'package:halkyon_learn_flutter/src/core/sync/sync_service.dart';
import 'package:halkyon_learn_flutter/src/core/version/app_version.dart';
import 'package:halkyon_learn_flutter/src/core/video/media_players.dart';
import 'package:halkyon_learn_flutter/src/core/video/youtube_videos.dart';
import 'package:halkyon_learn_flutter/src/feature/about/data/about_gateway.dart';
import 'package:halkyon_learn_flutter/src/feature/about/data/about_repository.dart';
import 'package:halkyon_learn_flutter/src/feature/anki/data/anki_package.dart';
import 'package:halkyon_learn_flutter/src/feature/anki/data/anki_package_reader.dart';
import 'package:halkyon_learn_flutter/src/feature/archive/data/archive_reader.dart';
import 'package:halkyon_learn_flutter/src/feature/archive/model/import_archive.dart';
import 'package:halkyon_learn_flutter/src/feature/assistant/data/assistant_gateway.dart';
import 'package:halkyon_learn_flutter/src/feature/assistant/data/assistant_hand_off.dart';
import 'package:halkyon_learn_flutter/src/feature/assistant/data/assistant_repository.dart';
import 'package:halkyon_learn_flutter/src/feature/assistant/data/web_search.dart';
import 'package:halkyon_learn_flutter/src/feature/auth/data/auth_gateway.dart';
import 'package:halkyon_learn_flutter/src/feature/auth/data/auth_repository.dart';
import 'package:halkyon_learn_flutter/src/feature/card/data/card_deletions.dart';
import 'package:halkyon_learn_flutter/src/feature/card/data/card_gateway.dart';
import 'package:halkyon_learn_flutter/src/feature/card/data/card_repository.dart';
import 'package:halkyon_learn_flutter/src/feature/dictionary/data/dictionary_gateway.dart';
import 'package:halkyon_learn_flutter/src/feature/dictionary/data/dictionary_repository.dart';
import 'package:halkyon_learn_flutter/src/feature/explore/data/copy_updater.dart';
import 'package:halkyon_learn_flutter/src/feature/explore/data/explore_gateway.dart';
import 'package:halkyon_learn_flutter/src/feature/explore/data/explore_repository.dart';
import 'package:halkyon_learn_flutter/src/feature/explore/data/release_copier.dart';
import 'package:halkyon_learn_flutter/src/feature/export/data/export_archive.dart';
import 'package:halkyon_learn_flutter/src/feature/export/data/studyset_markdown.dart';
import 'package:halkyon_learn_flutter/src/feature/feedback/data/feedback_gateway.dart';
import 'package:halkyon_learn_flutter/src/feature/feedback/data/feedback_repository.dart';
import 'package:halkyon_learn_flutter/src/feature/geo_map/data/geo_map_repository.dart';
import 'package:halkyon_learn_flutter/src/feature/jellyfin/data/jellyfin_gateway.dart';
import 'package:halkyon_learn_flutter/src/feature/jellyfin/data/jellyfin_repository.dart';
import 'package:halkyon_learn_flutter/src/feature/jellyfin/data/jellyfin_store.dart';
import 'package:halkyon_learn_flutter/src/feature/jellyfin/data/jellyfin_suggestion.dart';
import 'package:halkyon_learn_flutter/src/feature/kanji/data/stroke_repository.dart';
import 'package:halkyon_learn_flutter/src/feature/learning_path/data/learning_path_deletions.dart';
import 'package:halkyon_learn_flutter/src/feature/learning_path/data/learning_path_gateway.dart';
import 'package:halkyon_learn_flutter/src/feature/learning_path/data/learning_path_repository.dart';
import 'package:halkyon_learn_flutter/src/feature/learning_path/data/path_answer_gateway.dart';
import 'package:halkyon_learn_flutter/src/feature/learning_path/data/path_progress_repository.dart';
import 'package:halkyon_learn_flutter/src/feature/note/data/note_deletions.dart';
import 'package:halkyon_learn_flutter/src/feature/note/data/note_gateway.dart';
import 'package:halkyon_learn_flutter/src/feature/note/data/note_repository.dart';
import 'package:halkyon_learn_flutter/src/feature/pdf_source/data/pdf_deletions.dart';
import 'package:halkyon_learn_flutter/src/feature/pdf_source/data/pdf_engine.dart';
import 'package:halkyon_learn_flutter/src/feature/pdf_source/data/pdf_source_gateway.dart';
import 'package:halkyon_learn_flutter/src/feature/pdf_source/data/pdf_source_repository.dart';
import 'package:halkyon_learn_flutter/src/feature/pomodoro/data/pomodoro_platform.dart';
import 'package:halkyon_learn_flutter/src/feature/pomodoro/data/pomodoro_run_store.dart';
import 'package:halkyon_learn_flutter/src/feature/profile/data/profile_gateway.dart';
import 'package:halkyon_learn_flutter/src/feature/profile/data/profile_repository.dart';
import 'package:halkyon_learn_flutter/src/feature/publish/data/publish_gateway.dart';
import 'package:halkyon_learn_flutter/src/feature/publish/data/publish_repository.dart';
import 'package:halkyon_learn_flutter/src/feature/publish/data/published_store.dart';
import 'package:halkyon_learn_flutter/src/feature/settings/data/settings_gateway.dart';
import 'package:halkyon_learn_flutter/src/feature/settings/data/settings_repository.dart';
import 'package:halkyon_learn_flutter/src/feature/study/data/review_gateway.dart';
import 'package:halkyon_learn_flutter/src/feature/study/data/review_repository.dart';
import 'package:halkyon_learn_flutter/src/feature/studyset/data/studyset_deletions.dart';
import 'package:halkyon_learn_flutter/src/feature/studyset/data/studyset_gateway.dart';
import 'package:halkyon_learn_flutter/src/feature/studyset/data/studyset_repository.dart';
import 'package:halkyon_learn_flutter/src/feature/studyset/data/studyset_tabs.dart';
import 'package:halkyon_learn_flutter/src/feature/tag/data/tag_gateway.dart';
import 'package:halkyon_learn_flutter/src/feature/tag/data/tag_repository.dart';
import 'fake_about_gateway.dart';
import 'fake_asset_gateway.dart';
import 'fake_asset_server_gateway.dart';
import 'fake_assistant_gateway.dart';
import 'fake_auth_gateway.dart';
import 'fake_card_gateway.dart';
import 'fake_dictionary_gateway.dart';
import 'fake_explore_gateway.dart';
import 'fake_feedback_gateways.dart';
import 'fake_file_picker.dart';
import 'fake_file_saver.dart';
import 'fake_jellyfin.dart';
import 'fake_learning_path_gateway.dart';
import 'fake_note_gateway.dart';
import 'fake_path_answer_gateway.dart';
import 'fake_pdf_engine.dart';
import 'fake_pdf_source_gateway.dart';
import 'fake_pomodoro_platform.dart';
import 'fake_profile_gateway.dart';
import 'fake_publish_gateway.dart';
import 'fake_review_gateway.dart';
import 'fake_settings_gateway.dart';
import 'fake_sound_player.dart';
import 'fake_studyset_gateway.dart';
import 'fake_tag_gateway.dart';
import 'fake_youtube_videos.dart';
import 'memory_asset_file_cache.dart';
import 'memory_device_settings_store.dart';
import 'memory_pomodoro_run_store.dart';
import 'memory_published_store.dart';
import 'memory_studyset_tab_store.dart';
/// The app's dependencies with fakes instead of the server and the device's
/// database; each test passes in only the fakes it looks at.
///
/// Every person gets [studysetGateway], [cardGateway], [noteGateway],
/// [pathGateway], [answerGateway], [tagGateway], [settingsGateway] and the
/// asset fakes (or fresh fakes of their own),
/// unless [openPerson] replaces how a person's dependencies are opened.
///
/// The device starts with [deviceSettings], kept in memory unless a test
/// passes the [deviceSettingsStore] to look at.
///
/// A person's sync runs [syncOnce] (by default, going through without doing
/// anything): once when their data opens if a test passes it, and whenever
/// the person asks; never on timers, which a widget test would find pending.
Dependencies testDependencies({
AuthGateway? authGateway,
StudysetGateway? studysetGateway,
CardGateway? cardGateway,
NoteGateway? noteGateway,
LearningPathGateway? pathGateway,
PathAnswerGateway? answerGateway,
PdfSourceGateway? pdfGateway,
ReviewGateway? reviewGateway,
TagGateway? tagGateway,
GeoMapRepository? maps,
StrokeRepository? strokes,
SoundPlayer? sounds,
FilePicker? files,
FileSaver? fileSaver,
YoutubeVideos? videos,
PdfEngine? pdfEngine,
SettingsGateway? settingsGateway,
AssetGateway? assetGateway,
AssetServerGateway? assetServer,
AssetFileCache? assetFiles,
AboutGateway? aboutGateway,
DictionaryGateway? dictionaryGateway,
DeviceSettingsStore? deviceSettingsStore,
DeviceSettings deviceSettings = const DeviceSettings(),
AppVersion appVersion = const AppVersion(version: '1.0.0', build: '1'),
PomodoroPlatform? pomodoro,
PomodoroRunStore? pomodoroRuns,
ProfileGateway? profileGateway,
PublishGateway? publishGateway,
PublishedStore? publishedStore,
StudysetTabStore? studysetTabStore,
ExploreGateway? exploreGateway,
FeedbackWriteGateway? feedbackWriteGateway,
FeedbackServerGateway? feedbackServerGateway,
SyncOnce? syncOnce,
PendingChangesStore? pendingChanges,
OpenPerson? openPerson,
AssistantHandOff? assistantHandOff,
WebSearch? webSearch,
AssistantGateway? assistantGateway,
JellyfinGateway? jellyfinGateway,
JellyfinStore? jellyfinStore,
JellyfinSuggestion? jellyfinSuggestion,
MediaPlayers? mediaPlayers,
}) => Dependencies(
serverName: 'learn.example.org',
auth: AuthRepository(authGateway ?? FakeAuthGateway()),
about: AboutRepository(aboutGateway ?? FakeAboutGateway()),
dictionaries: DictionaryRepository(
dictionaryGateway ?? FakeDictionaryGateway(),
),
maps: maps ?? GeoMapRepository(),
strokes: strokes ?? StrokeRepository(),
sounds: sounds ?? FakeSoundPlayer(),
files: files ?? FakeFilePicker(),
fileSaver: fileSaver ?? FakeFileSaver(),
videos: videos ?? FakeYoutubeVideos(),
pdfEngine: pdfEngine ?? FakePdfEngine(),
ankiPackages: InlineAnkiPackageReader(),
archives: InlineArchiveReader(),
exportArchiver: InlineExportArchiver(),
deviceSettingsStore:
deviceSettingsStore ?? MemoryDeviceSettingsStore(deviceSettings),
deviceSettings: deviceSettings,
appVersion: appVersion,
pomodoro: pomodoro ?? FakePomodoroPlatform(),
assistantHandOff:
assistantHandOff ?? AssistantHandOff(open: (url) async => true),
webSearch:
webSearch ??
WebSearch(
open: (url) async => true,
deviceSearch: (query) async => false,
),
assistant: AssistantRepository(assistantGateway ?? FakeAssistantGateway()),
jellyfin: JellyfinRepository(
(device) => jellyfinGateway ?? FakeJellyfinGateway(),
jellyfinStore ?? MemoryJellyfinStore(),
deviceName: 'Test',
appVersion: appVersion.version,
),
jellyfinSuggestion:
jellyfinSuggestion ?? JellyfinSuggestion(() async => null),
mediaPlayers: mediaPlayers ?? FakeMediaPlayers(),
openPerson:
openPerson ??
(person) async {
final sync = SyncService(
syncOnce ?? ({required onMerge}) async {},
pendingChanges: pendingChanges,
);
if (syncOnce != null) {
sync.syncNow();
}
final studysets = StudysetRepository(
studysetGateway ?? FakeStudysetGateway(),
);
final cards = CardRepository(cardGateway ?? FakeCardGateway());
final notes = NoteRepository(noteGateway ?? FakeNoteGateway());
final paths = LearningPathRepository(
pathGateway ?? FakeLearningPathGateway(),
);
final pathProgress = PathProgressRepository(
answerGateway ?? FakePathAnswerGateway(),
);
final tags = TagRepository(
tagGateway ?? FakeTagGateway(),
cardChanges: cards.changes,
noteChanges: notes.changes,
pathChanges: paths.changes,
);
final settings = SettingsRepository(
settingsGateway ?? FakeSettingsGateway(),
);
final reviews = ReviewRepository(reviewGateway ?? FakeReviewGateway());
final assets = AssetRepository(
assetGateway ?? FakeAssetGateway(),
assetServer ?? FakeAssetServerGateway(),
assetFiles ?? MemoryAssetFileCache(),
);
final pdfSources = PdfSourceRepository(
pdfGateway ?? FakePdfSourceGateway(),
assets,
);
final deletions = StudysetDeletions(
studysets,
cards,
notes,
paths,
pdfSources,
);
final noteDeletions = NoteDeletions(notes);
final cardDeletions = CardDeletions(cards);
final pathDeletions = LearningPathDeletions(paths);
final stepDeletions = StepDeletions(paths);
final pdfDeletions = PdfDeletions(pdfSources);
final studysetTabs = StudysetTabs(
studysetTabStore ?? MemoryStudysetTabStore(),
);
await studysetTabs.open();
return PersonDependencies(
sync: sync,
studysets: studysets,
cards: cards,
notes: notes,
paths: paths,
pathProgress: pathProgress,
pdfSources: pdfSources,
reviews: reviews,
tags: tags,
settings: settings,
assets: assets,
deletions: deletions,
noteDeletions: noteDeletions,
cardDeletions: cardDeletions,
pathDeletions: pathDeletions,
stepDeletions: stepDeletions,
pdfDeletions: pdfDeletions,
pomodoroRuns: pomodoroRuns ?? MemoryPomodoroRunStore(),
studysetTabs: studysetTabs,
// The picture as it is: drawing it anew is real work a widget
// test's fake clock can't wait for (avatar_image_test.dart has it).
profile: ProfileRepository(
profileGateway ?? FakeProfileGateway(),
drawAvatar: (picture) async => picture,
),
publishing: PublishRepository(
publishGateway ?? FakePublishGateway(),
publishedStore ?? MemoryPublishedStore(),
),
explore: ExploreRepository(
exploreGateway ?? FakeExploreGateway(),
ReleaseCopier(
studysets,
cards,
notes,
paths,
pdfSources,
tags,
assets,
),
CopyUpdater(
studysets,
cards,
notes,
paths,
pdfSources,
tags,
assets,
),
studysets,
),
feedback: FeedbackRepository(
feedbackWriteGateway ?? FakeFeedbackWriteGateway(),
feedbackServerGateway ?? FakeFeedbackServerGateway(),
cards,
notes,
paths,
assets,
),
onDispose: () async {
await deletions.dispose();
await noteDeletions.dispose();
await cardDeletions.dispose();
await pathDeletions.dispose();
await stepDeletions.dispose();
await pdfDeletions.dispose();
await studysets.dispose();
await tags.dispose();
await settings.dispose();
await reviews.dispose();
await cards.dispose();
await notes.dispose();
await paths.dispose();
await pathProgress.dispose();
await pdfSources.dispose();
await assets.dispose();
await sync.dispose();
},
);
},
);
/// Reads Anki packages where it is asked to: a widget test's fake clock
/// can't wait for another isolate.
class InlineAnkiPackageReader() implements AnkiPackageReader {
@override
Future<AnkiPackage> read(Uint8List bytes) async =>
readAnkiPackage(bytes, temp: Directory.systemTemp);
}
/// Reads archives where it is asked to, as [InlineAnkiPackageReader] reads.
class InlineArchiveReader() implements ArchiveReader {
@override
Future<ImportArchive> read(Uint8List bytes) async => readArchive(bytes);
}
/// Packs exports where it is asked to, as [InlineAnkiPackageReader] reads.
class InlineExportArchiver() implements ExportArchiver {
@override
Future<Uint8List> zip(List<ExportFile> files) async => zipExport(files);
}