mirror of
https://github.com/kuhyx/todo-app.git
synced 2026-11-10 20:27:01 +01:00
NoteRepository.bulkUpdate sets status/priority on many notes in one store write (mergeLogs + replaceAll; only changed fields plus updated_at get a clock). saveReplacing saves one note and tombstones others in one write. assignToRepo moves plain notes into the repo's open note as "## title" sections via addRepoTask, demoting body headings below "##" so a template note stays one section; notes already in a repo are refused. The seeded watch stream moved to log_watch.dart for the 250-line cap. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WzxdWXdsB7CQPNXVN829G1
113 lines
3.7 KiB
Dart
113 lines
3.7 KiB
Dart
/// Bulk status/priority and save-replacing: one store write each.
|
|
library;
|
|
|
|
import 'package:crdt_sync/crdt_sync.dart';
|
|
import 'package:flutter_test/flutter_test.dart';
|
|
import 'package:todo/data/note.dart';
|
|
import 'package:todo/data/note_repository.dart';
|
|
|
|
import 'note_repository_helpers.dart';
|
|
|
|
/// Counts log writes, so "one write" is measured, not assumed.
|
|
class _CountingPersistence implements LogPersistence {
|
|
final _inner = MemoryPersistence();
|
|
int writes = 0;
|
|
|
|
@override
|
|
Future<String?> read() => _inner.read();
|
|
|
|
@override
|
|
Future<void> write(String text) {
|
|
writes++;
|
|
return _inner.write(text);
|
|
}
|
|
}
|
|
|
|
Future<(NoteRepository, _CountingPersistence)> _open() async {
|
|
final p = _CountingPersistence();
|
|
final repo = await NoteRepository.openWith(persistence: p, nodeId: 'dev');
|
|
addTearDown(repo.close);
|
|
return (repo, p);
|
|
}
|
|
|
|
Hlc _clock(NoteRepository repo, String id, String field) =>
|
|
repo.exportLog()[id]!.fields[field]!.$2;
|
|
|
|
void main() {
|
|
test('sets status and priority on every id in ONE write', () async {
|
|
final (repo, p) = await _open();
|
|
for (final id in ['a', 'b', 'c']) {
|
|
await repo.upsert(
|
|
noteFixture(id, 'note $id').copyWith(createdBy: 'kuhy'),
|
|
);
|
|
}
|
|
final textClock = _clock(repo, 'a', 'text');
|
|
final createdClock = _clock(repo, 'a', 'created_at');
|
|
p.writes = 0;
|
|
|
|
await repo.bulkUpdate(
|
|
{'a', 'b', 'c'},
|
|
status: Status.inProgress,
|
|
priority: Priority.high,
|
|
);
|
|
|
|
expect(p.writes, 1);
|
|
final notes = {for (final n in await repo.listNotes()) n.id: n};
|
|
for (final n in notes.values) {
|
|
expect(n.status, Status.inProgress);
|
|
expect(n.priority, Priority.high);
|
|
expect(n.createdBy, 'kuhy');
|
|
}
|
|
expect(_clock(repo, 'a', 'text'), textClock);
|
|
expect(_clock(repo, 'a', 'created_at'), createdClock);
|
|
expect(_clock(repo, 'a', 'status') > textClock, isTrue);
|
|
expect(_clock(repo, 'a', 'updated_at') > textClock, isTrue);
|
|
});
|
|
|
|
test('touches only fields whose value changes', () async {
|
|
final (repo, _) = await _open();
|
|
await repo.upsert(noteFixture('a', 'A', priority: Priority.high));
|
|
final priorityClock = _clock(repo, 'a', 'priority');
|
|
|
|
await repo.bulkUpdate({'a'}, priority: Priority.high, status: Status.done);
|
|
|
|
expect(_clock(repo, 'a', 'priority'), priorityClock);
|
|
expect((await repo.listNotes()).single.status, Status.done);
|
|
});
|
|
|
|
test('no write when nothing changes or ids are unknown/deleted', () async {
|
|
final (repo, p) = await _open();
|
|
await repo.upsert(noteFixture('a', 'A'));
|
|
await repo.upsert(noteFixture('gone', 'G'));
|
|
await repo.delete('gone');
|
|
p.writes = 0;
|
|
|
|
await repo.bulkUpdate({'a'}, priority: Priority.medium);
|
|
await repo.bulkUpdate({'missing', 'gone'}, status: Status.done);
|
|
await repo.bulkUpdate({'a'});
|
|
|
|
expect(p.writes, 0);
|
|
expect(repo.exportLog().containsKey('missing'), isFalse);
|
|
expect(repo.exportLog()['gone']!.fields.containsKey('status'), isTrue);
|
|
});
|
|
|
|
test('saveReplacing saves one note and tombstones the rest', () async {
|
|
final (repo, p) = await _open();
|
|
await repo.upsert(noteFixture('a', 'A').copyWith(createdBy: 'kuhy'));
|
|
await repo.upsert(noteFixture('b', 'B'));
|
|
await repo.upsert(noteFixture('keep', 'K'));
|
|
p.writes = 0;
|
|
|
|
final saved = noteFixture('a', 'A2', status: Status.inProgress);
|
|
await repo.saveReplacing(saved, ['b', 'missing']);
|
|
|
|
expect(p.writes, 1);
|
|
final notes = {for (final n in await repo.listNotes()) n.id: n};
|
|
expect(notes.keys, unorderedEquals(['a', 'keep']));
|
|
expect(notes['a']!.text, 'A2');
|
|
expect(notes['a']!.createdBy, 'kuhy');
|
|
expect(repo.exportLog()['b']!.deleted, isTrue);
|
|
expect(repo.exportLog().containsKey('missing'), isFalse);
|
|
});
|
|
}
|