// 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([ '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> 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'), [ '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'), [ 'id', 'owner_id', 'task_id', 'project_id', 'operation_id', 'code', 'occurred_at_utc', 'metadata_json', ]); expect(await columnsFor('projects'), [ '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'), [ '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'), [ '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'), [ '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'), [ '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'), [ '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'), [ 'operation_id', 'owner_id', 'operation_name', 'committed_at_utc', ]); expect(await columnsFor('notice_acknowledgements'), [ '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(); } }