todo-app/lib/sync/backlog_export_io.dart
Krzysztof kuhy Rudnicki c37a9ef118 Add the desktop wrapper, and stop the test suite eating BACKLOG.md
The desktop app is a browser now, so it cannot write files. This adds the other
half: a loopback HTTP server that serves the web build and owns the two files
the browser cannot touch -- `~/todo/BACKLOG.md`, which the user's tooling and
the todo MCP server read, and a disk copy of the note log so a wiped Chrome
profile is not a total-loss event for unsynced notes.

Notable details:

- Binds to 127.0.0.1 only. The endpoints overwrite files in the user's home
  with no authentication, so a routable bind would let anything on the network
  rewrite the backlog.
- Serves explicit content types. CanvasKit refuses a `.wasm` labelled anything
  else and the app then renders nothing at all.
- Mirroring to disk is debounced. The store persists on every keystroke, so
  mirroring eagerly meant an HTTP request plus a whole-log write per character;
  IndexedDB still gets every keystroke, only the durability copy waits.

Separately, this fixes a pre-existing destructive bug found while testing:
`settings_screen_test` did real file I/O and overwrote the user's canonical
`~/todo/BACKLOG.md` with a single fake note on *every* `flutter test` run. It
went unnoticed because the app rewrites the file whenever it launches. The
export destination is now injectable and the test redirects it to a temp dir;
verified that a full suite run leaves the real file byte-identical.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-20 13:33:25 +02:00

44 lines
1.8 KiB
Dart

import 'dart:io';
import 'package:path_provider/path_provider.dart';
import 'package:share_plus/share_plus.dart';
/// Resolves the home directory the desktop export writes under.
///
/// Overridable **so the test suite never writes to the real `~/todo`**. This is
/// not hypothetical: before this seam existed, running `flutter test` exported
/// a fake note over the user's canonical `~/todo/BACKLOG.md` every time.
String Function() resolveExportHome =
() => Platform.environment['HOME'] ?? Directory.current.path;
/// Writes the Markdown export somewhere the user can get at it, and returns a
/// human-readable description of where it went.
///
/// On mobile this opens the system share sheet; on a `dart:io` desktop host it
/// writes the canonical `~/todo/BACKLOG.md`.
Future<String> exportBacklog(String markdown, int noteCount) async {
// coverage:ignore-start
// Mobile-only share path: Platform.isAndroid/isIOS are always false on the
// Linux test host, so these lines are structurally unreachable in CI and
// excluded from the coverage denominator. Verified on-device.
if (Platform.isAndroid || Platform.isIOS) {
final dir = await getTemporaryDirectory();
final file = File('${dir.path}/todo-backlog.md');
await file.writeAsString(markdown);
await SharePlus.instance.share(
ShareParams(
files: [XFile(file.path, mimeType: 'text/markdown')],
subject: 'todo backlog ($noteCount notes)',
),
);
return 'Shared $noteCount notes';
}
// coverage:ignore-end
final home = resolveExportHome();
final dir = Directory('$home/todo');
if (!dir.existsSync()) dir.createSync(recursive: true);
final file = File('${dir.path}/BACKLOG.md');
await file.writeAsString(markdown);
return 'Exported $noteCount notes to ${file.path}';
}