309 lines
15 KiB
Python
309 lines
15 KiB
Python
from __future__ import annotations
|
|
|
|
import os
|
|
import subprocess
|
|
import unittest
|
|
from pathlib import Path
|
|
|
|
|
|
SCRIPTS = Path(__file__).resolve().parent
|
|
BOOTSTRAP = SCRIPTS / "bootstrap-legacy-public-runtime-upload-root.ps1"
|
|
START = SCRIPTS / "start-public-runtime.ps1"
|
|
INITIALIZER = SCRIPTS / "initialize-public-runtime-upload-root.ps1"
|
|
|
|
|
|
class LegacyPublicRuntimeUploadBootstrapContractTest(unittest.TestCase):
|
|
@classmethod
|
|
def setUpClass(cls) -> None:
|
|
cls.bootstrap = BOOTSTRAP.read_text(encoding="utf-8")
|
|
cls.start = START.read_text(encoding="utf-8")
|
|
cls.initializer = INITIALIZER.read_text(encoding="utf-8")
|
|
|
|
def test_success_state_machine_has_exact_irreversible_order(self) -> None:
|
|
markers = (
|
|
"LEGACY_BOOTSTRAP_STAGE:task_maintenance_enter",
|
|
"LEGACY_BOOTSTRAP_STAGE:exact_runtime_capture",
|
|
"LEGACY_BOOTSTRAP_STAGE:tunnel_quiescence",
|
|
"LEGACY_BOOTSTRAP_STAGE:listener_quiescence",
|
|
"LEGACY_BOOTSTRAP_STAGE:offline_initializer",
|
|
"LEGACY_BOOTSTRAP_STAGE:new_api_frozen",
|
|
"LEGACY_BOOTSTRAP_STAGE:new_tunnel_public_frozen",
|
|
"LEGACY_BOOTSTRAP_STAGE:no_rollback_boundary",
|
|
"LEGACY_BOOTSTRAP_STAGE:write_release",
|
|
"LEGACY_BOOTSTRAP_STAGE:cutover_receipt_publish",
|
|
"LEGACY_BOOTSTRAP_STAGE:task_maintenance_exit",
|
|
"LEGACY_BOOTSTRAP_STAGE:task_recovery_receipt_publish",
|
|
)
|
|
positions = [self.bootstrap.index(marker) for marker in markers]
|
|
self.assertEqual(sorted(positions), positions)
|
|
boundary = self.bootstrap.index("$noRollback = $true", positions[6])
|
|
release = self.bootstrap.index("Remove-OwnedFreezeByHash", boundary)
|
|
self.assertLess(boundary, release)
|
|
|
|
def test_bootstrap_holds_one_inherited_lock_and_competing_start_cannot_enter(
|
|
self,
|
|
) -> None:
|
|
acquire = self.bootstrap.index("Enter-InheritedBootstrapRecoveryLock")
|
|
task_enter = self.bootstrap.index("LEGACY_BOOTSTRAP_STAGE:task_maintenance_enter")
|
|
start_call = self.bootstrap.index("& $startScript")
|
|
dispose = self.bootstrap.rindex("$bootstrapLock.Stream.Dispose()")
|
|
self.assertLess(acquire, task_enter)
|
|
self.assertLess(start_call, dispose)
|
|
for marker in (
|
|
"[System.IO.FileShare]::Read",
|
|
"-InheritedRecoveryLockReceiptPath $bootstrapLock.Path",
|
|
"-ExpectedInheritedRecoveryLockReceiptSha256 $bootstrapLock.ReceiptSha256",
|
|
):
|
|
self.assertIn(marker, self.bootstrap)
|
|
self.assertLess(
|
|
self.bootstrap.index("Legacy preserved source inventory preflight failed"),
|
|
self.bootstrap.index("LEGACY_BOOTSTRAP_STAGE:task_maintenance_enter"),
|
|
)
|
|
self.assertIn("Inherited recovery lock is not held", self.start)
|
|
self.assertIn("[System.IO.FileShare]::None", self.start)
|
|
self.assertIn("[int]$payload.owner_pid -ne $PID", self.start)
|
|
|
|
def test_quiescence_race_is_rechecked_around_initializer_and_api_start(self) -> None:
|
|
self.assertGreaterEqual(
|
|
self.bootstrap.count("Assert-OfflinedRuntimeRaceGate"), 4
|
|
)
|
|
initializer = self.bootstrap.index("& $initializer")
|
|
api_start = self.bootstrap.index("& $startScript")
|
|
race_positions = []
|
|
offset = 0
|
|
while True:
|
|
found = self.bootstrap.find("Assert-OfflinedRuntimeRaceGate", offset)
|
|
if found < 0:
|
|
break
|
|
race_positions.append(found)
|
|
offset = found + 1
|
|
self.assertTrue(any(position < initializer for position in race_positions))
|
|
self.assertTrue(any(initializer < position < api_start for position in race_positions))
|
|
self.assertIn("Assert-PublicRuntimeTasksDisabledAndIdle", self.bootstrap)
|
|
self.assertIn("Assert-LoopbackListenerAbsent", self.bootstrap)
|
|
self.assertIn("Assert-TunnelAbsent", self.bootstrap)
|
|
|
|
def test_rollback_stops_only_exact_owned_replacements_tunnel_first(self) -> None:
|
|
restore = self.bootstrap[
|
|
self.bootstrap.index("function Restore-LegacyRuntime") :
|
|
self.bootstrap.index("$resolvedStableSourceRoot =", self.bootstrap.index("function Restore-LegacyRuntime"))
|
|
]
|
|
tunnel = restore.index('Role "owned new tunnel"')
|
|
api = restore.index('Role "owned new API"')
|
|
self.assertLess(tunnel, api)
|
|
self.assertIn("Rollback refuses to stop an unowned tunnel", restore)
|
|
self.assertIn("Rollback refuses to stop an unowned API listener", restore)
|
|
self.assertNotIn("foreach ($tunnel in $tunnelMatches)", restore)
|
|
self.assertIn("-OwnedNewApi $newApiIdentity", self.bootstrap)
|
|
self.assertIn("-OwnedNewTunnel $newTunnelIdentity", self.bootstrap)
|
|
|
|
def test_pre_boundary_restores_then_enables_post_boundary_disables(self) -> None:
|
|
catch = self.bootstrap[
|
|
self.bootstrap.index("} catch {\n $rollbackSucceeded") :
|
|
]
|
|
restore = catch.index("Restore-LegacyRuntime")
|
|
task_exit = catch.index("Exit-PublicRuntimeTaskMaintenance")
|
|
self.assertLess(restore, task_exit)
|
|
self.assertIn("if (-not $noRollback)", catch)
|
|
self.assertIn("Suspend-PublicRuntimeTasks", catch)
|
|
self.assertIn("tasks_remain_disabled = [bool]$taskTruth.all_disabled_and_idle", catch)
|
|
self.assertIn("cutover_receipt_published", catch)
|
|
|
|
def test_prior_effective_dotenv_and_new_pid_environment_are_hash_bound(self) -> None:
|
|
for marker in (
|
|
'Join-Path ([string]$Identity.cwd) ".env"',
|
|
'check-ignore --quiet -- "apps/api/.env"',
|
|
"Set-CompleteProcessEnvironment -Environment $Identity.environment",
|
|
"from app.config import settings as s",
|
|
"Assert-NewApiEnvironmentMatches",
|
|
"Get-ConnectedDatabaseTargetSha256",
|
|
"manifestProof.Payload.database_target_sha256",
|
|
"required_environment_sha256 = $requiredEnvironmentDigest",
|
|
):
|
|
self.assertIn(marker, self.bootstrap)
|
|
receipt = self.bootstrap[
|
|
self.bootstrap.index("$cutoverReceipt =") :
|
|
self.bootstrap.index("Write-PrivacySafeReceiptCreateOnly", self.bootstrap.index("$cutoverReceipt ="))
|
|
]
|
|
self.assertNotIn("DATABASE_URL =", receipt)
|
|
self.assertNotIn("OAUTH_GOOGLE_CLIENT_SECRET =", receipt)
|
|
self.assertNotIn("SESSION_SECRET =", receipt)
|
|
|
|
def test_google_only_contract_is_checked_local_public_frozen_and_unfrozen(self) -> None:
|
|
self.assertGreaterEqual(self.bootstrap.count("Wait-GoogleAuthContract"), 5)
|
|
self.assertIn('$enabledProviders[0] -eq "google"', self.bootstrap)
|
|
self.assertIn("$config.dev_login_enabled -eq $false", self.bootstrap)
|
|
self.assertGreaterEqual(self.bootstrap.count("Assert-CurrentTunnelIdentity"), 3)
|
|
self.assertIn("public_unfrozen = $true", self.bootstrap)
|
|
|
|
def test_legacy_upload_root_falls_back_only_when_variable_is_absent(self) -> None:
|
|
source_root = self.bootstrap[
|
|
self.bootstrap.index("function Get-ValidatedLegacySourceRoots") :
|
|
self.bootstrap.index("function Restore-LegacyRuntime")
|
|
]
|
|
present = source_root.index("$declaredPresent = $true")
|
|
empty_abort = source_root.index(
|
|
'throw "Legacy USER_UPLOAD_DIR is present but empty"', present
|
|
)
|
|
fallback = source_root.index("if (-not $declaredPresent)", empty_abort)
|
|
self.assertLess(present, empty_abort)
|
|
self.assertLess(empty_abort, fallback)
|
|
for marker in (
|
|
"$resolvedAllowedRoots += $resolvedAllowed",
|
|
'throw "Allowed legacy upload roots contain a duplicate"',
|
|
"source_root_sha256s = @($sourceRootSha256s)",
|
|
"source_root_set_sha256 = Get-Utf8Sha256",
|
|
"vignette.public-upload-offline-quiescence-capture.v2",
|
|
"-SourceUploadDir $legacySourceRoots",
|
|
"Get-ValidatedExplicitLegacySourceRoots",
|
|
"Legacy bootstrap requires exactly three explicit source roots",
|
|
'"probe-preserved-inventory"',
|
|
"-ExpectedPreservedObjectCount $ExpectedPreservedObjectCount",
|
|
"-ExpectedPreservedInventorySha256 $ExpectedPreservedInventorySha256",
|
|
"Legacy preserved source inventory preflight failed",
|
|
'Test-ArgumentPair -Arguments $arguments -Name "--workers" -Value "1"',
|
|
"initializerPayload.preserved_inventory_sha256",
|
|
"initializerPayload.preserved_total_size_bytes",
|
|
"preservedProbeDecodeCounts.ValidCount",
|
|
"initializerPreservedDecodeCounts.ValidCount",
|
|
"initializerRequiredDecodeCounts.ObjectCount",
|
|
"manifestProof.Payload.preserved_object_set_sha256",
|
|
"manifestProof.Payload.preserved_total_size_bytes",
|
|
"manifestPreservedDecodeCounts.ValidCount",
|
|
"manifestRequiredDecodeCounts.ObjectCount",
|
|
"manifestCurrentDecodeCounts.ObjectCount",
|
|
"Offline initializer preserved inventory proof drifted",
|
|
"Offline initializer decode proof drifted across preflight, manifest, or current DB",
|
|
'"--expected-preserved-total-size-bytes"',
|
|
"preservedProbePayload.preserved_total_size_bytes",
|
|
"-ExpectedPreservedTotalSizeBytes $ExpectedPreservedTotalSizeBytes",
|
|
):
|
|
self.assertIn(marker, self.bootstrap)
|
|
self.assertNotIn(
|
|
"Offline quiescence initialization requires exactly one validated source root",
|
|
self.initializer,
|
|
)
|
|
|
|
def test_total_size_pin_is_recorded_in_all_success_receipts(self) -> None:
|
|
total_size_field = (
|
|
"preserved_total_size_bytes = "
|
|
"[long]$manifestProof.Payload.preserved_total_size_bytes"
|
|
)
|
|
cutover = self.bootstrap[
|
|
self.bootstrap.index("$cutoverReceipt =") :
|
|
self.bootstrap.index(
|
|
"Write-PrivacySafeReceiptCreateOnly",
|
|
self.bootstrap.index("$cutoverReceipt ="),
|
|
)
|
|
]
|
|
task_recovery = self.bootstrap[
|
|
self.bootstrap.index("$taskRecoveryReceipt =") :
|
|
self.bootstrap.index(
|
|
"Write-PrivacySafeReceiptCreateOnly",
|
|
self.bootstrap.index("$taskRecoveryReceipt ="),
|
|
)
|
|
]
|
|
final_result = self.bootstrap[
|
|
self.bootstrap.rindex("$result = [ordered]@{") :
|
|
]
|
|
self.assertIn(total_size_field, cutover)
|
|
self.assertIn(total_size_field, task_recovery)
|
|
self.assertIn(total_size_field, final_result)
|
|
|
|
def test_decode_counts_are_validated_and_recorded_in_all_success_receipts(
|
|
self,
|
|
) -> None:
|
|
for expected in (
|
|
"function Get-RequiredPrivacySafeCount",
|
|
"function Get-PreservedDecodeCountProof",
|
|
"function Get-RequiredDecodeInvalidCountProof",
|
|
"function Get-CurrentDecodeInvalidCountProof",
|
|
'Name "preserved_decode_valid_count"',
|
|
'Name "preserved_decode_invalid_count"',
|
|
'Name "required_decode_invalid_object_count"',
|
|
'Name "required_decode_invalid_reference_count"',
|
|
'Name "current_decode_invalid_object_count"',
|
|
'Name "current_decode_invalid_reference_count"',
|
|
):
|
|
with self.subTest(expected=expected):
|
|
self.assertIn(expected, self.bootstrap)
|
|
|
|
proof_compare = self.bootstrap[
|
|
self.bootstrap.index("$initializerRequiredObjectCount =") :
|
|
self.bootstrap.index("LEGACY_BOOTSTRAP_STAGE:new_api_frozen")
|
|
]
|
|
for expected in (
|
|
"$preservedProbeDecodeCounts.ValidCount",
|
|
"$initializerPreservedDecodeCounts.ValidCount",
|
|
"$initializerRequiredDecodeCounts.ObjectCount",
|
|
"$manifestPreservedDecodeCounts.ValidCount",
|
|
"$manifestRequiredDecodeCounts.ObjectCount",
|
|
"$manifestCurrentDecodeCounts.ObjectCount",
|
|
):
|
|
with self.subTest(proof=expected):
|
|
self.assertIn(expected, proof_compare)
|
|
|
|
cutover = self.bootstrap[
|
|
self.bootstrap.index("$cutoverReceipt =") :
|
|
self.bootstrap.index(
|
|
"Write-PrivacySafeReceiptCreateOnly",
|
|
self.bootstrap.index("$cutoverReceipt ="),
|
|
)
|
|
]
|
|
task_recovery = self.bootstrap[
|
|
self.bootstrap.index("$taskRecoveryReceipt =") :
|
|
self.bootstrap.index(
|
|
"Write-PrivacySafeReceiptCreateOnly",
|
|
self.bootstrap.index("$taskRecoveryReceipt ="),
|
|
)
|
|
]
|
|
final_result = self.bootstrap[self.bootstrap.rindex("$result = [ordered]@{") :]
|
|
receipt_fields = {
|
|
"preserved_decode_valid_count": "manifestPreservedDecodeCounts.ValidCount",
|
|
"preserved_decode_invalid_count": "manifestPreservedDecodeCounts.InvalidCount",
|
|
"required_decode_invalid_object_count": "manifestRequiredDecodeCounts.ObjectCount",
|
|
"required_decode_invalid_reference_count": "manifestRequiredDecodeCounts.ReferenceCount",
|
|
"current_decode_invalid_object_count": "manifestCurrentDecodeCounts.ObjectCount",
|
|
"current_decode_invalid_reference_count": "manifestCurrentDecodeCounts.ReferenceCount",
|
|
}
|
|
for receipt_name, receipt in (
|
|
("cutover", cutover),
|
|
("task_recovery", task_recovery),
|
|
("final", final_result),
|
|
):
|
|
for field, source in receipt_fields.items():
|
|
with self.subTest(receipt=receipt_name, field=field):
|
|
self.assertIn(f"{field} = [int]${source}", receipt)
|
|
|
|
def test_initializer_online_cleanup_and_offline_cleanup_are_separate(self) -> None:
|
|
self.assertIn("Assert-OnlineUploadWritesRecovered", self.initializer)
|
|
self.assertIn("$freeze.active -eq $false", self.initializer)
|
|
self.assertIn("$freeze.valid -eq $true", self.initializer)
|
|
self.assertIn("[int]$freeze.in_flight -eq 0", self.initializer)
|
|
self.assertIn("if (-not $offlineQuiescenceMode)", self.initializer)
|
|
|
|
@unittest.skipUnless(os.name == "nt", "PowerShell 5.1 is Windows-only")
|
|
def test_all_bootstrap_contract_scripts_parse_in_windows_powershell_51(self) -> None:
|
|
powershell = Path(os.environ["SystemRoot"]) / "System32/WindowsPowerShell/v1.0/powershell.exe"
|
|
files = (BOOTSTRAP, START, INITIALIZER)
|
|
quoted = ",".join("'" + str(path).replace("'", "''") + "'" for path in files)
|
|
command = (
|
|
"$ErrorActionPreference='Stop';"
|
|
f"$files=@({quoted});"
|
|
"foreach($file in $files){$tokens=$null;$errors=$null;"
|
|
"[void][System.Management.Automation.Language.Parser]::ParseFile($file,[ref]$tokens,[ref]$errors);"
|
|
"if($errors.Count -gt 0){exit 7}};exit 0"
|
|
)
|
|
result = subprocess.run(
|
|
[str(powershell), "-NoProfile", "-Command", command],
|
|
check=False,
|
|
capture_output=True,
|
|
text=True,
|
|
timeout=30,
|
|
)
|
|
self.assertEqual(0, result.returncode, msg=result.stderr)
|
|
self.assertNotIn("$pid =", self.bootstrap.lower())
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|