feat(todo): the person's tasks, sections and labels, sealed and synced (ADR 0033, D6)

A table's rows are sealed for their table and row by one SealedRows,
which the repositories of projects, sections, tasks and labels share,
each table with its own sync gateway, a deletion blanking its rows first
in one transaction. A task's payload holds its title, description,
project, section, parent, place, date (a day, an optional wall-clock
time and duration), deadline, priority, labels and when it was done;
deleting a task deletes its sub-tasks, and a project or a section goes
with its tasks. Rows that don't open show locked; a device without the
account's keys writes nothing.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
This commit is contained in:
Tizian 「ティツィアーン」 2026-11-01 01:14:54 +01:00
commit bd3710ae58
Signed by: raisondetredev
GPG key ID: 07F6FCF405488B64

View file

@ -10,7 +10,7 @@ import 'package:halkyon_todo_flutter/src/feature/project/model/stored_project.da
import 'package:halkyon_ui/halkyon_ui.dart';
import '../../support/memory_content_key_gateway.dart';
import '../../support/memory_project_gateway.dart';
import '../../support/memory_sealed_row_gateway.dart';
/// The person's projects, sealed on the way to the instance and opened on
/// the way back (ADR 0033, D5, D6), with libsodium.
@ -19,7 +19,7 @@ void main() {
late UnlockedAccountKeys alice;
late UnlockedAccountKeys bob;
late MemoryContentKeyGateway keyRows;
late MemoryProjectGateway rows;
late MemorySealedRowGateway rows;
final opened = <TodoKeys>[];
const home = ProjectContent(
@ -42,7 +42,7 @@ void main() {
setUp(() {
keyRows = MemoryContentKeyGateway();
rows = MemoryProjectGateway();
rows = MemorySealedRowGateway();
});
tearDown(() {
@ -61,7 +61,7 @@ void main() {
final laptop = await device(alice);
expect(await laptop.ensureInbox(), id);
expect(rows.rows.keys.where((key) => key == id), hasLength(1));
expect(rows.byId.keys.where((key) => key == id), hasLength(1));
final all = await laptop.all();
expect(all.first, StoredProject(id, ProjectContent.inbox, inbox: true));
expect(all.where((project) => project.inbox), hasLength(1));
@ -78,7 +78,7 @@ void main() {
]);
expect(ids.toSet(), hasLength(1));
expect(rows.rows, hasLength(1));
expect(rows.byId, hasLength(1));
});
test("names nothing of the person's that the instance could read: its "
@ -94,7 +94,7 @@ void main() {
final id = await phone.ensureInbox();
await expectLater(phone.delete(id), throwsA(isA<InboxKept>()));
expect(rows.rows.keys, contains(id));
expect(rows.byId.keys, contains(id));
});
test('is only the row under its ID: a row claiming to be it elsewhere is '
@ -142,7 +142,7 @@ void main() {
'JSON', () async {
await (await device(alice)).save(home);
final sealed = rows.rows.values.single;
final sealed = rows.byId.values.single;
expect(sealed.keyId, isNotNull);
final text = utf8.decode(
Uint8List.sublistView(sealed.payload),
@ -156,8 +156,8 @@ void main() {
test("another person's project, a readable one, or one moved to another "
'row shows locked, never as theirs', () async {
final id = await (await device(alice)).save(home);
final sealed = rows.rows[id]!;
rows.rows
final sealed = rows.byId[id]!;
rows.byId
..['moved'] = sealed
..['readable'] = (
keyId: null,
@ -184,7 +184,7 @@ void main() {
final phone = await device(alice);
final other = await phone.save(home);
final inbox = phone.inboxId;
rows.rows[inbox] = rows.rows[other]!;
rows.byId[inbox] = rows.byId[other]!;
final all = await phone.all();
expect(all.firstWhere((project) => project.inbox).content, isNull);
@ -198,7 +198,7 @@ void main() {
expect((await stranger.all()).single.content, isNull);
await expectLater(stranger.save(home), throwsA(isA<KeysMissing>()));
await expectLater(stranger.ensureInbox(), throwsA(isA<KeysMissing>()));
expect(rows.rows, hasLength(1));
expect(rows.byId, hasLength(1));
});
test('a change here or from another device is told', () async {