todo-app/test/note_repository_bulk_test.dart
Krzysztof kuhy Rudnicki 5c1bf75bf9 Bulk note writes and assign-to-repo helper
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
2026-11-10 15:38:54 +01:00

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);
});
}