391 lines
10 KiB
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();
|
|
}
|
|
}
|