test(sync): test devices sync through their sync service

A test device synced by calling the engine itself, so its repositories
never heard of what a sync merged; reading the database each time, they
didn't need to. Since cards and reviews are kept until something changes
them, a device went on reading its cards from before the sync, and the
conflict test failed. Devices now sync through their SyncService, as the
app does, and fail when the sync failed. SYNC_TEST_SERVER_DIR names the
server folder whose database the SQL checks read, for a worktree testing
against another checkout's running server. All 30 sync tests pass.

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-17 14:57:33 +02:00
commit f1fee7e63c
Signed by: raisondetredev
GPG key ID: 07F6FCF405488B64

View file

@ -68,7 +68,10 @@ Future<pg.Result> queryServer(
}
Future<pg.Connection> _openServerDatabase() async {
const server = '../halkyon_learn_server';
// A worktree's tests run against the server `serverpod start` runs, which
// may be another checkout's: SYNC_TEST_SERVER_DIR names its server folder.
final server =
Platform.environment['SYNC_TEST_SERVER_DIR'] ?? '../halkyon_learn_server';
const settings = pg.ConnectionSettings(sslMode: pg.SslMode.disable);
final development = loadYaml(
File('$server/config/development.yaml').readAsStringSync(),
@ -169,9 +172,17 @@ final class SyncDevice(
database.path,
persistentUserId: space,
);
// The gateways ask for a sync after every write; these tests sync
// explicitly instead, so the requests go nowhere.
final syncRequests = SyncService(({required onMerge}) async {});
// Syncs as the app's do, so what a sync merges reaches the repositories
// (which keep what they read until then). The gateways ask for a sync
// after every write; these tests sync explicitly instead ([sync]), so
// a request would wait a day.
final syncRequests = SyncService(
({required onMerge}) => client.offlineSync.syncOnce(
db,
onInboundMerge: (space) => onMerge(),
),
debounce: const Duration(days: 1),
);
final cards = CardRepository(
SyncCardGateway(db, syncRequests, person.authUserId.uuid),
);
@ -213,8 +224,17 @@ final class SyncDevice(
/// Explore).
protocol.Client get client => _client;
/// Sends this device's changes and receives everyone else's.
Future<void> sync() => _client.offlineSync.syncOnce(_db);
/// Sends this device's changes and receives everyone else's, through
/// its sync service, as the app syncs; throws when the sync failed.
Future<void> sync() async {
final done = _syncRequests.statusChanges.firstWhere(
(status) => !status.syncing,
);
_syncRequests.syncNow();
if ((await done).failure != null) {
throw StateError('The sync failed; the log says why.');
}
}
/// The studyset with [id] on this device, or null.
Future<Studyset?> studyset(String id) async {