edge_gateway_manager::INSTALL_SESSION_STATUS_RUNNING, 'step' => 'STEP_' . $index, 'message' => 'Installer phase ' . $index, ], strtotime('2026-04-08 10:00:' . str_pad((string)$index, 2, '0', STR_PAD_LEFT))); } $failed = edge_gateway_manager::mergeInstallSessionUpdate($session, [ 'status' => edge_gateway_manager::INSTALL_SESSION_STATUS_FAILED, 'step' => 'START_STACK', 'message' => 'Compose rollout failed during startup.', 'diagnostics' => array_map( static fn(int $index): array => [ 'name' => 'Diagnostic ' . $index, 'output' => 'Output ' . $index, ], range(1, 8) ), ], strtotime('2026-04-08 10:01:30')); expect($failed['status'])->toBe(edge_gateway_manager::INSTALL_SESSION_STATUS_FAILED); expect($failed['step'])->toBe('START_STACK'); expect($failed['message'])->toBe('Compose rollout failed during startup.'); expect($failed['started_at'])->toBe('2026-04-08 10:00:01'); expect($failed['updated_at'])->toBe('2026-04-08 10:01:30'); expect($failed['last_error'])->toBe('Compose rollout failed during startup.'); expect($failed['diagnostics'])->toHaveCount(6); expect($failed['diagnostics'][0]['name'])->toBe('Diagnostic 3'); expect($failed['diagnostics'][5]['name'])->toBe('Diagnostic 8'); expect($failed['events'])->toHaveCount(12); expect($failed['events'][11]['status'])->toBe(edge_gateway_manager::INSTALL_SESSION_STATUS_FAILED); expect($failed['events'][11]['step'])->toBe('START_STACK'); }); it('clears terminal failure details after a successful claim update', function (): void { $claimed = edge_gateway_manager::mergeInstallSessionUpdate([ 'status' => edge_gateway_manager::INSTALL_SESSION_STATUS_FAILED, 'step' => 'START_STACK', 'message' => 'Compose rollout failed during startup.', 'started_at' => '2026-04-08 10:00:01', 'updated_at' => '2026-04-08 10:01:30', 'last_error' => 'Compose rollout failed during startup.', 'diagnostics' => [ ['name' => 'systemctl status', 'output' => 'failed'], ], 'events' => [ [ 'status' => edge_gateway_manager::INSTALL_SESSION_STATUS_FAILED, 'step' => 'START_STACK', 'message' => 'Compose rollout failed during startup.', 'at' => '2026-04-08 10:01:30', ], ], ], [ 'status' => edge_gateway_manager::INSTALL_SESSION_STATUS_CLAIMED, 'step' => edge_gateway_manager::INSTALL_SESSION_STATUS_CLAIMED, 'message' => 'Gateway claimed successfully.', 'gateway_id' => 703, ], strtotime('2026-04-08 10:02:00')); expect($claimed['status'])->toBe(edge_gateway_manager::INSTALL_SESSION_STATUS_CLAIMED); expect($claimed['gateway_id'])->toBe(703); expect($claimed['last_error'])->toBeNull(); expect($claimed['diagnostics'])->toBe([]); expect($claimed['events'])->toHaveCount(2); expect($claimed['events'][1]['status'])->toBe(edge_gateway_manager::INSTALL_SESSION_STATUS_CLAIMED); }); it('marks expired non-terminal install sessions as terminal when read back', function (): void { $normalized = edge_gateway_manager::normalizeInstallSessionRecord([ 'status' => edge_gateway_manager::INSTALL_SESSION_STATUS_RUNNING, 'step' => 'WAIT_FOR_CLAIM', 'message' => 'Installer is waiting for the gateway heartbeat and claim.', 'started_at' => '2026-04-08 10:00:01', 'updated_at' => '2026-04-08 10:01:30', ], '2026-04-08 10:01:00', strtotime('2026-04-08 10:02:00')); expect($normalized['status'])->toBe(edge_gateway_manager::INSTALL_SESSION_STATUS_EXPIRED); expect($normalized['step'])->toBe('WAIT_FOR_CLAIM'); expect($normalized['updated_at'])->toBe('2026-04-08 10:01:30'); expect($normalized['message'])->toBe('Installer is waiting for the gateway heartbeat and claim.'); expect($normalized['last_error'])->toBe('Install token expired.'); expect($normalized['terminal'])->toBeTrue(); });