아바타 저장소 승격 계약을 완성
This commit is contained in:
parent
ac9b702688
commit
ccdcfcd2f5
36 changed files with 14734 additions and 222 deletions
|
|
@ -16,6 +16,25 @@ WHISPER_START = (SCRIPTS / "start-local-whisper-stt.ps1").read_text(
|
|||
|
||||
|
||||
class PublicRuntimeVoiceContractTest(unittest.TestCase):
|
||||
def test_public_api_is_forced_to_one_uvicorn_worker(self) -> None:
|
||||
launch_start = PUBLIC_RUNTIME.index(
|
||||
'$proc = Start-Process -WindowStyle Hidden -FilePath $Python'
|
||||
)
|
||||
api_launch = PUBLIC_RUNTIME[
|
||||
launch_start : PUBLIC_RUNTIME.index(
|
||||
'Start-Sleep -Seconds 3', launch_start
|
||||
)
|
||||
]
|
||||
self.assertIn('"--workers", "1"', api_launch)
|
||||
self.assertIn(
|
||||
'"--port", "$ApiPort", "--workers", "1"',
|
||||
" ".join(api_launch.split()),
|
||||
)
|
||||
self.assertIn(
|
||||
'@("uvicorn", "app.main:app", "--port", "$ApiPort", "--workers", "1"',
|
||||
api_launch,
|
||||
)
|
||||
|
||||
def test_recovery_is_serialized_and_lock_is_always_released(self) -> None:
|
||||
lock = PUBLIC_RUNTIME.index("$recoveryLock = Enter-RecoveryLock")
|
||||
main_try = PUBLIC_RUNTIME.index("try {", lock)
|
||||
|
|
@ -125,10 +144,16 @@ exit 0
|
|||
if powershell is None:
|
||||
self.skipTest("Windows PowerShell 5.1 is not available")
|
||||
|
||||
environment_repair = PUBLIC_RUNTIME[
|
||||
PUBLIC_RUNTIME.index(
|
||||
"function Repair-CaseInsensitiveProcessEnvironment"
|
||||
) : PUBLIC_RUNTIME.index("$resolvedWorkspace")
|
||||
].strip()
|
||||
with tempfile.TemporaryDirectory() as temporary_directory:
|
||||
harness = Path(temporary_directory) / "process-exit-code.ps1"
|
||||
harness.write_text(
|
||||
"""
|
||||
environment_repair
|
||||
+ """
|
||||
$process = Start-Process -FilePath 'cmd.exe' `
|
||||
-ArgumentList @('/c', 'exit 0') `
|
||||
-NoNewWindow `
|
||||
|
|
@ -201,7 +226,7 @@ try {{
|
|||
}}
|
||||
if (-not (Test-Path -LiteralPath '{quoted_child_pid_for_harness}')) {{ exit 2 }}
|
||||
$childProcessId = [int](Get-Content -LiteralPath '{quoted_child_pid_for_harness}' -Raw)
|
||||
$stopped = @(Stop-ProcessTreeBounded -RootProcessId $root.Id -TimeoutSec 10 -Role 'test tree')
|
||||
$stopped = @(Stop-ProcessTreeBounded -RootProcess $root -TimeoutSec 10 -Role 'test tree')
|
||||
$exitDeadline = (Get-Date).AddSeconds(10)
|
||||
do {{
|
||||
$rootAlive = $null -ne (Get-Process -Id $root.Id -ErrorAction SilentlyContinue)
|
||||
|
|
@ -349,7 +374,7 @@ class FreshPublicProvenanceContractTest(unittest.TestCase):
|
|||
"output preflight failed before runtime mutation",
|
||||
"[System.IO.FileMode]::Open",
|
||||
"[System.IO.FileAccess]::ReadWrite",
|
||||
"[System.IO.File]::Replace($probeSource, $probeTarget, $probeBackup)",
|
||||
"[System.IO.File]::Replace($probeSource, $probeTarget, $probeBackup, $true)",
|
||||
"[System.IO.File]::Delete($probeTarget)",
|
||||
):
|
||||
with self.subTest(expected=expected):
|
||||
|
|
@ -363,7 +388,7 @@ class FreshPublicProvenanceContractTest(unittest.TestCase):
|
|||
for expected in (
|
||||
"[System.IO.FileMode]::CreateNew",
|
||||
"$stream.Flush($true)",
|
||||
"[System.IO.File]::Replace($temporaryPath, $OutputPath, $backupPath)",
|
||||
"[System.IO.File]::Replace($temporaryPath, $OutputPath, $backupPath, $true)",
|
||||
"[System.IO.File]::Move($temporaryPath, $OutputPath)",
|
||||
"[System.IO.File]::Delete($temporaryPath)",
|
||||
):
|
||||
|
|
@ -411,12 +436,14 @@ $failedPath = Write-FailedFreshPromotionEvidence `
|
|||
-RollbackSucceeded $true `
|
||||
-RollbackResult @{{local_health=$true;public_health=$true}} `
|
||||
-SourceCommit ('a' * 40) `
|
||||
-SourceTree ('b' * 40)
|
||||
-SourceTree ('b' * 40) `
|
||||
-UserUploadRoot 'C:\\stable-uploads'
|
||||
$failed = Get-Content -LiteralPath $failedPath -Raw -Encoding UTF8 | ConvertFrom-Json
|
||||
if ((Split-Path -Leaf $failedPath) -notlike '*.failed.log') {{ throw 'failure evidence suffix mismatch' }}
|
||||
if ($failed.status -ne 'failed_rolled_back') {{ throw 'failure evidence status mismatch' }}
|
||||
if ($failed.failure_stage -ne 'receipt_publish') {{ throw 'failure evidence stage mismatch' }}
|
||||
if (-not $failed.rollback.succeeded) {{ throw 'failure evidence rollback mismatch' }}
|
||||
if ($failed.storage.user_upload_root -cne 'C:\\stable-uploads') {{ throw 'failure evidence upload root mismatch' }}
|
||||
if (@(Get-ChildItem -LiteralPath (Split-Path -Parent $resolved) -Filter '*.tmp').Count -ne 0) {{
|
||||
throw 'temporary receipt files were not cleaned'
|
||||
}}
|
||||
|
|
@ -478,7 +505,9 @@ if ($preserved.status -ne 'replaced') {{ throw 'failed preflight changed the pri
|
|||
receipt = PUBLIC_RUNTIME.index("$provenance = [ordered]@{")
|
||||
api_section = PUBLIC_RUNTIME[api:cloud]
|
||||
cloud_section = PUBLIC_RUNTIME[cloud:receipt]
|
||||
self.assertIn("Stop-UvicornByPort `", api_section)
|
||||
self.assertIn("Get-ExactLoopbackListenerProcess `", api_section)
|
||||
self.assertIn('Where-Object { $_.LocalAddress -eq "127.0.0.1" }', PUBLIC_RUNTIME)
|
||||
self.assertNotIn("Stop-UvicornByPort `", api_section)
|
||||
self.assertIn("-TimeoutSec $ProcessStopTimeoutSeconds", api_section)
|
||||
self.assertIn("-WorkingDirectory $ApiDir", api_section)
|
||||
self.assertIn("did not receive a replacement PID", api_section)
|
||||
|
|
@ -507,6 +536,15 @@ if ($preserved.status -ne 'replaced') {{ throw 'failed preflight changed the pri
|
|||
self.assertIn("if ($RequireFreshPublicProvenance)", cloud_section)
|
||||
self.assertIn("-ConfigPath $resolvedCloudflaredConfig `", cloud_section)
|
||||
self.assertIn("-ExactPath", cloud_section)
|
||||
self.assertIn(
|
||||
"Fresh public tunnel config was reacquired by an unpinned process before launch",
|
||||
cloud_section,
|
||||
)
|
||||
fresh_branch = cloud_section[
|
||||
cloud_section.index("if ($RequireFreshPublicProvenance)") :
|
||||
cloud_section.index("} else {")
|
||||
]
|
||||
self.assertNotIn("Stop-ProcessesBounded `", fresh_branch)
|
||||
matcher = PUBLIC_RUNTIME[
|
||||
PUBLIC_RUNTIME.index("function Get-CloudflaredProcessesForConfig") :
|
||||
PUBLIC_RUNTIME.index("function Wait-ProcessIdentity")
|
||||
|
|
@ -537,6 +575,58 @@ if ($preserved.status -ne 'replaced') {{ throw 'failed preflight changed the pri
|
|||
self.assertNotIn("credential", config_projection)
|
||||
self.assertNotIn("contents", config_projection)
|
||||
|
||||
def test_decode_proofs_are_cross_checked_and_bound_to_success_receipts(
|
||||
self,
|
||||
) -> None:
|
||||
manifest_validation = PUBLIC_RUNTIME[
|
||||
PUBLIC_RUNTIME.index("$uploadManifestProof =") :
|
||||
PUBLIC_RUNTIME.index("if ($RequireFreshPublicProvenance) {", PUBLIC_RUNTIME.index("$uploadManifestProof =") + 1)
|
||||
]
|
||||
for expected in (
|
||||
"Get-PreservedDecodeCountProof",
|
||||
"Get-RequiredDecodeInvalidCountProof",
|
||||
"Get-CurrentDecodeInvalidCountProof",
|
||||
"$offlinePreservedDecodeCounts.ValidCount",
|
||||
"$manifestPreservedDecodeCounts.ValidCount",
|
||||
"$offlineRequiredDecodeCounts.ObjectCount",
|
||||
"$manifestRequiredDecodeCounts.ObjectCount",
|
||||
"$manifestCurrentDecodeCounts.ObjectCount",
|
||||
"preserved_inventory_sha256",
|
||||
"preserved_object_set_sha256",
|
||||
):
|
||||
with self.subTest(validation=expected):
|
||||
self.assertIn(expected, manifest_validation)
|
||||
|
||||
runtime_receipt = PUBLIC_RUNTIME[
|
||||
PUBLIC_RUNTIME.index("$provenance = [ordered]@{") :
|
||||
PUBLIC_RUNTIME.index(
|
||||
'$freshFailureStage = "receipt_publish"',
|
||||
PUBLIC_RUNTIME.index("$provenance = [ordered]@{"),
|
||||
)
|
||||
]
|
||||
task_recovery_receipt = PUBLIC_RUNTIME[
|
||||
PUBLIC_RUNTIME.index("$taskRecoveryReceipt = [ordered]@{") :
|
||||
PUBLIC_RUNTIME.index(
|
||||
"Write-Utf8TextAtomically `",
|
||||
PUBLIC_RUNTIME.index("$taskRecoveryReceipt = [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 (
|
||||
("runtime", runtime_receipt),
|
||||
("task_recovery", task_recovery_receipt),
|
||||
):
|
||||
for field, source in receipt_fields.items():
|
||||
with self.subTest(receipt=receipt_name, field=field):
|
||||
self.assertIn(f"{field} = [int]${source}", receipt)
|
||||
|
||||
def test_process_start_and_command_hash_match_topology_psutil_algorithm(self) -> None:
|
||||
identity = PUBLIC_RUNTIME[
|
||||
PUBLIC_RUNTIME.index("function Wait-ProcessIdentity") :
|
||||
|
|
@ -678,13 +768,20 @@ try {{
|
|||
f"""$ErrorActionPreference = 'Stop'
|
||||
. '{quoted_functions}'
|
||||
$events = New-Object System.Collections.ArrayList
|
||||
$script:apiRestored = $false
|
||||
$root = '{quoted_root}'
|
||||
function Stop-UvicornByPort {{ param($AppImport,$Port,$TimeoutSec); $null=$events.Add('stop-api'); 11 }}
|
||||
function Get-ExactLoopbackListenerProcess {{ param($Port,$Role); [pscustomobject]@{{ProcessId=404}} }}
|
||||
function Get-VerifiedProcessFromIdentity {{
|
||||
param($Identity,$Role,$TimeoutSec)
|
||||
if ($null -eq $Identity) {{ return $null }}
|
||||
[pscustomobject]@{{ProcessId=[int]$Identity.pid}}
|
||||
}}
|
||||
function Get-ListenerProcessIds {{ param($Port); if ($script:apiRestored) {{ @(101) }} else {{ @() }} }}
|
||||
function Restore-ManagedEnvironment {{ param($Snapshot); $null=$events.Add('restore-env') }}
|
||||
function Start-PinnedPriorProcess {{
|
||||
param($Identity,$Role,$StdoutLog,$StderrLog)
|
||||
$null=$events.Add("start-$Role")
|
||||
if ($Role -eq 'api') {{ [pscustomobject]@{{Id=101}} }} else {{ [pscustomobject]@{{Id=202}} }}
|
||||
if ($Role -eq 'api') {{ $script:apiRestored=$true; [pscustomobject]@{{Id=101}} }} else {{ [pscustomobject]@{{Id=202}} }}
|
||||
}}
|
||||
function Wait-ProcessIdentity {{
|
||||
param($ProcessId,$Role,$ExpectedCwd,$TimeoutSec)
|
||||
|
|
@ -712,8 +809,11 @@ function Wait-JsonHealth {{
|
|||
$payload
|
||||
}}
|
||||
function Test-VoiceSidecarReady {{ param($Component); $true }}
|
||||
function Get-CloudflaredProcessesForConfig {{ param($ConfigPath,[switch]$ExactPath); @([pscustomobject]@{{ProcessId=303}}) }}
|
||||
function Stop-ProcessesBounded {{ param($Processes,$TimeoutSec,$Role); $null=$events.Add('stop-cloudflared'); 303 }}
|
||||
function Get-CloudflaredProcessesForConfig {{ param($ConfigPath,[switch]$ExactPath); @() }}
|
||||
function Stop-ProcessesBounded {{
|
||||
param($Processes,$TimeoutSec,$Role)
|
||||
if ($Role -like '*API*') {{ $null=$events.Add('stop-api'); 404 }} else {{ $null=$events.Add('stop-cloudflared'); 303 }}
|
||||
}}
|
||||
function ConvertTo-SafeProcessIdentity {{ param($Identity); $Identity }}
|
||||
|
||||
$configPath = Join-Path $root 'cloudflared.yml'
|
||||
|
|
@ -723,11 +823,15 @@ $priorVoice = [ordered]@{{
|
|||
stt_provider='openai';stt_model='gpt-4o-mini-transcribe';
|
||||
tts_provider='openai';tts_model='gpt-4o-mini-tts';uvicorn_ws_max_queue=4
|
||||
}}
|
||||
$priorApi=[ordered]@{{executable_sha256=('a' * 64);command_line_sha256=('b' * 64);cwd=$root}}
|
||||
$priorCloud=[ordered]@{{executable_sha256=('a' * 64);command_line_sha256=('b' * 64);cwd=$root}}
|
||||
$priorApi=[ordered]@{{pid=11;started_at_utc='2026-08-09T00:00:00Z';executable_sha256=('a' * 64);command_line_sha256=('b' * 64);cwd=$root}}
|
||||
$priorCloud=[ordered]@{{pid=22;started_at_utc='2026-08-09T00:00:00Z';executable_sha256=('a' * 64);command_line_sha256=('b' * 64);cwd=$root}}
|
||||
$replacementApi=[ordered]@{{pid=404;started_at_utc='2026-08-09T00:00:00Z';executable_sha256=('a' * 64);command_line_sha256=('b' * 64);cwd=$root}}
|
||||
$replacementCloud=[ordered]@{{pid=303;started_at_utc='2026-08-09T00:00:00Z';executable_sha256=('a' * 64);command_line_sha256=('b' * 64);cwd=$root}}
|
||||
$result = Restore-PriorPublicRuntime `
|
||||
-PriorApi $priorApi `
|
||||
-PriorCloudflared $priorCloud `
|
||||
-ReplacementApi $replacementApi `
|
||||
-ReplacementCloudflared $replacementCloud `
|
||||
-PriorLocalVoiceContract $priorVoice `
|
||||
-PriorPublicVoiceContract $priorVoice `
|
||||
-EnvironmentSnapshot @{{}} `
|
||||
|
|
@ -821,11 +925,11 @@ if (Test-VoiceHealthContract -Health $changed -Expected $priorVoice) {{ throw 'v
|
|||
self.assertEqual(status.stdout, b"")
|
||||
|
||||
def test_fresh_cutover_requires_and_restores_a_pinned_prior_runtime(self) -> None:
|
||||
prior_capture = PUBLIC_RUNTIME.index("$priorApiProcesses = @(")
|
||||
prior_capture = PUBLIC_RUNTIME.index("$priorApiListenerProof =")
|
||||
mutation = PUBLIC_RUNTIME.index('$freshFailureStage = "api_cutover"')
|
||||
self.assertLess(prior_capture, mutation)
|
||||
for expected in (
|
||||
"requires exactly one prior API process for transactional rollback",
|
||||
"requires one exact prior API listener identity",
|
||||
"requires exactly one prior cloudflared process for transactional rollback",
|
||||
"$freshPriorApiIdentity = Wait-ProcessIdentity",
|
||||
"$freshPriorCloudflaredIdentity = Wait-ProcessIdentity",
|
||||
|
|
@ -863,13 +967,36 @@ if (Test-VoiceHealthContract -Health $changed -Expected $priorVoice) {{ throw 'v
|
|||
stt_start = PUBLIC_RUNTIME.index("& $WhisperStartScript `")
|
||||
tts_start = PUBLIC_RUNTIME.index("& $MeloTtsStartScript `")
|
||||
self.assertLess(
|
||||
PUBLIC_RUNTIME.index("Fresh public promotion will not mutate local_whisper"),
|
||||
PUBLIC_RUNTIME.index("Public upload-root promotion will not mutate local_whisper"),
|
||||
stt_start,
|
||||
)
|
||||
self.assertLess(
|
||||
PUBLIC_RUNTIME.index("Fresh public promotion will not mutate MeloTTS"),
|
||||
PUBLIC_RUNTIME.index("Public upload-root promotion will not mutate MeloTTS"),
|
||||
tts_start,
|
||||
)
|
||||
self.assertGreaterEqual(
|
||||
PUBLIC_RUNTIME.count("$RequireFreshPublicProvenance -or $offlineBootstrapMode"),
|
||||
2,
|
||||
)
|
||||
|
||||
def test_offline_bootstrap_never_stops_a_reappeared_api_listener(self) -> None:
|
||||
listener_probe = PUBLIC_RUNTIME.index(
|
||||
'$apiListenerProcess = Get-ExactLoopbackListenerProcess `'
|
||||
)
|
||||
offline_abort = PUBLIC_RUNTIME.index(
|
||||
"if ($offlineBootstrapMode -and $null -ne $apiListenerProcess)",
|
||||
listener_probe,
|
||||
)
|
||||
exact_stop = PUBLIC_RUNTIME.index(
|
||||
"Stop-ProcessesBounded `",
|
||||
offline_abort,
|
||||
)
|
||||
self.assertLess(listener_probe, offline_abort)
|
||||
self.assertLess(offline_abort, exact_stop)
|
||||
self.assertIn(
|
||||
"Offline bootstrap API listener reappeared after the quiescence receipt",
|
||||
PUBLIC_RUNTIME[offline_abort:exact_stop],
|
||||
)
|
||||
|
||||
def test_success_and_rollback_require_canonical_public_engine_and_voice(self) -> None:
|
||||
rollback = PUBLIC_RUNTIME[
|
||||
|
|
@ -894,6 +1021,42 @@ if (Test-VoiceHealthContract -Health $changed -Expected $priorVoice) {{ throw 'v
|
|||
self.assertIn("$health.engine -eq $true", success_section)
|
||||
self.assertIn("Test-VoiceSidecarReady", success_section)
|
||||
|
||||
def test_fresh_tunnel_identity_is_unique_at_release_and_rollback_closes_ingress_first(
|
||||
self,
|
||||
) -> None:
|
||||
rollback = PUBLIC_RUNTIME[
|
||||
PUBLIC_RUNTIME.index("function Restore-PriorPublicRuntime") :
|
||||
PUBLIC_RUNTIME.index("function Stop-NodeByPortHint")
|
||||
]
|
||||
tunnel_stop = rollback.index("$verifiedReplacementCloudflared =")
|
||||
api_listener = rollback.index("$replacementApiListener =")
|
||||
self.assertLess(tunnel_stop, api_listener)
|
||||
|
||||
provenance_json = PUBLIC_RUNTIME.index(
|
||||
"$provenanceJson = ConvertTo-Json -InputObject $provenance"
|
||||
)
|
||||
boundary_tunnel = PUBLIC_RUNTIME.index(
|
||||
"$boundaryTunnelOwners = @(", provenance_json
|
||||
)
|
||||
no_rollback = PUBLIC_RUNTIME.index("$freshNoRollback = $true", boundary_tunnel)
|
||||
release = PUBLIC_RUNTIME.index(
|
||||
"Exit-PublicUploadWriteFreeze `", no_rollback
|
||||
)
|
||||
public_unfrozen = PUBLIC_RUNTIME.index(
|
||||
"-Uri $CanonicalPublicHealthUrl `", release
|
||||
)
|
||||
receipt_stage = PUBLIC_RUNTIME.index(
|
||||
'$freshFailureStage = "receipt_publish"', public_unfrozen
|
||||
)
|
||||
self.assertLess(provenance_json, boundary_tunnel)
|
||||
self.assertLess(boundary_tunnel, no_rollback)
|
||||
self.assertLess(no_rollback, release)
|
||||
self.assertLess(release, public_unfrozen)
|
||||
self.assertLess(public_unfrozen, receipt_stage)
|
||||
self.assertIn("$freeze.active -eq $false", PUBLIC_RUNTIME[public_unfrozen:receipt_stage])
|
||||
self.assertIn("$freeze.valid -eq $true", PUBLIC_RUNTIME[public_unfrozen:receipt_stage])
|
||||
self.assertIn("[int]$freeze.in_flight -eq 0", PUBLIC_RUNTIME[public_unfrozen:receipt_stage])
|
||||
|
||||
def test_failed_cutover_emits_metadata_only_rollback_evidence(self) -> None:
|
||||
failure_writer = PUBLIC_RUNTIME[
|
||||
PUBLIC_RUNTIME.index("function Write-FailedFreshPromotionEvidence") :
|
||||
|
|
@ -904,7 +1067,7 @@ if (Test-VoiceHealthContract -Health $changed -Expected $priorVoice) {{ throw 'v
|
|||
'"failed_rolled_back"',
|
||||
'"failed_rollback"',
|
||||
"failure_stage = $FailureStage",
|
||||
"attempted = $true",
|
||||
"attempted = $RollbackAttempted",
|
||||
"succeeded = $RollbackSucceeded",
|
||||
"Write-Utf8TextAtomically",
|
||||
):
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue