feat(learn): Learn's agent, an MCP server suggesting in shared studysets
Some checks failed
CI / check (push) Failing after 1m56s

Halkyon's ADR 0044, step 6c (D7, D49 to D52). apps/learn/halkyon_learn_agent
is one executable, `halkyon-agent --instance URL sign-in | sync | mcp`:

- sign-in keeps the token read from stdin, signs in with it, makes the
  agent's key pair on its first sign-in, keeps it before setting the
  public halves on the register (agents.setKeys), and syncs;
- it opens its data as the app's _openPersonData does, on
  halkyon_learn_core and halkyon_content: its own SQLite database, the
  sync's space router, its keys and those of the spaces shared with it,
  and the repositories of studysets, cards, notes, paths, files and
  feedback;
- mcp serves ten tools over stdio (dart_mcp): list, read and search the
  studysets shared with it, suggest a change or a new card or note,
  report, list a studyset's suggestions and reply. Each syncs first and
  a suggestion again at once; a refusal is the tool's error;
- its files live in $XDG_CONFIG_HOME/halkyon-agent/<host>, 0700 and
  0600, refused when others may read them (D50);
- tool/install.sh builds it with `dart build cli` (libsodium and SQLite
  bundled) and copies it to its user on the host, the build before kept
  (D52).

The synced gateways of learning paths and files, and the fakes of the
core's gateways, move into halkyon_learn_core (its testing.dart), which
the app's tests and the agent's share.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
This commit is contained in:
Tizian 「ティツィアーン」 2026-11-11 22:06:29 +01:00
commit 54e212a51c
Signed by: raisondetredev
GPG key ID: 07F6FCF405488B64
103 changed files with 2925 additions and 314 deletions

View file

@ -1,132 +0,0 @@
import 'dart:typed_data';
import 'package:halkyon_app/halkyon_app.dart';
import 'package:halkyon_learn_client/halkyon_learn_client.dart' as protocol;
import 'package:halkyon_learn_core/core/asset/data/asset_gateway.dart';
import 'package:halkyon_learn_core/core/content/payload.dart';
import 'package:halkyon_learn_core/core/privacy/data/privacy.dart';
import 'package:serverpod_offline_sync_client/offline_sync.dart';
/// [AssetGateway] on the person's local database, which the sync engine keeps
/// in step with their other devices. A file of a private studyset has its
/// row's payload sealed with its tree's key ([Privacy], ADR 0019).
class SyncAssetGateway(
final OfflineSyncDatabaseSession _db,
final SyncService _sync,
final Privacy _privacy,
) implements AssetGateway {
@override
Future<AssetRow?> byId(String id) async {
final protocol.UuidValue uuid;
try {
uuid = protocol.UuidValue.withValidation(id);
} on FormatException {
// Content can name anything; a name that is no id names no row.
return null;
}
await _privacy.keys.refresh();
return _row(await protocol.Asset.db.findById(_db, uuid));
}
@override
Future<AssetRow?> bySha256(String sha256) async {
await _privacy.keys.refresh();
return _row(
await protocol.Asset.db.findFirstRow(
_db,
where: (t) => t.sha256.equals(sha256),
),
);
}
@override
Future<List<AssetRow>> all() async {
await _privacy.keys.refresh();
return [
for (final asset in await protocol.Asset.db.find(_db)) ?_row(asset),
];
}
@override
String newId() => Privacy.newId().uuid;
@override
Future<String> insert({
required String id,
required String sha256,
required String mimeType,
required int byteSize,
required ByteData payload,
String? keyId,
}) async {
final sealed = _privacy.seal(
payload,
keyId: keyId,
table: 'asset',
bound: '',
id: id,
item: 'asset:$id',
);
final asset = await protocol.Asset.db.insertRow(
_db,
protocol.Asset(
id: protocol.UuidValue.withValidation(id),
sha256: sha256,
mimeType: mimeType,
byteSize: byteSize,
keyId: sealed.keyId,
nonce: sealed.nonce,
payload: sealed.payload,
),
);
_sync.requestSync();
return asset.id!.uuid;
}
@override
Future<void> delete(String id) async {
final uuid = protocol.UuidValue.withValidation(id);
if (await protocol.Asset.db.findById(_db, uuid) case final asset?) {
// The name it came with is content (ADR 0002, rule 2).
await protocol.Asset.db.updateRow(
_db,
asset.copyWith(payload: redactedPayload),
columns: (t) => [t.payload],
);
await protocol.Asset.db.deleteWhere(
_db,
where: (t) => t.id.equals(uuid),
);
_sync.requestSync();
}
}
AssetRow? _row(protocol.Asset? asset) {
if (asset == null) {
return null;
}
// A file this device saw sealed never counts as readable again (the
// reading of 22 Oct): a readable row in its place nobody signed is
// locked.
final opened = _privacy.openItem(
(keyId: asset.keyId, nonce: asset.nonce, payload: asset.payload),
table: 'asset',
bound: '',
id: asset.id!.uuid,
item: 'asset:${asset.id!.uuid}',
treeKey: null,
);
return (
id: asset.id!.uuid,
sha256: asset.sha256,
mimeType: asset.mimeType,
byteSize: asset.byteSize,
payload: switch (opened) {
Readable(:final payload) => payload,
Locked() => redactedPayload,
},
keyId: asset.keyId,
locked: opened is Locked,
);
}
}

View file

@ -6,6 +6,7 @@ import 'package:halkyon_app/halkyon_app.dart';
import 'package:halkyon_learn_client/halkyon_learn_client.dart'
show Client, PublishRefusal, PublishRefusedException, UuidValue;
import 'package:halkyon_learn_core/core/asset/data/asset_repository.dart';
import 'package:halkyon_learn_core/core/asset/data/sync_asset_gateway.dart';
import 'package:halkyon_learn_core/core/privacy/data/content_keys.dart';
import 'package:halkyon_learn_core/core/privacy/data/privacy.dart';
import 'package:halkyon_learn_core/core/privacy/data/privacy_ledger.dart';
@ -21,6 +22,7 @@ import 'package:halkyon_learn_core/feature/feedback/data/feedback_repository.dar
import 'package:halkyon_learn_core/feature/feedback/data/serverpod_feedback_gateway.dart';
import 'package:halkyon_learn_core/feature/feedback/data/sync_feedback_gateway.dart';
import 'package:halkyon_learn_core/feature/learning_path/data/learning_path_repository.dart';
import 'package:halkyon_learn_core/feature/learning_path/data/sync_learning_path_gateway.dart';
import 'package:halkyon_learn_core/feature/note/data/note_deletions.dart';
import 'package:halkyon_learn_core/feature/note/data/note_repository.dart';
import 'package:halkyon_learn_core/feature/note/data/sync_note_gateway.dart';
@ -31,7 +33,6 @@ import 'package:halkyon_learn_core/feature/studyset/model/studyset_tree.dart';
import 'package:halkyon_learn_core/feature/tag/data/sync_tag_gateway.dart';
import 'package:halkyon_learn_core/feature/tag/data/tag_repository.dart';
import 'package:halkyon_learn_flutter/src/core/asset/data/sound_player.dart';
import 'package:halkyon_learn_flutter/src/core/asset/data/sync_asset_gateway.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';
@ -71,7 +72,6 @@ import 'package:halkyon_learn_flutter/src/feature/jellyfin/data/jellyfin_suggest
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/path_progress_repository.dart';
import 'package:halkyon_learn_flutter/src/feature/learning_path/data/sync_learning_path_gateway.dart';
import 'package:halkyon_learn_flutter/src/feature/learning_path/data/sync_path_answer_gateway.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';

View file

@ -1,386 +0,0 @@
import 'dart:typed_data';
import 'package:halkyon_app/halkyon_app.dart';
import 'package:halkyon_learn_client/halkyon_learn_client.dart' as protocol;
import 'package:halkyon_learn_core/core/content/payload.dart';
import 'package:halkyon_learn_core/core/privacy/data/privacy.dart';
import 'package:halkyon_learn_core/feature/learning_path/data/learning_path_gateway.dart';
import 'package:serverpod_offline_sync_client/offline_sync.dart';
/// [LearningPathGateway] on the person's local database, which the sync
/// engine keeps in step with their other devices. Step revisions record
/// [_authorId] as their author. In a private studyset, paths and step
/// revisions are sealed with their tree's key ([Privacy], ADR 0019); one
/// that doesn't open here reads as blank.
class SyncLearningPathGateway(
final OfflineSyncDatabaseSession _db,
final SyncService _sync,
final String _authorId,
final Privacy _privacy,
) implements LearningPathGateway {
@override
Future<List<PathRow>> paths({String? studysetId}) async {
await _privacy.keys.refresh();
final treeKey = await _privacy.treeKeys();
return [
for (final path in await protocol.LearningPath.db.find(
_db,
where: studysetId == null
? null
: (t) => t.studysetId.equals(_uuid(studysetId)),
))
(
id: path.id!.uuid,
studysetId: path.studysetId.uuid,
position: path.position,
payload: _opened(
'learning_path',
path.id!.uuid,
(keyId: path.keyId, nonce: path.nonce, payload: path.payload),
bound: 'studyset:${path.studysetId.uuid}',
item: 'path:${path.id!.uuid}',
treeKey: treeKey(path.studysetId.uuid),
),
),
];
}
/// [envelope]'s payload opened as [item]'s, [bound] to the row it
/// belongs to, in a tree with [treeKey]; blank where it doesn't open here,
/// or isn't trusted there.
ByteData _opened(
String table,
String id,
Envelope envelope, {
required String bound,
required String item,
required String? treeKey,
}) => switch (_privacy.openItem(
envelope,
table: table,
id: id,
bound: bound,
item: item,
treeKey: treeKey,
)) {
Readable(:final payload) => payload,
Locked() => redactedPayload,
};
@override
Future<String> insertPath({
required String studysetId,
required String position,
required ByteData payload,
}) async {
final id = Privacy.newId();
final sealed = _privacy.seal(
payload,
keyId: await _privacy.keyToWriteIn(studysetId),
table: 'learning_path',
id: id.uuid,
bound: 'studyset:$studysetId',
item: 'path:${id.uuid}',
);
final path = await protocol.LearningPath.db.insertRow(
_db,
protocol.LearningPath(
id: id,
studysetId: _uuid(studysetId),
position: position,
keyId: sealed.keyId,
nonce: sealed.nonce,
payload: sealed.payload,
),
);
_sync.requestSync();
return path.id!.uuid;
}
@override
Future<void> updatePathPayload(String id, ByteData payload) async {
if (await protocol.LearningPath.db.findById(_db, _uuid(id))
case final path?) {
final sealed = _privacy.seal(
payload,
keyId: await _privacy.keyOfPath(id),
table: 'learning_path',
id: id,
bound: 'studyset:${path.studysetId.uuid}',
item: 'path:$id',
);
// Only the named columns change (ADR 0001, rule 1).
await protocol.LearningPath.db.updateRow(
_db,
path.copyWith(
keyId: sealed.keyId,
nonce: sealed.nonce,
payload: sealed.payload,
),
columns: (t) => [t.keyId, t.nonce, t.payload],
);
_sync.requestSync();
}
}
@override
Future<void> updatePathPosition(String id, String position) async {
if (await protocol.LearningPath.db.findById(_db, _uuid(id))
case final path?) {
await protocol.LearningPath.db.updateRow(
_db,
path.copyWith(position: position),
columns: (t) => [t.position],
);
_sync.requestSync();
}
}
@override
Future<void> deletePath(String id) async {
await protocol.LearningPathTag.db.deleteWhere(
_db,
where: (t) => t.pathId.equals(_uuid(id)),
);
await protocol.LearningPath.db.deleteWhere(
_db,
where: (t) => t.id.equals(_uuid(id)),
);
_sync.requestSync();
}
@override
Future<List<StepRow>> steps(Set<String> pathIds) async {
if (pathIds.isEmpty) {
return const [];
}
return [
for (final step in await protocol.LearningPathStep.db.find(
_db,
where: (t) => t.pathId.inSet({for (final id in pathIds) _uuid(id)}),
))
(
id: step.id!.uuid,
pathId: step.pathId.uuid,
position: step.position,
kind: step.kind,
),
];
}
@override
Future<List<StepRevisionRow>> revisions(Set<String> stepIds) async {
if (stepIds.isEmpty) {
return const [];
}
await _privacy.keys.refresh();
final ids = {for (final id in stepIds) _uuid(id)};
final treeKey = await _privacy.treeKeys();
final pathOf = {
for (final step in await protocol.LearningPathStep.db.find(
_db,
where: (t) => t.id.inSet(ids),
))
step.id!.uuid: step.pathId,
};
final studysetOf = pathOf.isEmpty
? const <String, String>{}
: {
for (final path in await protocol.LearningPath.db.find(
_db,
where: (t) => t.id.inSet(<protocol.UuidValue>{...pathOf.values}),
))
path.id!.uuid: path.studysetId.uuid,
};
String? treeKeyOf(String stepId) =>
switch (studysetOf[pathOf[stepId]?.uuid]) {
final studyset? => treeKey(studyset),
null => null,
};
return [
for (final revision in await protocol.LearningPathStepRevision.db.find(
_db,
where: (t) => t.stepId.inSet(ids),
))
(
id: revision.id!.uuid,
stepId: revision.stepId.uuid,
parents: {for (final parent in revision.parents) parent.uuid},
payload: _opened(
'learning_path_step_revision',
revision.id!.uuid,
(
keyId: revision.keyId,
nonce: revision.nonce,
payload: revision.payload,
),
bound: 'step:${revision.stepId.uuid}',
item: 'step:${revision.stepId.uuid}',
treeKey: treeKeyOf(revision.stepId.uuid),
),
),
];
}
@override
Future<({String stepId, String revisionId})> insertStep({
required String pathId,
required String position,
required String kind,
required ByteData payload,
}) async {
// Refused before anything is written where this device may write
// nothing (the final reading of 22 Oct).
await _privacy.keyOfPath(pathId);
final step = await protocol.LearningPathStep.db.insertRow(
_db,
protocol.LearningPathStep(
pathId: _uuid(pathId),
position: position,
kind: kind,
),
);
final revisionId = await insertRevision(
stepId: step.id!.uuid,
parents: const {},
payload: payload,
);
return (stepId: step.id!.uuid, revisionId: revisionId);
}
@override
Future<List<String>> insertSteps(
String pathId,
List<NewStepRow> steps,
) async {
if (steps.isEmpty) {
return const [];
}
final keyId = await _privacy.keyOfPath(pathId);
final rows = await protocol.LearningPathStep.db.insert(_db, [
for (final step in steps)
protocol.LearningPathStep(
pathId: _uuid(pathId),
position: step.position,
kind: step.kind,
),
]);
// The batch's positions are distinct, so they tell the stored rows apart
// whatever order they come back in.
final idOf = {for (final row in rows) row.position: row.id!};
await protocol.LearningPathStepRevision.db.insert(_db, [
for (final step in steps)
_newRevision(idOf[step.position]!, const [], step.payload, keyId),
]);
_sync.requestSync();
return [for (final step in steps) idOf[step.position]!.uuid];
}
@override
Future<String> insertRevision({
required String stepId,
required Set<String> parents,
required ByteData payload,
}) async {
final revision = await protocol.LearningPathStepRevision.db.insertRow(
_db,
_newRevision(
_uuid(stepId),
parents,
payload,
await _privacy.keyOfStep(stepId),
),
);
_sync.requestSync();
return revision.id!.uuid;
}
/// A new revision of [stepId] with [payload], sealed with [keyId].
protocol.LearningPathStepRevision _newRevision(
protocol.UuidValue stepId,
Iterable<String> parents,
ByteData payload,
String? keyId,
) {
final id = Privacy.newId();
final sealed = _privacy.seal(
payload,
keyId: keyId,
table: 'learning_path_step_revision',
id: id.uuid,
bound: 'step:${stepId.uuid}',
item: 'step:${stepId.uuid}',
);
return protocol.LearningPathStepRevision(
id: id,
stepId: stepId,
parents: [for (final parent in parents) _uuid(parent)],
authorId: _uuid(_authorId),
keyId: sealed.keyId,
nonce: sealed.nonce,
payload: sealed.payload,
);
}
@override
Future<void> updateStepPosition(String id, String position) async {
if (await protocol.LearningPathStep.db.findById(_db, _uuid(id))
case final step?) {
await protocol.LearningPathStep.db.updateRow(
_db,
step.copyWith(position: position),
columns: (t) => [t.position],
);
_sync.requestSync();
}
}
@override
Future<void> blank({String? stepId, String? pathId}) async {
if (stepId != null) {
final revisions = await protocol.LearningPathStepRevision.db.find(
_db,
where: (t) => t.stepId.equals(_uuid(stepId)),
);
// Column-scoped updates, which sync like any other edit (ADR 0002,
// rule 2).
await protocol.LearningPathStepRevision.db.update(
_db,
[
for (final revision in revisions)
revision.copyWith(payload: redactedPayload),
],
columns: (t) => [t.payload],
);
}
if (pathId != null) {
await updatePathPayload(pathId, redactedPayload);
}
_sync.requestSync();
}
@override
Future<void> deleteStep(String id) async {
final step = _uuid(id);
// Its answers go too: no content, but personal study data that would
// otherwise stay on the server as hidden rows.
await protocol.LearningPathAnswer.db.deleteWhere(
_db,
where: (t) => t.stepId.equals(step),
);
await protocol.LearningPathStepRevision.db.deleteWhere(
_db,
where: (t) => t.stepId.equals(step),
);
await protocol.LearningPathStep.db.deleteWhere(
_db,
where: (t) => t.id.equals(step),
);
_sync.requestSync();
}
@override
Stream<void> get remoteChanges => _sync.merges;
protocol.UuidValue _uuid(String id) => protocol.UuidValue.withValidation(id);
}

View file

@ -12,19 +12,15 @@ import 'package:halkyon_learn_core/feature/note/model/note_content.dart';
import 'package:halkyon_learn_core/feature/studyset/data/studyset_payload.dart';
import 'package:halkyon_learn_core/feature/studyset/model/studyset_content.dart';
import 'package:halkyon_learn_core/feature/tag/data/tag_repository.dart';
import 'package:halkyon_learn_core/testing.dart';
import 'package:halkyon_learn_flutter/src/core/router/route_paths.dart';
import 'package:halkyon_learn_flutter/src/feature/pomodoro/model/pomodoro_profile.dart';
import 'package:halkyon_learn_flutter/src/feature/settings/data/settings_repository.dart';
import 'package:halkyon_ui/halkyon_ui.dart';
import 'package:material_ui/material_ui.dart';
import '../support/fake_card_gateway.dart';
import '../support/fake_discussion_gateway.dart';
import '../support/fake_learning_path_gateway.dart';
import '../support/fake_note_gateway.dart';
import '../support/fake_settings_gateway.dart';
import '../support/fake_studyset_gateway.dart';
import '../support/fake_tag_gateway.dart';
import '../support/pump_app.dart';
import '../support/test_dependencies.dart';

View file

@ -5,10 +5,9 @@ import 'package:flutter_test/flutter_test.dart';
import 'package:halkyon_app/testing.dart'
show FakeFileServerGateway, MemoryFileCache, sha256Of;
import 'package:halkyon_learn_core/core/asset/data/asset_repository.dart';
import 'package:halkyon_learn_core/testing.dart';
import 'package:halkyon_learn_flutter/src/core/asset/bloc/sound_bloc.dart';
import '../../../support/fake_asset_gateway.dart';
import '../../../support/fake_asset_sealing.dart';
import '../../../support/fake_sound_player.dart';
void main() {

View file

@ -8,9 +8,7 @@ import 'package:halkyon_app/testing.dart'
import 'package:halkyon_learn_core/core/asset/data/asset_payload.dart';
import 'package:halkyon_learn_core/core/asset/data/asset_repository.dart';
import 'package:halkyon_learn_core/core/asset/model/stored_file.dart';
import '../../../support/fake_asset_gateway.dart';
import '../../../support/fake_asset_sealing.dart';
import 'package:halkyon_learn_core/testing.dart';
void main() {
late FakeAssetGateway rows;

View file

@ -6,9 +6,7 @@ import 'package:halkyon_app/testing.dart'
show FakeFileServerGateway, MemoryFileCache, sha256Of;
import 'package:halkyon_learn_core/core/asset/data/asset_payload.dart';
import 'package:halkyon_learn_core/core/asset/data/asset_repository.dart';
import '../../../support/fake_asset_gateway.dart';
import '../../../support/fake_asset_sealing.dart';
import 'package:halkyon_learn_core/testing.dart';
/// A private studyset's files (ADR 0019, D5): sealed with its tree's key for
/// their row before they are hashed, uploaded and kept.

View file

@ -7,14 +7,12 @@ import 'package:halkyon_app/testing.dart';
import 'package:halkyon_editor/halkyon_editor.dart';
import 'package:halkyon_learn_core/feature/studyset/data/studyset_payload.dart';
import 'package:halkyon_learn_core/feature/studyset/model/studyset_content.dart';
import 'package:halkyon_learn_core/testing.dart';
import 'package:halkyon_learn_flutter/src/core/editor/learn_content.dart';
import 'package:halkyon_learn_flutter/src/core/l10n/app_languages.dart';
import 'package:halkyon_learn_flutter/src/core/router/route_paths.dart';
import 'package:material_ui/material_ui.dart';
import '../../support/fake_asset_gateway.dart';
import '../../support/fake_note_gateway.dart';
import '../../support/fake_studyset_gateway.dart';
import '../../support/pump_app.dart';
import '../../support/test_dependencies.dart';

View file

@ -12,16 +12,13 @@ import 'package:halkyon_learn_core/feature/card/data/card_repository.dart';
import 'package:halkyon_learn_core/feature/card/model/card_content.dart';
import 'package:halkyon_learn_core/feature/studyset/data/studyset_payload.dart';
import 'package:halkyon_learn_core/feature/studyset/model/studyset_content.dart';
import 'package:halkyon_learn_core/testing.dart';
import 'package:halkyon_learn_flutter/src/core/asset/widget/stored_file_image.dart';
import 'package:halkyon_learn_flutter/src/core/editor/file_blocks.dart';
import 'package:halkyon_learn_flutter/src/core/router/route_paths.dart';
import 'package:material_ui/material_ui.dart';
import '../../support/fake_asset_gateway.dart';
import '../../support/fake_asset_sealing.dart';
import '../../support/fake_card_gateway.dart';
import '../../support/fake_sound_player.dart';
import '../../support/fake_studyset_gateway.dart';
import '../../support/pump_app.dart';
import '../../support/test_dependencies.dart';

View file

@ -4,6 +4,7 @@ import 'package:halkyon_learn_core/feature/note/data/note_repository.dart';
import 'package:halkyon_learn_core/feature/note/model/note_content.dart';
import 'package:halkyon_learn_core/feature/studyset/data/studyset_payload.dart';
import 'package:halkyon_learn_core/feature/studyset/model/studyset_content.dart';
import 'package:halkyon_learn_core/testing.dart';
import 'package:halkyon_learn_flutter/src/core/device_settings/model/device_settings.dart';
import 'package:halkyon_learn_flutter/src/core/grammar/grammar_match.dart';
import 'package:halkyon_learn_flutter/src/core/grammar/grammar_overlay.dart';
@ -13,9 +14,7 @@ import 'package:halkyon_learn_flutter/src/feature/settings/model/person_settings
import 'package:material_ui/material_ui.dart';
import '../../support/fake_grammar_gateway.dart';
import '../../support/fake_note_gateway.dart';
import '../../support/fake_settings_gateway.dart';
import '../../support/fake_studyset_gateway.dart';
import '../../support/pump_app.dart';
import '../../support/test_dependencies.dart';

View file

@ -14,10 +14,10 @@ import 'package:halkyon_learn_core/core/privacy/data/privacy_ledger.dart';
import 'package:halkyon_learn_core/core/privacy/data/sync_content_key_gateway.dart';
import 'package:halkyon_learn_core/core/privacy/data/tree_files.dart';
import 'package:halkyon_learn_core/feature/card/data/sync_card_gateway.dart';
import 'package:halkyon_learn_core/feature/learning_path/data/sync_learning_path_gateway.dart';
import 'package:halkyon_learn_core/feature/note/data/sync_note_gateway.dart';
import 'package:halkyon_learn_core/feature/studyset/data/sync_studyset_gateway.dart';
import 'package:halkyon_learn_core/feature/tag/data/sync_tag_gateway.dart';
import 'package:halkyon_learn_flutter/src/feature/learning_path/data/sync_learning_path_gateway.dart';
import 'package:halkyon_learn_flutter/src/feature/learning_path/data/sync_path_answer_gateway.dart';
import 'package:halkyon_learn_flutter/src/feature/pdf_source/data/sync_pdf_source_gateway.dart';
import 'package:halkyon_learn_flutter/src/feature/publish/data/publish_repository.dart';

View file

@ -16,9 +16,9 @@ import 'package:halkyon_learn_core/core/privacy/data/sync_content_key_gateway.da
import 'package:halkyon_learn_core/core/privacy/data/tree_files.dart';
import 'package:halkyon_learn_core/core/privacy/model/unsigned_edit.dart';
import 'package:halkyon_learn_core/feature/card/data/sync_card_gateway.dart';
import 'package:halkyon_learn_core/feature/learning_path/data/sync_learning_path_gateway.dart';
import 'package:halkyon_learn_core/feature/studyset/data/sync_studyset_gateway.dart';
import 'package:halkyon_learn_core/feature/tag/data/sync_tag_gateway.dart';
import 'package:halkyon_learn_flutter/src/feature/learning_path/data/sync_learning_path_gateway.dart';
import 'package:halkyon_learn_flutter/src/feature/learning_path/data/sync_path_answer_gateway.dart';
import 'package:halkyon_learn_flutter/src/feature/settings/data/sync_settings_gateway.dart';
import 'package:serverpod_offline_sync_client/offline_sync.dart';

View file

@ -5,6 +5,7 @@ import 'package:halkyon_learn_core/feature/card/data/card_repository.dart';
import 'package:halkyon_learn_core/feature/card/model/card_content.dart';
import 'package:halkyon_learn_core/feature/studyset/data/studyset_payload.dart';
import 'package:halkyon_learn_core/feature/studyset/model/studyset_content.dart';
import 'package:halkyon_learn_core/testing.dart';
import 'package:halkyon_learn_flutter/src/core/router/route_paths.dart';
import 'package:halkyon_learn_flutter/src/feature/home/widget/activity_heatmap.dart';
import 'package:halkyon_learn_flutter/src/feature/home/widget/home_screen.dart';
@ -13,8 +14,6 @@ import 'package:halkyon_learn_flutter/src/feature/studyset/widget/studyset_scree
import 'package:halkyon_ui/halkyon_ui.dart';
import 'package:material_ui/material_ui.dart';
import '../../support/fake_card_gateway.dart';
import '../../support/fake_studyset_gateway.dart';
import '../../support/memory_device_settings_store.dart';
import '../../support/pump_app.dart';
import '../../support/test_dependencies.dart';

View file

@ -4,12 +4,11 @@ import 'package:halkyon_learn_core/feature/card/data/card_repository.dart';
import 'package:halkyon_learn_core/feature/card/model/card_content.dart';
import 'package:halkyon_learn_core/feature/studyset/data/studyset_payload.dart';
import 'package:halkyon_learn_core/feature/studyset/model/studyset_content.dart';
import 'package:halkyon_learn_core/testing.dart';
import 'package:halkyon_learn_flutter/src/core/router/route_paths.dart';
import 'package:halkyon_ui/halkyon_ui.dart';
import 'package:material_ui/material_ui.dart';
import '../../support/fake_card_gateway.dart';
import '../../support/fake_studyset_gateway.dart';
import '../../support/pump_app.dart';
import '../../support/test_dependencies.dart';

View file

@ -13,6 +13,7 @@ import 'package:halkyon_learn_core/feature/note/data/note_repository.dart';
import 'package:halkyon_learn_core/feature/studyset/data/studyset_repository.dart';
import 'package:halkyon_learn_core/feature/studyset/model/studyset_content.dart';
import 'package:halkyon_learn_core/feature/tag/data/tag_repository.dart';
import 'package:halkyon_learn_core/testing.dart';
import 'package:halkyon_learn_flutter/src/core/work/work_progress.dart';
import 'package:halkyon_learn_flutter/src/feature/anki/data/anki_importer.dart';
import 'package:halkyon_learn_flutter/src/feature/anki/data/anki_package.dart';
@ -22,15 +23,8 @@ import 'package:halkyon_learn_flutter/src/feature/study/model/rating.dart';
import 'package:halkyon_learn_flutter/src/feature/study/model/review.dart';
import 'package:halkyon_learn_flutter/src/feature/studyset/data/studyset_deletions.dart';
import '../../../support/fake_asset_gateway.dart';
import '../../../support/fake_asset_sealing.dart';
import '../../../support/fake_card_gateway.dart';
import '../../../support/fake_learning_path_gateway.dart';
import '../../../support/fake_note_gateway.dart';
import '../../../support/fake_pdf_source_gateway.dart';
import '../../../support/fake_review_gateway.dart';
import '../../../support/fake_studyset_gateway.dart';
import '../../../support/fake_tag_gateway.dart';
import 'anki_package_test.dart' show fixture;
void main() {

View file

@ -10,14 +10,12 @@ import 'package:halkyon_learn_core/feature/note/model/note_content.dart';
import 'package:halkyon_learn_core/feature/studyset/data/studyset_payload.dart';
import 'package:halkyon_learn_core/feature/studyset/data/studyset_repository.dart';
import 'package:halkyon_learn_core/feature/studyset/model/studyset_content.dart';
import 'package:halkyon_learn_core/testing.dart';
import 'package:halkyon_learn_flutter/src/core/router/route_paths.dart';
import 'package:halkyon_learn_flutter/src/core/widget/work_progress_view.dart';
import 'package:halkyon_learn_flutter/src/feature/studyset/bloc/content_tab_cubit.dart';
import 'package:material_ui/material_ui.dart';
import '../../../support/fake_card_gateway.dart';
import '../../../support/fake_note_gateway.dart';
import '../../../support/fake_studyset_gateway.dart';
import '../../../support/pump_app.dart';
import '../../../support/test_dependencies.dart';
import '../data/anki_package_test.dart' show fixture;

View file

@ -22,6 +22,7 @@ import 'package:halkyon_learn_core/feature/studyset/model/exam_plan.dart';
import 'package:halkyon_learn_core/feature/studyset/model/studyset_content.dart';
import 'package:halkyon_learn_core/feature/studyset/model/studyset_tree.dart';
import 'package:halkyon_learn_core/feature/tag/data/tag_repository.dart';
import 'package:halkyon_learn_core/testing.dart';
import 'package:halkyon_learn_flutter/src/core/work/work_progress.dart';
import 'package:halkyon_learn_flutter/src/feature/archive/data/archive_exporter.dart';
import 'package:halkyon_learn_flutter/src/feature/archive/data/archive_importer.dart';
@ -40,18 +41,11 @@ import 'package:halkyon_learn_flutter/src/feature/study/model/review.dart';
import 'package:halkyon_learn_flutter/src/feature/studyset/data/studyset_deletions.dart';
import 'package:halkyon_ui/halkyon_ui.dart';
import '../../support/fake_asset_gateway.dart';
import '../../support/fake_asset_sealing.dart';
import '../../support/fake_card_gateway.dart';
import '../../support/fake_device_dictionaries.dart';
import '../../support/fake_dictionary_gateway.dart';
import '../../support/fake_learning_path_gateway.dart';
import '../../support/fake_note_gateway.dart';
import '../../support/fake_path_answer_gateway.dart';
import '../../support/fake_pdf_source_gateway.dart';
import '../../support/fake_review_gateway.dart';
import '../../support/fake_studyset_gateway.dart';
import '../../support/fake_tag_gateway.dart';
/// One person's repositories, on fake gateways of their own.
final class _Person() {

View file

@ -22,6 +22,7 @@ import 'package:halkyon_learn_core/feature/studyset/data/studyset_payload.dart';
import 'package:halkyon_learn_core/feature/studyset/data/studyset_repository.dart';
import 'package:halkyon_learn_core/feature/studyset/model/studyset_content.dart';
import 'package:halkyon_learn_core/feature/tag/data/tag_repository.dart';
import 'package:halkyon_learn_core/testing.dart';
import 'package:halkyon_learn_flutter/src/core/work/work_progress.dart';
import 'package:halkyon_learn_flutter/src/feature/archive/data/archive_importer.dart';
import 'package:halkyon_learn_flutter/src/feature/archive/data/archive_reader.dart';
@ -33,18 +34,11 @@ import 'package:halkyon_learn_flutter/src/feature/study/data/review_repository.d
import 'package:halkyon_learn_flutter/src/feature/study/model/rating.dart';
import 'package:halkyon_learn_flutter/src/feature/studyset/data/studyset_deletions.dart';
import '../../support/fake_asset_gateway.dart';
import '../../support/fake_asset_sealing.dart';
import '../../support/fake_card_gateway.dart';
import '../../support/fake_device_dictionaries.dart';
import '../../support/fake_dictionary_gateway.dart';
import '../../support/fake_learning_path_gateway.dart';
import '../../support/fake_note_gateway.dart';
import '../../support/fake_path_answer_gateway.dart';
import '../../support/fake_pdf_source_gateway.dart';
import '../../support/fake_review_gateway.dart';
import '../../support/fake_studyset_gateway.dart';
import '../../support/fake_tag_gateway.dart';
/// The JSON a synced [payload] holds.
Map<String, Object?> json(ByteData payload) =>

View file

@ -3,14 +3,13 @@ import 'dart:async';
import 'package:flutter_test/flutter_test.dart';
import 'package:halkyon_app/testing.dart';
import 'package:halkyon_learn_core/feature/studyset/data/studyset_repository.dart';
import 'package:halkyon_learn_core/testing.dart';
import 'package:halkyon_learn_flutter/src/core/router/route_paths.dart';
import 'package:halkyon_learn_flutter/src/core/widget/work_progress_view.dart';
import 'package:halkyon_learn_flutter/src/feature/studyset/widget/studyset_screen.dart';
import 'package:material_ui/material_ui.dart';
import '../../support/fake_card_gateway.dart';
import '../../support/fake_dictionary_gateway.dart';
import '../../support/fake_studyset_gateway.dart';
import '../../support/pump_app.dart';
import '../../support/test_dependencies.dart';
import 'archive_test.dart' show japanese, zip;

View file

@ -6,6 +6,7 @@ import 'package:halkyon_learn_core/feature/card/data/card_repository.dart';
import 'package:halkyon_learn_core/feature/card/model/card_content.dart';
import 'package:halkyon_learn_core/feature/studyset/data/studyset_payload.dart';
import 'package:halkyon_learn_core/feature/studyset/model/studyset_content.dart';
import 'package:halkyon_learn_core/testing.dart';
import 'package:halkyon_learn_flutter/src/core/device_settings/model/device_settings.dart';
import 'package:halkyon_learn_flutter/src/core/router/route_paths.dart';
import 'package:halkyon_learn_flutter/src/feature/assistant/data/web_search.dart';
@ -17,8 +18,6 @@ import 'package:halkyon_learn_flutter/src/feature/study/widget/rating_row.dart';
import 'package:lucide_icons_flutter/lucide_icons.dart';
import 'package:material_ui/material_ui.dart';
import '../../support/fake_card_gateway.dart';
import '../../support/fake_studyset_gateway.dart';
import '../../support/memory_device_settings_store.dart';
import '../../support/pump_app.dart';
import '../../support/test_dependencies.dart';

Some files were not shown because too many files have changed in this diff Show more