"""Guards on the installer's durable install record. The record replaced a set of guesses. "Is this a re-run of MY install?" used to be answered from whatever the machine looked like - a MySQL service exists, the database has tables, the site exists - and none of those say who created them. A retry after a failed first install was therefore taken for an upgrade of a working system, which dead-ended the retry the wizard invites the operator to run. Getting it wrong is asymmetric. Answering "still first provisioning" for a server holding real data would let `prune-schema --force` drop live tables. The behavioural checks live in the PowerShell harness next to the installer, run here when pwsh is available; the text assertions below hold everywhere, because CI does not necessarily have pwsh and these are the invariants worth failing a build over. """ import shutil import subprocess from pathlib import Path import pytest REPO = Path(__file__).resolve().parent.parent INSTALLER = REPO / 'deploy' / 'windows' / 'installer' / 'shopdb-install.ps1' HARNESS = REPO / 'deploy' / 'windows' / 'installer' / 'tests' / 'test-install-state.ps1' pytestmark = pytest.mark.skipif( not INSTALLER.is_file(), reason='installer not in this checkout') def installer_text(): return INSTALLER.read_text(encoding='utf-8', errors='replace') def test_prune_force_is_gated_on_first_provisioning(): """--force drops tables that contain rows. It is correct on a first install, where core migrations seed a few plugin reference tables, and destructive on an upgrade, where those rows are the site's data. The gate must therefore test BOTH "database was empty" and the install record, never the database alone. """ text = installer_text() assert "'prune-schema','--yes','--force'" in text, 'the forcing call moved or was renamed' guard = "if ((-not $script:DbWasEmpty) -and (-not $firstRun)) {" assert guard in text, ( 'the prune gate no longer consults the install record. Forcing on the ' 'strength of an empty database alone is what made every retry of a ' 'failed first install fail at stage 3.') def test_first_provisioning_comes_from_the_record_not_the_database(): text = installer_text() assert 'function Test-FirstProvisioning' in text assert '$firstRun = Test-FirstProvisioning' in text, ( 'stage 3 must ask the record, not infer from table counts') def test_missing_record_takes_the_safe_side(): """An install predating the record has a version stamp and probably data. Treating that as a first run would arm prune --force against it. """ text = installer_text() assert "'predates-this-record'" in text, ( 'the fallback for an unrecorded but stamped install is gone; without it ' 'an established server can be classified as first provisioning') def test_created_items_returns_an_array_when_empty(): """A zero-length array returned from a PowerShell function unrolls to $null. $null.Count is then a terminating error under Set-StrictMode -Version 2.0 - the same fault that made bundle verification fail on every install. The leading comma is what prevents it. """ text = installer_text() assert 'return ,@()' in text, 'Get-CreatedItems lost its comma protection' assert 'return ,@(Get-StateValue $created $Kind' in text, ( 'the populated return path lost its comma protection') def test_record_is_written_before_any_stage_runs(): """Written at the START of provisioning, not the end. A run that dies halfway is exactly the run whose retry needs the record. """ text = installer_text() assert "if ($Stage -ne 'uninstall') { Initialize-InstallState }" in text start = text.index('Initialize-InstallState }') switch = text.index('switch ($Stage)', start - 400) assert start < switch, 'the record must be initialised before the stage switch' @pytest.mark.skipif(shutil.which('pwsh') is None, reason='pwsh not installed') def test_install_state_behaviour(): """Run the PowerShell harness: retries, upgrades, corrupt and foreign records.""" assert HARNESS.is_file(), 'the PowerShell harness is missing' result = subprocess.run( ['pwsh', '-NoProfile', '-File', str(HARNESS)], capture_output=True, text=True, timeout=300) assert result.returncode == 0, ( 'install-record harness failed:\n' + result.stdout + result.stderr) assert 'all checks passed' in result.stdout