Restructure step 0 done: notice table test, flaky-link proxy, liveness on the engine clock, STARSYNC_PROFILE

- notice_table_test: one row per AppEvent variant through noticeFor.
- FlakyProxy (core/testing.dart): pass, blackhole (half-open), delay,
  hold, cut after N bytes, reset; demo tests against today's server.
- Liveness (stale/drop/ping) on the engine clock, so grace can be tested
  with a fake clock; Connection takes an injectable clock.
- AppDirs with STARSYNC_PROFILE=<name>: separate data and logs, so two
  instances run on one machine.
- DEVELOPMENT: scenario list per restructure step; test tags follow the
  new step order.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
foxyzandClaude Opus 5.5 committed 2026-10-15 20:05:52 +03:00
1 parent eac45746ce
commit f4ca02c3cf
23 files changed
+959 -48

No files matched your search

@@ -0,0 +1,64 @@
import 'dart:io';
import 'package:path_provider/path_provider.dart';
/// The environment variable that picks a separate profile (DEVELOPMENT.md
/// "Dev loop": two instances on one machine).
const profileEnv = 'STARSYNC_PROFILE';
/// Where the app keeps its own files, resolved once in `main` ([resolve])
/// and handed on from there.
///
/// Without a profile it is exactly today's layout: Hive's boxes in Hive's
/// default folder (the documents folder) and the logs in
/// `<app support>/logs`. With `STARSYNC_PROFILE=<name>` (letters, digits,
/// `-`, `_`; at most 32) the per-install data moves to
/// `<app support>/profiles/<name>/`: the Hive boxes (settings and so the
/// profile, connect form, window bounds, folders, Jellyfin sign-ins; the
/// client id from restructure step 1 on) under `hive/`, the logs under
/// `logs/`. Downloads and caches stay shared: yt-dlp and its cookies, the
/// extracted shaders, thumbnails, the AniList cache.
class AppDirs {
/// The profile name; null for the normal app.
final String? profile;
/// The app support folder (yt-dlp, shaders; logs without a profile).
final String support;
/// Hive's folder; null: Hive's default (`Hive.initFlutter()`).
final String? hive;
/// The app's log folder.
final String logs;
const AppDirs({required this.profile, required this.support, required this.hive, required this.logs});
/// The layout for [support] (the app support folder) and [env]; an
/// invalid profile name is ignored ([invalidProfile] says which).
static AppDirs fromEnv({required String support, required Map<String, String> env}) {
final sep = Platform.pathSeparator;
final name = validProfile(env[profileEnv]);
if (name == null) return AppDirs(profile: null, support: support, hive: null, logs: '$support${sep}logs');
final root = '$support${sep}profiles$sep$name';
return AppDirs(profile: name, support: support, hive: '$root${sep}hive', logs: '$root${sep}logs');
}
/// [raw] when it is a usable profile name, else null.
static String? validProfile(String? raw) {
final v = raw?.trim() ?? '';
return RegExp(r'^[A-Za-z0-9_-]{1,32}$').hasMatch(v) ? v : null;
}
/// The env value when it is set but not a usable name (for a warning).
static String? invalidProfile(Map<String, String> env) {
final raw = env[profileEnv];
return raw != null && raw.trim().isNotEmpty && validProfile(raw) == null ? raw : null;
}
/// Resolves the layout from the platform and the process environment.
/// Needs Flutter's binding (path_provider).
static Future<AppDirs> resolve() async {
final support = (await getApplicationSupportDirectory()).path;
final dirs = fromEnv(support: support, env: Platform.environment);
final hive = dirs.hive;
if (hive != null) await Directory(hive).create(recursive: true);
return dirs;
}
}
+9 -8
View File
@@ -2,10 +2,10 @@ import 'dart:async';
import 'dart:io';
import 'package:flutter/foundation.dart';
import 'package:path_provider/path_provider.dart';
import 'package:sync_relay_core/core.dart';
import '../screens/home/ui.dart' show appName, displayVersion;
import 'app_dirs.dart';
import '../state/session_state.dart';
// The app's log on disk (<app support>/logs/app-YYYY-MM-DD.log; core's
@@ -17,13 +17,11 @@ final _appLog = createLogger('app');
/// The logs folder, once [startAppLogging] ran.
String? appLogDir;
/// Opens the log files and routes `debugPrint` and uncaught errors into
/// the log. Needs Flutter's binding (path_provider).
Future<void> startAppLogging() async {
/// Opens the log files in [dirs]' log folder and routes `debugPrint` and
/// uncaught errors into the log.
Future<void> startAppLogging(AppDirs dirs) async {
try {
final base = await getApplicationSupportDirectory();
final dir = '${base.path}${Platform.pathSeparator}logs';
if (startLogFiles(dir) != null) appLogDir = dir;
if (startLogFiles(dirs.logs) != null) appLogDir = dirs.logs;
} catch (e) {
_appLog.warning('no log files: $e');
}
@@ -39,7 +37,10 @@ Future<void> startAppLogging() async {
return false;
};
_appLog.info('$appName ${displayVersion()} on ${Platform.operatingSystem} '
'${Platform.operatingSystemVersion}, pid $pid, Dart ${Platform.version.split(' ').first}');
'${Platform.operatingSystemVersion}, pid $pid, Dart ${Platform.version.split(' ').first}'
'${dirs.profile == null ? '' : ', profile ${dirs.profile}'}');
final bad = AppDirs.invalidProfile(Platform.environment);
if (bad != null) _appLog.warning('$profileEnv="$bad" ignored: use letters, digits, - and _ (at most 32)');
}
/// The text "Copy diagnostics" gives: versions, then the last [since] of