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>
128 lines
4.5 KiB
Dart
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']);
|
|
});
|
|
}
|