mirror of
https://github.com/kuhyx/todo-app.git
synced 2026-07-04 15:03:01 +02:00
Notes list & filtering: - Text-search filter plus independent date-range filters for both created and last-updated (AND-combined), a priority filter, and a new status filter. Default view hides Done/Abandoned and renders as "unfiltered" (no badge for the default state); fixed badge clipping. - NoteSort options wired into the list UI; watchCount() for the "N saved". Status & priority: - New Status enum (toDo/inProgress/Done/Abandoned) as a settable + filterable attribute on every note, with capture-screen dropdown. - Removed "None" priority: every note is Low/Medium/High, default Medium. Schema migration v2->v3 rewrites legacy priority 0 -> Medium. Export / import: - NotesMarkdown round-trippable single-file format with HTML-comment markers. - Settings "Export notes" (mobile share sheet / desktop writes ~/todo/BACKLOG.md) and "Import notes" (file picker + safe newer-wins merge by id). Structured template: - Every new note pre-fills the richer what/where/must/nice/out/done/depends/ estimate/refs scaffold. Tests: - New fast (~5s), deterministic suite via FakeNoteRepository (no DB timers) and injected http/file-selector/url-launcher fakes. 86 tests, 96.2% line coverage (note.dart & sync_service.dart at 100%, settings 98.7%). Mobile-only share branch excluded via coverage:ignore (unreachable on the Linux test host). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
97 lines
3.1 KiB
Dart
97 lines
3.1 KiB
Dart
// Headless end-to-end proof of the sync engine against a REAL GitHub repo.
|
|
//
|
|
// Simulates two devices (A and B), each creating a note offline, then syncs
|
|
// them through the repo and asserts both devices converge to both notes.
|
|
// Cleans up its throwaway changeset files afterwards so the real
|
|
// `changesets/` directory is never touched.
|
|
//
|
|
// Run: GH_TOKEN=$(gh auth token) dart run tool/sync_smoke.dart
|
|
import 'dart:io';
|
|
|
|
import 'package:sqflite_common_ffi/sqflite_ffi.dart';
|
|
import 'package:todo/data/note.dart';
|
|
import 'package:todo/data/note_repository.dart';
|
|
import 'package:todo/sync/github_client.dart';
|
|
import 'package:todo/sync/sync_service.dart';
|
|
|
|
Future<void> main() async {
|
|
sqfliteFfiInit();
|
|
|
|
final token = Platform.environment['GH_TOKEN'];
|
|
if (token == null || token.isEmpty) {
|
|
stderr.writeln('Set GH_TOKEN (e.g. GH_TOKEN=\$(gh auth token)).');
|
|
exit(2);
|
|
}
|
|
|
|
// Throwaway directory so we never pollute the real `changesets/`.
|
|
const service = SyncService(changesetDir: 'changesets_smoketest');
|
|
GitHubClient client() =>
|
|
GitHubClient(owner: 'kuhyx', repo: 'todo-sync', token: token);
|
|
|
|
final deviceA = await NoteRepository.openInMemory();
|
|
final deviceB = await NoteRepository.openInMemory();
|
|
final stamp = DateTime.now().toIso8601String();
|
|
|
|
await _insert(deviceA, 'Idea from device A @ $stamp');
|
|
await _insert(deviceB, 'Idea from device B @ $stamp');
|
|
|
|
stdout.writeln('Device A nodeId: ${deviceA.nodeId}');
|
|
stdout.writeln('Device B nodeId: ${deviceB.nodeId}');
|
|
|
|
// Sync order: A pushes, B pulls A + pushes, A pulls B. Both converge.
|
|
final ghA = client();
|
|
final ghB = client();
|
|
stdout.writeln('A.sync(): ${await service.sync(deviceA, ghA)}');
|
|
stdout.writeln('B.sync(): ${await service.sync(deviceB, ghB)}');
|
|
stdout.writeln('A.sync(): ${await service.sync(deviceA, ghA)}');
|
|
|
|
final aNotes = (await deviceA.listNotes()).map((n) => n.text).toSet();
|
|
final bNotes = (await deviceB.listNotes()).map((n) => n.text).toSet();
|
|
stdout.writeln('\nDevice A sees: $aNotes');
|
|
stdout.writeln('Device B sees: $bNotes');
|
|
|
|
final expected = {
|
|
'Idea from device A @ $stamp',
|
|
'Idea from device B @ $stamp',
|
|
};
|
|
final converged =
|
|
aNotes.containsAll(expected) && bNotes.containsAll(expected);
|
|
|
|
// Cleanup: remove the throwaway changeset files.
|
|
final cleanup = client();
|
|
for (final f in await cleanup.listDirectory('changesets_smoketest')) {
|
|
await cleanup.deleteFile(f.path, f.sha, message: 'smoke test cleanup');
|
|
}
|
|
stdout.writeln('Cleaned up throwaway changeset files.');
|
|
|
|
ghA.close();
|
|
ghB.close();
|
|
cleanup.close();
|
|
await deviceA.close();
|
|
await deviceB.close();
|
|
|
|
if (converged) {
|
|
stdout.writeln(
|
|
'\n✅ PASS: both devices converged to both notes via GitHub.',
|
|
);
|
|
exit(0);
|
|
} else {
|
|
stdout.writeln('\n❌ FAIL: devices did not converge. Expected $expected.');
|
|
exit(1);
|
|
}
|
|
}
|
|
|
|
Future<void> _insert(NoteRepository repo, String text) async {
|
|
final now = DateTime.now();
|
|
await repo.upsert(
|
|
Note(
|
|
id: '${now.microsecondsSinceEpoch}-${text.hashCode}',
|
|
text: text,
|
|
priority: Priority.medium,
|
|
status: Status.todo,
|
|
createdAt: now,
|
|
updatedAt: now,
|
|
),
|
|
);
|
|
}
|