layer-wizard/test/history_service_test.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

128 lines
4.5 KiB
Dart

import 'package:flutter_test/flutter_test.dart';
import 'package:layer_wizard/history/history_db.dart';
import 'package:layer_wizard/history/history_service.dart';
import 'package:layer_wizard/ostree_service.dart';
DeploymentInfo _deployment({
required String checksum,
bool booted = false,
bool staged = false,
String version = 'v1',
List<String> requestedPackages = const [],
}) {
return DeploymentInfo(
version: version,
checksum: checksum,
containerImageRef: 'ostree-image-signed:docker://ghcr.io/ublue-os/aurora:stable',
requestedPackages: requestedPackages,
booted: booted,
staged: staged,
);
}
void main() {
final history = HistoryService.instance;
setUp(() {
HistoryDb.resetForTesting();
});
test('first reconcile bootstraps a root node and both pointers', () async {
final a = _deployment(checksum: 'aaa', booted: true);
final current = await history.reconcile(deployments: [a]);
expect(current.checksum, 'aaa');
expect(current.parentId, isNull);
expect(current.source, 'detected');
final safe = await history.getLastKnownSafe();
expect(safe?.checksum, 'aaa');
});
test('reconcile is idempotent for an already-known checksum', () async {
final a = _deployment(checksum: 'aaa', booted: true);
await history.reconcile(deployments: [a]);
await history.reconcile(deployments: [a]);
await history.reconcile(deployments: [a]);
final all = await history.getAllNodes();
expect(all, hasLength(1));
});
test('recordInstall adds a child of current without moving current', () async {
final a = _deployment(checksum: 'aaa', booted: true);
final root = await history.reconcile(deployments: [a]);
final staged = _deployment(checksum: 'bbb', staged: true, requestedPackages: ['vim']);
final node = await history.recordInstall(packageName: 'vim', staged: staged);
expect(node.parentId, root.id);
expect(node.action, 'install');
expect(node.source, 'layer_wizard');
expect(node.packageName, 'vim');
final current = await history.getCurrent();
expect(current?.id, root.id, reason: 'current must not move until the staged deployment is booted');
});
test('rebooting into a staged install moves current without a duplicate row', () async {
final a = _deployment(checksum: 'aaa', booted: true);
await history.reconcile(deployments: [a]);
final staged = _deployment(checksum: 'bbb', staged: true, requestedPackages: ['vim']);
final installed = await history.recordInstall(packageName: 'vim', staged: staged);
final bootedAfterReboot = _deployment(checksum: 'bbb', booted: true, requestedPackages: ['vim']);
final current = await history.reconcile(deployments: [bootedAfterReboot]);
expect(current.id, installed.id);
final all = await history.getAllNodes();
expect(all, hasLength(2), reason: 'no new row — same checksum as the staged install');
});
test('a manual change (unknown checksum) is recorded as detected, parented on current', () async {
final a = _deployment(checksum: 'aaa', booted: true);
final root = await history.reconcile(deployments: [a]);
final manual = _deployment(checksum: 'ccc', booted: true, requestedPackages: ['htop']);
final current = await history.reconcile(deployments: [manual]);
expect(current.checksum, 'ccc');
expect(current.source, 'detected');
expect(current.parentId, root.id);
final all = await history.getAllNodes();
expect(all, hasLength(2));
});
test('markCurrentAsSafe moves last_known_safe to the current node', () async {
final a = _deployment(checksum: 'aaa', booted: true);
await history.reconcile(deployments: [a]);
final b = _deployment(checksum: 'bbb', booted: true);
await history.reconcile(deployments: [b]);
expect((await history.getLastKnownSafe())?.checksum, 'aaa');
await history.markCurrentAsSafe();
expect((await history.getLastKnownSafe())?.checksum, 'bbb');
});
test('getChainToCurrent walks root to current in order', () async {
final a = _deployment(checksum: 'aaa', booted: true);
await history.reconcile(deployments: [a]);
final staged = _deployment(checksum: 'bbb', staged: true, requestedPackages: ['vim']);
await history.recordInstall(packageName: 'vim', staged: staged);
final bootedAfterReboot = _deployment(checksum: 'bbb', booted: true, requestedPackages: ['vim']);
await history.reconcile(deployments: [bootedAfterReboot]);
final chain = await history.getChainToCurrent();
expect(chain.map((n) => n.checksum), ['aaa', 'bbb']);
});
}