focus-flow/packages/scheduler_persistence_sqlite/test/scheduler_db_test.dart

391 lines
10 KiB
Dart

// SPDX-FileCopyrightText: 2026 FocusFlow contributors
// SPDX-License-Identifier: AGPL-3.0-only
/// Tests Scheduler Db behavior for the Scheduler Persistence Sqlite package.
library;
import 'dart:io';
import 'package:drift/native.dart';
import 'package:scheduler_persistence_sqlite/sqlite.dart';
import 'package:sqlite3/sqlite3.dart' as sqlite;
import 'package:test/test.dart';
/// Entry point for this executable Dart file.
/// It wires the local command, test, or application behavior needed by this repository without exposing additional public API.
void main() {
test('opens schema version 2 with expected tables', () async {
final db = SchedulerDb(NativeDatabase.memory());
addTearDown(db.close);
expect(db.schemaVersion, 2);
final rows = await db
.customSelect(
"SELECT name FROM sqlite_master "
"WHERE type = 'table' AND name NOT LIKE 'sqlite_%' "
'ORDER BY name',
)
.get();
final tableNames = rows.map((row) => row.data['name']).toList();
expect(
tableNames,
containsAll(<String>[
'application_operations',
'locked_blocks',
'locked_overrides',
'notice_acknowledgements',
'projects',
'project_statistics',
'settings',
'snapshots',
'task_activities',
'tasks',
]),
);
});
test('opens schema version 2 with expected columns', () async {
final db = SchedulerDb(NativeDatabase.memory());
addTearDown(db.close);
Future<List<String>> columnsFor(String tableName) async {
final rows =
await db.customSelect('PRAGMA table_info("$tableName")').get();
return rows.map((row) => row.data['name']! as String).toList();
}
expect(await columnsFor('tasks'), <String>[
'id',
'owner_id',
'title',
'project_id',
'parent_id',
'type',
'status',
'priority',
'reward',
'difficulty',
'duration_minutes',
'scheduled_start_utc',
'scheduled_end_utc',
'actual_start_utc',
'actual_end_utc',
'completed_at_utc',
'backlog_tags_json',
'reminder_override',
'stats_json',
'backlog_entered_at_utc',
'backlog_entered_provenance',
'revision',
'created_at_utc',
'updated_at_utc',
]);
expect(await columnsFor('task_activities'), <String>[
'id',
'owner_id',
'task_id',
'project_id',
'operation_id',
'code',
'occurred_at_utc',
'metadata_json',
]);
expect(await columnsFor('projects'), <String>[
'id',
'owner_id',
'name',
'color_key',
'default_priority',
'default_reward',
'default_difficulty',
'default_reminder_profile',
'default_duration_minutes',
'archived_at_utc',
'revision',
'created_at_utc',
'updated_at_utc',
]);
expect(await columnsFor('project_statistics'), <String>[
'project_id',
'owner_id',
'completed_task_count',
'duration_minute_counts_json',
'completion_time_bucket_counts_json',
'total_pushes_before_completion',
'completed_after_push_count',
'reward_counts_json',
'difficulty_counts_json',
'reminder_profile_counts_json',
'revision',
'created_at_utc',
'updated_at_utc',
]);
expect(await columnsFor('locked_blocks'), <String>[
'id',
'owner_id',
'name',
'date',
'start_time',
'end_time',
'recurrence_json',
'hidden_by_default',
'project_id',
'archived_at_utc',
'revision',
'created_at_utc',
'updated_at_utc',
]);
expect(await columnsFor('locked_overrides'), <String>[
'id',
'owner_id',
'locked_block_id',
'date',
'type',
'name',
'start_time',
'end_time',
'hidden_by_default',
'project_id',
'revision',
'created_at_utc',
'updated_at_utc',
]);
expect(await columnsFor('settings'), <String>[
'owner_id',
'timezone_id',
'day_start_minutes',
'day_end_minutes',
'compact_mode',
'backlog_staleness_json',
'revision',
'created_at_utc',
'updated_at_utc',
]);
expect(await columnsFor('snapshots'), <String>[
'id',
'owner_id',
'captured_at_utc',
'operation_name',
'source_date',
'target_date',
'window_json',
'tasks_json',
'locked_intervals_json',
'required_visible_intervals_json',
'notices_json',
'changes_json',
'overlaps_json',
'retention_expires_utc',
'truncated',
'revision',
'created_at_utc',
'updated_at_utc',
]);
expect(await columnsFor('application_operations'), <String>[
'operation_id',
'owner_id',
'operation_name',
'committed_at_utc',
]);
expect(await columnsFor('notice_acknowledgements'), <String>[
'owner_id',
'notice_id',
'acknowledged_at_utc',
'revision',
'created_at_utc',
'updated_at_utc',
]);
});
test('migrates schema version 1 file and preserves existing rows', () async {
final directory = await Directory.systemTemp.createTemp(
'focus-flow-sqlite-migration-',
);
addTearDown(() => directory.delete(recursive: true));
final file = File('${directory.path}/scheduler.sqlite');
_createVersionOneDatabase(file);
final db = SchedulerDb(NativeDatabase(file));
addTearDown(db.close);
final rows = await db
.customSelect(
"SELECT name FROM sqlite_master "
"WHERE type = 'table' AND name NOT LIKE 'sqlite_%' "
'ORDER BY name',
)
.get();
final tableNames = rows.map((row) => row.data['name']).toList();
expect(tableNames, contains('application_operations'));
expect(tableNames, contains('task_activities'));
expect(tableNames, contains('project_statistics'));
expect(tableNames, contains('notice_acknowledgements'));
final taskRows = await db
.customSelect("SELECT title FROM tasks WHERE id = 'v1-task'")
.get();
final projectRows = await db
.customSelect("SELECT name FROM projects WHERE id = 'home'")
.get();
final settingsRows = await db
.customSelect(
"SELECT timezone_id FROM settings WHERE owner_id = 'owner-1'")
.get();
expect(taskRows.single.data['title'], 'Existing V1 task');
expect(projectRows.single.data['name'], 'Home');
expect(settingsRows.single.data['timezone_id'], 'UTC');
});
}
/// Creates a minimal version-1 SQLite database file.
void _createVersionOneDatabase(File file) {
final database = sqlite.sqlite3.open(file.path);
try {
database
..execute('''
CREATE TABLE tasks (
id TEXT PRIMARY KEY NOT NULL,
owner_id TEXT NOT NULL,
title TEXT NOT NULL,
project_id TEXT NOT NULL,
parent_id TEXT NULL,
type TEXT NOT NULL,
status TEXT NOT NULL,
priority TEXT NULL,
reward TEXT NOT NULL,
difficulty TEXT NOT NULL,
duration_minutes INTEGER NULL,
scheduled_start_utc INTEGER NULL,
scheduled_end_utc INTEGER NULL,
actual_start_utc INTEGER NULL,
actual_end_utc INTEGER NULL,
completed_at_utc INTEGER NULL,
backlog_tags_json TEXT NOT NULL,
reminder_override TEXT NULL,
stats_json TEXT NOT NULL,
backlog_entered_at_utc INTEGER NULL,
backlog_entered_provenance TEXT NULL,
revision INTEGER NOT NULL,
created_at_utc INTEGER NOT NULL,
updated_at_utc INTEGER NOT NULL
)
''')
..execute('''
CREATE TABLE projects (
id TEXT PRIMARY KEY NOT NULL,
owner_id TEXT NOT NULL,
name TEXT NOT NULL,
color_key TEXT NOT NULL,
default_priority TEXT NOT NULL,
default_reward TEXT NOT NULL,
default_difficulty TEXT NOT NULL,
default_reminder_profile TEXT NOT NULL,
default_duration_minutes INTEGER NULL,
archived_at_utc INTEGER NULL,
revision INTEGER NOT NULL,
created_at_utc INTEGER NOT NULL,
updated_at_utc INTEGER NOT NULL
)
''')
..execute('''
CREATE TABLE settings (
owner_id TEXT PRIMARY KEY NOT NULL,
timezone_id TEXT NOT NULL,
day_start_minutes INTEGER NOT NULL,
day_end_minutes INTEGER NOT NULL,
compact_mode INTEGER NOT NULL,
backlog_staleness_json TEXT NOT NULL,
revision INTEGER NOT NULL,
created_at_utc INTEGER NOT NULL,
updated_at_utc INTEGER NOT NULL
)
''')
..execute('''
INSERT INTO tasks (
id,
owner_id,
title,
project_id,
type,
status,
reward,
difficulty,
backlog_tags_json,
stats_json,
revision,
created_at_utc,
updated_at_utc
) VALUES (
'v1-task',
'owner-1',
'Existing V1 task',
'home',
'flexible',
'backlog',
'notSet',
'notSet',
'[]',
'{}',
1,
0,
0
)
''')
..execute('''
INSERT INTO projects (
id,
owner_id,
name,
color_key,
default_priority,
default_reward,
default_difficulty,
default_reminder_profile,
revision,
created_at_utc,
updated_at_utc
) VALUES (
'home',
'owner-1',
'Home',
'project-home',
'medium',
'notSet',
'notSet',
'none',
1,
0,
0
)
''')
..execute('''
INSERT INTO settings (
owner_id,
timezone_id,
day_start_minutes,
day_end_minutes,
compact_mode,
backlog_staleness_json,
revision,
created_at_utc,
updated_at_utc
) VALUES (
'owner-1',
'UTC',
0,
1440,
1,
'{}',
1,
0,
0
)
''')
..execute('PRAGMA user_version = 1');
} finally {
database.close();
}
}