todo-app/test/repo_assign_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

110 lines
3.6 KiB
Dart

/// Assigning plain notes to a repo: sections in its one open note.
library;
import 'package:flutter_test/flutter_test.dart';
import 'package:todo/data/note.dart';
import 'package:todo/data/note_repository.dart';
import 'package:todo/data/repo_assign.dart';
import 'package:todo/data/repo_note_split.dart';
import 'note_repository_helpers.dart';
Future<NoteRepository> _repo(List<Note> seed) async {
final repo = await NoteRepository.openInMemory();
addTearDown(repo.close);
for (final n in seed) {
await repo.upsert(n);
}
return repo;
}
Note _task(String id, String text, String repo, {Status? status}) =>
noteFixture(
id,
text,
status: status ?? Status.todo,
createdAt: DateTime(2026),
).copyWith(repo: repo, doneCheck: 'true');
void main() {
test('appends each plain note as one section and removes it', () async {
final repo = await _repo([
_task('r', 'app todos\n\n## Old\nbody', 'kuhyx/app'),
_task('closed', 'app todos\n\n## x', 'kuhyx/app', status: Status.done),
noteFixture('p1', 'Fix login\n\n## what\n_why_\nit 500s'),
noteFixture('p2', 'Tidy', priority: Priority.high),
]);
final plain = (await repo.listNotes()).where((n) => n.repo == null).toList()
..sort((a, b) => a.id.compareTo(b.id));
// Sections land in the order given (the UI passes selection order).
final refused = await assignToRepo(repo, plain, 'kuhyx/app');
expect(refused, isEmpty);
final notes = {for (final n in await repo.listNotes()) n.id: n};
expect(notes.keys, unorderedEquals(['r', 'closed']));
final text = notes['r']!.text;
expect(openSections(text), ['Old', 'Fix login', 'Tidy']);
expect(text, contains('### what\n_why_\nit 500s'));
expect(notes['r']!.priority, Priority.high);
expect(notes['r']!.doneCheck, 'true');
});
test('creates the repo note when the repo has none open', () async {
final repo = await _repo([noteFixture('p', 'Ship it\nnow')]);
await assignToRepo(repo, await repo.listNotes(), 'kuhyx/new-app');
final made = (await repo.listNotes()).single;
expect(made.id, isNot('p'));
expect(made.repo, 'kuhyx/new-app');
expect(made.status, Status.todo);
expect(made.createdBy, 'kuhy');
expect(made.text, 'new-app todos\n\n## Ship it\nnow');
});
test('refuses notes that already have a repo, moves the rest', () async {
final repo = await _repo([
_task('t', 'other todos\n\n## A', 'kuhyx/other'),
noteFixture('p', 'Plain'),
]);
final refused = await assignToRepo(
repo,
await repo.listNotes(),
'kuhyx/app',
);
expect(refused.map((n) => n.id), ['t']);
final notes = {for (final n in await repo.listNotes()) n.id: n};
expect(notes['t']!.text, 'other todos\n\n## A');
expect(notes.containsKey('p'), isFalse);
});
test('only repo notes: a no-op that returns them all', () async {
final repo = await _repo([_task('t', 'other todos', 'kuhyx/other')]);
final before = repo.exportLog();
final refused = await assignToRepo(
repo,
await repo.listNotes(),
'kuhyx/app',
);
expect(refused, hasLength(1));
expect(repo.exportLog(), before);
});
test('demoteHeadings keeps a body inside its section', () {
expect(
demoteHeadings('# a\n## b\n### c\n```\n## code\n```\ntext #x'),
'### a\n### b\n#### c\n```\n## code\n```\ntext #x',
);
});
test('isRepoName accepts owner/name only', () {
expect(isRepoName('kuhyx/todo-app'), isTrue);
expect(isRepoName('kuhyx/'), isFalse);
expect(isRepoName('todo'), isFalse);
expect(isRepoName('a/b/c'), isFalse);
});
}