layer-wizard/lib/history/history_db.dart
Philip Peterson cb5dcdd666 Add automated tests for HistoryService instead of throwaway scripts
Gives HistoryDb an in-memory mode and reconcile() an injectable
deployments list so HistoryService can be exercised hermetically,
without touching the real history.db or shelling out to rpm-ostree.
Covers bootstrap, idempotent reconcile, install-then-reboot,
manual/detected changes, last_known_safe, and chain walking.

Also fixes the pre-existing widget boot-smoke test: startup now makes
a second real Process.run call (via HistoryService.reconcile), which
tipped flutter_test's fake-time zone into flagging a dangling Timer
from that real I/O. Wrapping the pump in tester.runAsync() is the
standard fix.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-10-07 13:30:07 -08:00

90 lines
2.7 KiB
Dart

import 'dart:ffi';
import 'dart:io';
import 'package:sqlite3/open.dart';
import 'package:sqlite3/sqlite3.dart';
/// Opens the local layer-history database.
///
/// Fedora/Aurora ship `libsqlite3.so.0` but not the unversioned
/// `libsqlite3.so` symlink that `package:sqlite3` looks for by default —
/// that symlink only shows up with `sqlite-devel`, a layer we don't want to
/// require just to run this app. Point `dlopen` at the versioned name
/// instead, same lib, no extra layer needed.
class HistoryDb {
static Database? _instance;
static bool _useInMemory = false;
static String get _stateDir => '${Platform.environment['HOME']}/.local/state/layer_wizard';
static String get _dbPath => '$_stateDir/history.db';
static Database database() {
final existing = _instance;
if (existing != null) return existing;
if (Platform.isLinux) {
open.overrideFor(OperatingSystem.linux, _openLinuxLibSqlite3);
}
final db = _useInMemory ? sqlite3.openInMemory() : _openOnDisk();
_migrate(db);
_instance = db;
return db;
}
static Database _openOnDisk() {
final dir = Directory(_stateDir);
if (!dir.existsSync()) {
dir.createSync(recursive: true);
}
return sqlite3.open(_dbPath);
}
/// Test-only: discards any open connection and makes the next
/// [database] call return a fresh in-memory database instead of touching
/// the real `~/.local/state/layer_wizard/history.db`. Call this from
/// `setUp` in every test that exercises [HistoryService].
static void resetForTesting() {
_instance?.dispose();
_instance = null;
_useInMemory = true;
}
static DynamicLibrary _openLinuxLibSqlite3() {
for (final name in ['libsqlite3.so.0', 'libsqlite3.so']) {
try {
return DynamicLibrary.open(name);
} catch (_) {
// Try the next candidate.
}
}
throw ArgumentError('Could not find libsqlite3.so.0 or libsqlite3.so');
}
static void _migrate(Database db) {
db.execute('''
CREATE TABLE IF NOT EXISTS nodes (
id INTEGER PRIMARY KEY AUTOINCREMENT,
checksum TEXT NOT NULL UNIQUE,
base_checksum TEXT,
parent_id INTEGER REFERENCES nodes(id),
action TEXT NOT NULL,
package_name TEXT,
version TEXT,
container_image_ref TEXT,
requested_packages TEXT,
source TEXT NOT NULL,
created_at TEXT NOT NULL,
applied_at TEXT,
notes TEXT
);
''');
db.execute('CREATE INDEX IF NOT EXISTS idx_nodes_parent ON nodes(parent_id);');
db.execute('''
CREATE TABLE IF NOT EXISTS pointers (
name TEXT PRIMARY KEY,
node_id INTEGER NOT NULL REFERENCES nodes(id)
);
''');
}
}