vignette/scripts/test_start_public_runtime_contract.py
2026-08-29 23:58:33 +09:00

1107 lines
50 KiB
Python

from __future__ import annotations
import unittest
import shutil
import subprocess
import sys
import tempfile
from pathlib import Path
SCRIPTS = Path(__file__).resolve().parent
PUBLIC_RUNTIME = (SCRIPTS / "start-public-runtime.ps1").read_text(encoding="utf-8")
WHISPER_START = (SCRIPTS / "start-local-whisper-stt.ps1").read_text(
encoding="utf-8"
)
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)
first_runtime_mutation = PUBLIC_RUNTIME.index("Stop-UvicornByPort", main_try)
finalizer = PUBLIC_RUNTIME.rindex("} finally {")
dispose = PUBLIC_RUNTIME.index("$recoveryLock.Dispose()", finalizer)
self.assertLess(lock, main_try)
self.assertLess(main_try, first_runtime_mutation)
self.assertLess(finalizer, dispose)
self.assertIn(
'"$env:LOCALAPPDATA\\Vignette\\public-runtime-start.lock"',
PUBLIC_RUNTIME,
)
def test_recovery_lock_is_exclusive_and_reusable_in_windows_powershell(self) -> None:
powershell = shutil.which("powershell.exe")
if powershell is None:
self.skipTest("Windows PowerShell 5.1 is not available")
lock_function = PUBLIC_RUNTIME[
PUBLIC_RUNTIME.index("function Enter-RecoveryLock") :
PUBLIC_RUNTIME.index(
"# boot task, watchdog, 수동 승격이 같은 포트와 프로세스를 동시에 교체하지 못하게 한다."
)
].strip()
with tempfile.TemporaryDirectory() as temporary_directory:
lock_path = str(Path(temporary_directory) / "runtime.lock").replace("'", "''")
harness = Path(temporary_directory) / "recovery-lock.ps1"
harness.write_text(
lock_function
+ f"""
$first = Enter-RecoveryLock -LockPath '{lock_path}' -WaitSeconds 0
try {{
try {{
$second = Enter-RecoveryLock -LockPath '{lock_path}' -WaitSeconds 0
exit 2
}} catch {{
if ($_.Exception.Message -notlike '*already in progress*') {{ exit 3 }}
}}
}} finally {{
$first.Dispose()
}}
$third = Enter-RecoveryLock -LockPath '{lock_path}' -WaitSeconds 0
$third.Dispose()
exit 0
""",
encoding="utf-8-sig",
)
completed = subprocess.run(
[
powershell,
"-NoLogo",
"-NoProfile",
"-NonInteractive",
"-ExecutionPolicy",
"Bypass",
"-File",
str(harness),
],
capture_output=True,
text=True,
encoding="utf-8",
errors="replace",
timeout=30,
check=False,
)
self.assertEqual(
completed.returncode,
0,
msg=f"stdout={completed.stdout}\nstderr={completed.stderr}",
)
def test_cold_start_waits_are_bounded_and_configurable(self) -> None:
for expected in (
"[int]$ApiReadySeconds = 180",
"[int]$VoiceApiReadySeconds = 90",
"[int]$WebBuildTimeoutSeconds = 600",
"-TimeoutSec $ApiReadySeconds",
"-TimeoutSec $VoiceApiReadySeconds",
"$build.WaitForExit($WebBuildTimeoutSeconds * 1000)",
"Web build timed out after $WebBuildTimeoutSeconds seconds",
):
with self.subTest(expected=expected):
self.assertIn(expected, PUBLIC_RUNTIME)
def test_web_build_keeps_process_handle_before_timed_wait(self) -> None:
build_start = PUBLIC_RUNTIME.index(
'$build = Start-Process -FilePath "cmd.exe"'
)
handle = PUBLIC_RUNTIME.index("$null = $build.Handle", build_start)
timed_wait = PUBLIC_RUNTIME.index(
"$build.WaitForExit($WebBuildTimeoutSeconds * 1000)", handle
)
null_guard = PUBLIC_RUNTIME.index(
"if ($null -eq $build.ExitCode)", timed_wait
)
failure_check = PUBLIC_RUNTIME.index(
"if ($build.ExitCode -ne 0)", null_guard
)
self.assertLess(build_start, handle)
self.assertLess(handle, timed_wait)
self.assertLess(timed_wait, null_guard)
self.assertLess(null_guard, failure_check)
def test_web_build_exit_code_is_available_in_windows_powershell(self) -> None:
powershell = shutil.which("powershell.exe")
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 `
-PassThru
$null = $process.Handle
if (-not $process.WaitForExit(10000)) { exit 2 }
$process.Refresh()
if ($null -eq $process.ExitCode) { exit 3 }
exit $process.ExitCode
""",
encoding="utf-8-sig",
)
completed = subprocess.run(
[
powershell,
"-NoLogo",
"-NoProfile",
"-NonInteractive",
"-ExecutionPolicy",
"Bypass",
"-File",
str(harness),
],
capture_output=True,
text=True,
encoding="utf-8",
errors="replace",
timeout=30,
check=False,
)
self.assertEqual(
completed.returncode,
0,
msg=f"stdout={completed.stdout}\nstderr={completed.stderr}",
)
def test_web_build_timeout_stops_the_owned_process_tree(self) -> None:
powershell = shutil.which("powershell.exe")
if powershell is None:
self.skipTest("Windows PowerShell 5.1 is not available")
tree_functions = PUBLIC_RUNTIME[
PUBLIC_RUNTIME.index("function Stop-ProcessTreeBounded") :
PUBLIC_RUNTIME.index("function Get-UvicornProcessesByPort")
].strip()
with tempfile.TemporaryDirectory() as temporary_directory:
harness = Path(temporary_directory) / "process-tree.ps1"
child = Path(temporary_directory) / "child-sleeper.ps1"
child_pid = Path(temporary_directory) / "child.pid"
quoted_child_pid = str(child_pid).replace("'", "''")
child.write_text(
f"$PID | Set-Content -LiteralPath '{quoted_child_pid}' -Encoding ascii\n"
"Start-Sleep -Seconds 300\n",
encoding="utf-8-sig",
)
quoted_child = str(child).replace("'", "''")
quoted_child_pid_for_harness = str(child_pid).replace("'", "''")
harness.write_text(
tree_functions
+ f'''
$root = Start-Process -FilePath 'cmd.exe' `
-ArgumentList @('/c', 'powershell.exe -NoProfile -ExecutionPolicy Bypass -File "{quoted_child}"') `
-WindowStyle Hidden `
-PassThru
$childProcessId = $null
try {{
$deadline = (Get-Date).AddSeconds(10)
while (-not (Test-Path -LiteralPath '{quoted_child_pid_for_harness}') -and (Get-Date) -lt $deadline) {{
Start-Sleep -Milliseconds 100
}}
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 -RootProcess $root -TimeoutSec 10 -Role 'test tree')
$exitDeadline = (Get-Date).AddSeconds(10)
do {{
$rootAlive = $null -ne (Get-Process -Id $root.Id -ErrorAction SilentlyContinue)
$childAlive = $null -ne (Get-Process -Id $childProcessId -ErrorAction SilentlyContinue)
if (-not $rootAlive -and -not $childAlive) {{ break }}
Start-Sleep -Milliseconds 200
}} while ((Get-Date) -lt $exitDeadline)
if ($rootAlive) {{ exit 3 }}
if ($childAlive) {{ exit 4 }}
exit 0
}} finally {{
Stop-Process -Id $root.Id -Force -ErrorAction SilentlyContinue
if ($null -ne $childProcessId) {{
Stop-Process -Id $childProcessId -Force -ErrorAction SilentlyContinue
}}
}}
''',
encoding="utf-8-sig",
)
completed = subprocess.run(
[
powershell,
"-NoLogo",
"-NoProfile",
"-NonInteractive",
"-ExecutionPolicy",
"Bypass",
"-File",
str(harness),
],
capture_output=True,
text=True,
encoding="utf-8",
errors="replace",
timeout=30,
check=False,
)
self.assertEqual(
completed.returncode,
0,
msg=f"stdout={completed.stdout}\nstderr={completed.stderr}",
)
def test_public_provider_model_and_loopback_environment_are_explicit(self) -> None:
for expected in (
'$WhisperModel = "small"',
'$WhisperDevice = "cpu"',
'$MeloTtsModel = "melotts-korean"',
'$env:VIGNETTE_VOICE_STT_PROVIDER = "local_whisper"',
'$env:VIGNETTE_LOCAL_WHISPER_STT_MODEL = $WhisperModel',
'$env:VIGNETTE_VOICE_TTS_PROVIDER = "melotts"',
'"ws://127.0.0.1:$WhisperPort/v1/listen"',
'"http://127.0.0.1:$MeloTtsPort"',
):
with self.subTest(expected=expected):
self.assertIn(expected, PUBLIC_RUNTIME)
def test_existing_listeners_require_exact_protocol_metadata(self) -> None:
self.assertIn('Test-VoiceSidecarReady -Component "stt"', PUBLIC_RUNTIME)
self.assertIn('Test-VoiceSidecarReady -Component "tts"', PUBLIC_RUNTIME)
self.assertIn("if (Test-PortListener -Port $WhisperPort)", PUBLIC_RUNTIME)
self.assertIn("if (Test-PortListener -Port $MeloTtsPort)", PUBLIC_RUNTIME)
self.assertIn("does not expose the exact local_whisper", PUBLIC_RUNTIME)
self.assertIn("does not expose the exact melotts", PUBLIC_RUNTIME)
def test_sidecar_fail_closed_gate_precedes_api_mutation(self) -> None:
sidecar_gate = PUBLIC_RUNTIME.index(
'# 두 sidecar를 한 번 더 함께 검사해 개별 probe 사이의 TOCTOU를 닫는다.'
)
api_stop = PUBLIC_RUNTIME.index("$apiStoppedProcessIds = @(")
self.assertLess(sidecar_gate, api_stop)
self.assertLess(
PUBLIC_RUNTIME.index("Voice sidecar readiness changed"),
api_stop,
)
def test_api_skip_and_post_start_checks_require_exact_voice_health(self) -> None:
for expected in (
'$Health.stt_provider -eq "local_whisper"',
"$Health.stt_model -eq $WhisperModel",
'$Health.tts_provider -eq "melotts"',
"$Health.tts_model -eq $MeloTtsModel",
"$Health.limits.uvicorn_ws_max_queue -eq 4",
"$health.engine -eq $true",
):
with self.subTest(expected=expected):
self.assertIn(expected, PUBLIC_RUNTIME)
self.assertGreaterEqual(
PUBLIC_RUNTIME.count("Test-VoiceApiReady -Health"),
3,
)
def test_api_uvicorn_websocket_queue_is_pinned(self) -> None:
start = PUBLIC_RUNTIME.index("$apiStoppedProcessIds = @(")
end = PUBLIC_RUNTIME.index("Start-Sleep -Seconds 3", start)
api_section = PUBLIC_RUNTIME[start:end]
self.assertIn('"app.main:app"', api_section)
self.assertIn('"--ws", "websockets"', api_section)
self.assertIn('"--ws-max-queue", "4"', api_section)
def test_standalone_whisper_launcher_matches_public_cpu_default(self) -> None:
self.assertIn("[string]$Model = 'small'", WHISPER_START)
self.assertIn("[string]$Device = 'cpu'", WHISPER_START)
class FreshPublicProvenanceContractTest(unittest.TestCase):
def test_fresh_mode_is_explicit_and_fail_closed(self) -> None:
for expected in (
"[switch]$RequireFreshPublicProvenance",
'throw "-RequireFreshPublicProvenance requires -ForceApiRestart"',
'throw "-RequireFreshPublicProvenance forbids -SkipCloudflaredRestart"',
'throw "-RequireFreshPublicProvenance requires -SkipEngineRestart;',
'throw "-RequireFreshPublicProvenance requires -SkipWebRestart;',
'throw "-RequireFreshPublicProvenance forbids DNS route mutation"',
"Fresh public promotion requires the canonical HTTPS public health URL",
"ExpectedSourceCommit",
"ExpectedSourceTree",
"ExpectedPythonSha256",
"ExpectedCloudflaredSha256",
"ExpectedCloudflaredConfigSha256",
):
with self.subTest(expected=expected):
self.assertIn(expected, PUBLIC_RUNTIME)
def test_fresh_gate_precedes_every_runtime_mutation(self) -> None:
gate = PUBLIC_RUNTIME.index("Assert-FreshPublicProvenanceContract `")
receipt_preflight = PUBLIC_RUNTIME.index(
"$resolvedRuntimeProvenancePath = Initialize-RuntimeProvenanceOutput"
)
exact_ingress = PUBLIC_RUNTIME.index("-RequireUnchanged", gate)
engine_probe = PUBLIC_RUNTIME.index("$engineHealth = Get-JsonHealth")
self.assertLess(gate, receipt_preflight)
self.assertLess(receipt_preflight, exact_ingress)
self.assertLess(gate, exact_ingress)
self.assertLess(exact_ingress, engine_probe)
self.assertIn("requires detached HEAD", PUBLIC_RUNTIME)
self.assertIn("requires a clean stable source", PUBLIC_RUNTIME)
def test_receipt_preflight_proves_sibling_atomic_replace_capability(self) -> None:
preflight = PUBLIC_RUNTIME[
PUBLIC_RUNTIME.index("function Initialize-RuntimeProvenanceOutput") :
PUBLIC_RUNTIME.index("function Write-Utf8TextAtomically")
]
for expected in (
"output preflight failed before runtime mutation",
"[System.IO.FileMode]::Open",
"[System.IO.FileAccess]::ReadWrite",
"[System.IO.File]::Replace($probeSource, $probeTarget, $probeBackup, $true)",
"[System.IO.File]::Delete($probeTarget)",
):
with self.subTest(expected=expected):
self.assertIn(expected, preflight)
def test_receipt_commit_is_atomic_and_failure_is_explicit(self) -> None:
writer = PUBLIC_RUNTIME[
PUBLIC_RUNTIME.index("function Write-Utf8TextAtomically") :
PUBLIC_RUNTIME.index("function Assert-FreshPublicProvenanceContract")
]
for expected in (
"[System.IO.FileMode]::CreateNew",
"$stream.Flush($true)",
"[System.IO.File]::Replace($temporaryPath, $OutputPath, $backupPath, $true)",
"[System.IO.File]::Move($temporaryPath, $OutputPath)",
"[System.IO.File]::Delete($temporaryPath)",
):
with self.subTest(expected=expected):
self.assertIn(expected, writer)
receipt = PUBLIC_RUNTIME[PUBLIC_RUNTIME.index("$provenance = [ordered]@{") :]
self.assertIn("Write-Utf8TextAtomically `", receipt)
self.assertIn("no atomic passed receipt was published", receipt)
self.assertNotIn(
"[System.IO.File]::WriteAllText(\n $resolvedRuntimeProvenancePath",
receipt,
)
def test_receipt_helpers_work_in_windows_powershell(self) -> None:
powershell = shutil.which("powershell.exe")
if powershell is None:
self.skipTest("Windows PowerShell 5.1 is not available")
function_source = PUBLIC_RUNTIME[
PUBLIC_RUNTIME.index("function Initialize-RuntimeProvenanceOutput") :
PUBLIC_RUNTIME.index("function Assert-FreshPublicProvenanceContract")
]
with tempfile.TemporaryDirectory() as temporary_directory:
root = Path(temporary_directory)
functions_path = root / "receipt-functions.ps1"
harness_path = root / "receipt-harness.ps1"
functions_path.write_text(function_source, encoding="utf-8-sig")
quoted_functions = str(functions_path).replace("'", "''")
quoted_root = str(root).replace("'", "''")
harness_path.write_text(
f"""$ErrorActionPreference = 'Stop'
. '{quoted_functions}'
$target = Join-Path '{quoted_root}' 'nested\\receipt.json'
$resolved = Initialize-RuntimeProvenanceOutput -OutputPath $target
Write-Utf8TextAtomically -OutputPath $resolved -Value '{{"status":"passed"}}'
$receipt = Get-Content -LiteralPath $resolved -Raw -Encoding UTF8 | ConvertFrom-Json
if ($receipt.status -ne 'passed') {{ throw 'atomic receipt content mismatch' }}
Write-Utf8TextAtomically -OutputPath $resolved -Value '{{"status":"replaced"}}'
$replacement = Get-Content -LiteralPath $resolved -Raw -Encoding UTF8 | ConvertFrom-Json
if ($replacement.status -ne 'replaced') {{ throw 'atomic replacement mismatch' }}
$failedPath = Write-FailedFreshPromotionEvidence `
-OutputPath $resolved `
-FailureStage 'receipt_publish' `
-RollbackSucceeded $true `
-RollbackResult @{{local_health=$true;public_health=$true}} `
-SourceCommit ('a' * 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'
}}
[System.IO.File]::SetAttributes($resolved, [System.IO.FileAttributes]::ReadOnly)
$preflightFailed = $false
try {{
Initialize-RuntimeProvenanceOutput -OutputPath $resolved | Out-Null
}} catch {{
$preflightFailed = $true
}} finally {{
[System.IO.File]::SetAttributes($resolved, [System.IO.FileAttributes]::Normal)
}}
if (-not $preflightFailed) {{ throw 'read-only receipt preflight did not fail' }}
$preserved = Get-Content -LiteralPath $resolved -Raw -Encoding UTF8 | ConvertFrom-Json
if ($preserved.status -ne 'replaced') {{ throw 'failed preflight changed the prior receipt' }}
""",
encoding="utf-8-sig",
)
completed = subprocess.run(
[
powershell,
"-NoLogo",
"-NoProfile",
"-NonInteractive",
"-ExecutionPolicy",
"Bypass",
"-File",
str(harness_path),
],
capture_output=True,
text=True,
encoding="utf-8",
errors="replace",
timeout=30,
check=False,
)
self.assertEqual(
completed.returncode,
0,
msg=f"stdout={completed.stdout}\nstderr={completed.stderr}",
)
def test_force_api_restart_keeps_regular_healthy_semantics(self) -> None:
self.assertIn(
"if ($apiControlPlaneReady -and -not $ForceApiRestart)",
PUBLIC_RUNTIME,
)
fresh_guard = PUBLIC_RUNTIME.index(
'throw "-RequireFreshPublicProvenance requires -ForceApiRestart"'
)
api_skip = PUBLIC_RUNTIME.index(
"if ($apiControlPlaneReady -and -not $ForceApiRestart)"
)
self.assertLess(fresh_guard, api_skip)
def test_api_and_tunnel_are_bounded_replacements_with_exact_cwds(self) -> None:
api = PUBLIC_RUNTIME.index("$apiStoppedProcessIds = @(")
cloud = PUBLIC_RUNTIME.index("$cloudflaredStoppedProcessIds = @(")
receipt = PUBLIC_RUNTIME.index("$provenance = [ordered]@{")
api_section = PUBLIC_RUNTIME[api:cloud]
cloud_section = PUBLIC_RUNTIME[cloud:receipt]
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)
self.assertIn("Stop-ProcessesBounded `", cloud_section)
self.assertIn("-TimeoutSec $ProcessStopTimeoutSeconds", cloud_section)
self.assertIn("-WorkingDirectory $Workspace", cloud_section)
self.assertIn("Cloudflared did not receive a replacement PID", cloud_section)
self.assertNotIn("Cloudflared already running; skipping", PUBLIC_RUNTIME)
def test_tunnel_command_and_config_are_pinned_before_receipt(self) -> None:
for expected in (
'"tunnel", "--config", $resolvedCloudflaredConfig, "run"',
"command line is not pinned to the expected config",
"Pinned cloudflared config drifted before provenance receipt",
"finalConfigSha256",
):
with self.subTest(expected=expected):
self.assertIn(expected, PUBLIC_RUNTIME)
def test_fresh_tunnel_stop_matches_only_the_resolved_full_config_path(self) -> None:
cloud = PUBLIC_RUNTIME.index("$resolvedCloudflaredConfig =")
start = PUBLIC_RUNTIME.index(
"$cloudflaredProcess = Start-Process", cloud
)
cloud_section = PUBLIC_RUNTIME[cloud:start]
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")
]
self.assertIn("-not $ExactPath", matcher)
self.assertIn("IndexOf($ConfigPath", matcher)
def test_receipt_projects_no_raw_command_or_config_contents(self) -> None:
projection = PUBLIC_RUNTIME[
PUBLIC_RUNTIME.index("function ConvertTo-SafeProcessIdentity") :
PUBLIC_RUNTIME.index("function Save-ManagedEnvironment")
]
self.assertIn("command_line_sha256 =", projection)
self.assertNotIn("command_line =", projection)
self.assertNotIn("executable_path =", projection)
receipt = PUBLIC_RUNTIME.index("$provenance = [ordered]@{")
receipt_section = PUBLIC_RUNTIME[receipt:]
self.assertIn("api = $safeApiIdentity", receipt_section)
self.assertIn("cloudflared = $safeCloudflaredIdentity", receipt_section)
self.assertNotIn("api = $apiFinalIdentity", receipt_section)
self.assertNotIn("cloudflared = $cloudflaredFinalIdentity", receipt_section)
config_projection = receipt_section[
receipt_section.index("config = [ordered]@{") :
receipt_section.index("replacement = [ordered]@{")
].lower()
self.assertNotIn("token", config_projection)
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") :
PUBLIC_RUNTIME.index("function ConvertTo-SafeProcessIdentity")
]
self.assertIn("datetime.fromtimestamp(p.create_time(), UTC)", identity)
self.assertIn("argv=p.cmdline()", identity)
self.assertIn("chr(0).join(argv)", identity)
self.assertIn("argument_list = $argumentList", identity)
self.assertIn("environment = $processEnvironment", identity)
self.assertIn("command_line_sha256 = $commandLineSha256", identity)
def test_prior_process_argv_and_environment_round_trip_in_powershell_51(self) -> None:
powershell = shutil.which("powershell.exe")
if powershell is None:
self.skipTest("Windows PowerShell 5.1 is not available")
function_source = PUBLIC_RUNTIME[
PUBLIC_RUNTIME.index("function Save-CompleteProcessEnvironment") :
PUBLIC_RUNTIME.index("function Restore-PriorPublicRuntime")
]
with tempfile.TemporaryDirectory() as temporary_directory:
root = Path(temporary_directory)
functions_path = root / "rollback-process-functions.ps1"
harness_path = root / "rollback-process-harness.ps1"
functions_path.write_text(function_source, encoding="utf-8-sig")
quoted_functions = str(functions_path).replace("'", "''")
quoted_root = str(root).replace("'", "''")
quoted_python = sys.executable.replace("'", "''")
harness_path.write_text(
f"""$ErrorActionPreference = 'Stop'
. '{quoted_functions}'
$root = '{quoted_root}'
$python = '{quoted_python}'
if ($null -eq (Get-Command Get-FileHash -ErrorAction SilentlyContinue)) {{
function Get-FileHash {{
param([string]$LiteralPath, [string]$Algorithm)
$stream = [IO.File]::OpenRead($LiteralPath)
try {{
$hasher = [Security.Cryptography.SHA256]::Create()
try {{ $hash = $hasher.ComputeHash($stream) }} finally {{ $hasher.Dispose() }}
}} finally {{
$stream.Dispose()
}}
[pscustomobject]@{{Hash = ([BitConverter]::ToString($hash)).Replace('-', '')}}
}}
}}
$pythonSha256 = (Get-FileHash -LiteralPath $python -Algorithm SHA256).Hash.ToLowerInvariant()
$originalEnvironment = Save-CompleteProcessEnvironment
try {{
[Environment]::SetEnvironmentVariable('VIGNETTE_ROLLBACK_ARGV_TEST', 'caller', 'Process')
[Environment]::SetEnvironmentVariable('VIGNETTE_CALLER_ONLY', 'caller-only', 'Process')
$callerEnvironment = Save-CompleteProcessEnvironment
[Environment]::SetEnvironmentVariable('VIGNETTE_ROLLBACK_ARGV_TEST', 'prior', 'Process')
[Environment]::SetEnvironmentVariable('VIGNETTE_CALLER_ONLY', $null, 'Process')
$priorEnvironment = Save-CompleteProcessEnvironment
Set-CompleteProcessEnvironment -Environment $callerEnvironment
$output = Join-Path $root 'argv-result.json'
$code = "import json,os,sys; open(sys.argv[1], 'w', encoding='utf-8').write(json.dumps({{'argv':sys.argv[2:],'marker':os.environ.get('VIGNETTE_ROLLBACK_ARGV_TEST'),'leak':os.environ.get('VIGNETTE_CALLER_ONLY')}}, ensure_ascii=True))"
$expected = @('plain', 'space value', 'quote"value', 'trailing\', '', 'slashes\\before"quote')
$identity = [ordered]@{{
executable_path = $python
executable_sha256 = $pythonSha256
cwd = $root
argument_list = @('-X', 'utf8', '-c', $code, $output) + $expected
environment = $priorEnvironment
}}
$process = Start-PinnedPriorProcess `
-Identity $identity `
-Role 'argv-roundtrip' `
-StdoutLog (Join-Path $root 'child.out.log') `
-StderrLog (Join-Path $root 'child.err.log')
$process.WaitForExit()
if (-not (Test-Path -LiteralPath $output -PathType Leaf)) {{ throw 'child did not write argv result' }}
$actual = Get-Content -LiteralPath $output -Raw -Encoding UTF8 | ConvertFrom-Json
if (@($actual.argv).Count -ne $expected.Count) {{ throw 'argument count mismatch' }}
for ($i = 0; $i -lt $expected.Count; $i++) {{
if ($actual.argv[$i] -cne $expected[$i]) {{ throw "argument mismatch at $i" }}
}}
if ($actual.marker -cne 'prior') {{ throw 'prior environment was not inherited' }}
if ($null -ne $actual.leak) {{ throw 'caller-only environment leaked into prior process' }}
if ($env:VIGNETTE_ROLLBACK_ARGV_TEST -cne 'caller') {{ throw 'caller environment marker was not restored' }}
if ($env:VIGNETTE_CALLER_ONLY -cne 'caller-only') {{ throw 'caller-only environment was not restored' }}
}} finally {{
Set-CompleteProcessEnvironment -Environment $originalEnvironment
}}
""",
encoding="utf-8-sig",
)
completed = subprocess.run(
[
powershell,
"-NoLogo",
"-NoProfile",
"-NonInteractive",
"-ExecutionPolicy",
"Bypass",
"-File",
str(harness_path),
],
capture_output=True,
text=True,
encoding="utf-8",
errors="replace",
timeout=30,
check=False,
)
self.assertEqual(
completed.returncode,
0,
msg=f"stdout={completed.stdout}\nstderr={completed.stderr}",
)
def test_rollback_restores_prior_openai_voice_contract_in_powershell_51(self) -> None:
powershell = shutil.which("powershell.exe")
if powershell is None:
self.skipTest("Windows PowerShell 5.1 is not available")
voice_contract_source = PUBLIC_RUNTIME[
PUBLIC_RUNTIME.index("function Test-PriorVoiceApiReady") :
PUBLIC_RUNTIME.index("function Test-RequiredOpenApiPaths")
]
rollback_source = PUBLIC_RUNTIME[
PUBLIC_RUNTIME.index("function Restore-PriorPublicRuntime") :
PUBLIC_RUNTIME.index("function Stop-NodeByPortHint")
]
with tempfile.TemporaryDirectory() as temporary_directory:
root = Path(temporary_directory)
functions_path = root / "rollback-contract-functions.ps1"
harness_path = root / "rollback-contract-harness.ps1"
functions_path.write_text(
voice_contract_source + rollback_source,
encoding="utf-8-sig",
)
quoted_functions = str(functions_path).replace("'", "''")
quoted_root = str(root).replace("'", "''")
harness_path.write_text(
f"""$ErrorActionPreference = 'Stop'
. '{quoted_functions}'
$events = New-Object System.Collections.ArrayList
$script:apiRestored = $false
$root = '{quoted_root}'
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') {{ $script:apiRestored=$true; [pscustomobject]@{{Id=101}} }} else {{ [pscustomobject]@{{Id=202}} }}
}}
function Wait-ProcessIdentity {{
param($ProcessId,$Role,$ExpectedCwd,$TimeoutSec)
$null=$events.Add("identity-$Role")
[ordered]@{{
pid=[int]$ProcessId; started_at_utc='2026-08-09T00:00:00Z';
executable_name='runtime.exe'; executable_sha256=('a' * 64);
command_line_sha256=('b' * 64); cwd=$root
}}
}}
function Wait-JsonHealth {{
param($Uri,$IsHealthy,$TimeoutSec)
$null=$events.Add("health-$Uri")
if ($Uri -like '*/voice/health') {{
$payload=[pscustomobject]@{{
status='ok';available=$true;stt_available=$true;tts_available=$true;
stt_provider='openai';stt_model='gpt-4o-mini-transcribe';
tts_provider='openai';tts_model='gpt-4o-mini-tts';
limits=[pscustomobject]@{{uvicorn_ws_max_queue=4}}
}}
}} else {{
$payload=[pscustomobject]@{{environment='prod';db=$true;engine=$true}}
}}
if (-not (& $IsHealthy $payload)) {{ throw "health predicate failed: $Uri" }}
$payload
}}
function Test-VoiceSidecarReady {{ param($Component); $true }}
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'
[IO.File]::WriteAllText($configPath, 'tunnel: test')
$priorVoice = [ordered]@{{
status='ok';available=$true;stt_available=$true;tts_available=$true;
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]@{{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 @{{}} `
-ConfigPath $configPath `
-ApiPortValue 8001 `
-HealthUrl 'https://api-vignette.chanpaca.net/health' `
-VoiceHealthUrl 'https://api-vignette.chanpaca.net/voice/health' `
-TimeoutSec 5
foreach ($field in @('local_health','local_voice_health','public_health','public_voice_health')) {{
if (-not $result[$field]) {{ throw "rollback result missing $field" }}
}}
if ($events.IndexOf('stop-api') -ge $events.IndexOf('start-api')) {{ throw 'API restore ordering mismatch' }}
if ($events.IndexOf('stop-cloudflared') -ge $events.IndexOf('start-cloudflared')) {{ throw 'cloudflared restore ordering mismatch' }}
if ($events.IndexOf('start-api') -ge $events.IndexOf('health-http://127.0.0.1:8001/health')) {{ throw 'local health ordering mismatch' }}
if ($events.IndexOf('start-cloudflared') -ge $events.IndexOf('health-https://api-vignette.chanpaca.net/health')) {{ throw 'public health ordering mismatch' }}
$changed=[pscustomobject]@{{
status='ok';available=$true;stt_available=$true;tts_available=$true;
stt_provider='openai';stt_model='changed-model';tts_provider='openai';tts_model='gpt-4o-mini-tts';
limits=[pscustomobject]@{{uvicorn_ws_max_queue=4}}
}}
if (Test-VoiceHealthContract -Health $changed -Expected $priorVoice) {{ throw 'voice contract drift was accepted' }}
""",
encoding="utf-8-sig",
)
completed = subprocess.run(
[
powershell,
"-NoLogo",
"-NoProfile",
"-NonInteractive",
"-ExecutionPolicy",
"Bypass",
"-File",
str(harness_path),
],
capture_output=True,
text=True,
encoding="utf-8",
errors="replace",
timeout=30,
check=False,
)
self.assertEqual(
completed.returncode,
0,
msg=f"stdout={completed.stdout}\nstderr={completed.stderr}",
)
def test_engine_log_rotation_cannot_dirty_the_stable_release(self) -> None:
self.assertIn('Destination "$logFile.$rotateStamp.bak.log"', PUBLIC_RUNTIME)
self.assertNotIn('Destination "$logFile.$rotateStamp.bak"', PUBLIC_RUNTIME)
def test_failure_evidence_keeps_a_clean_detached_git_source(self) -> None:
self.assertIn('$failedPath = "$OutputPath.failed.log"', PUBLIC_RUNTIME)
self.assertNotIn('$failedPath = "$OutputPath.failed.json"', PUBLIC_RUNTIME)
with tempfile.TemporaryDirectory() as temporary_directory:
root = Path(temporary_directory)
(root / ".gitignore").write_text("*.log\n", encoding="utf-8")
(root / "tracked.txt").write_text("release\n", encoding="utf-8")
commands = (
["git", "init", "--quiet"],
["git", "config", "user.name", "Yun Chan"],
["git", "config", "user.email", "yun.chan@example.invalid"],
["git", "add", ".gitignore", "tracked.txt"],
["git", "commit", "--quiet", "-m", "테스트 기준"],
["git", "checkout", "--quiet", "--detach", "HEAD"],
)
for command in commands:
subprocess.run(
command,
cwd=root,
capture_output=True,
check=True,
)
failure_evidence = root / "public-runtime-launch-provenance.log.failed.log"
failure_evidence.write_text('{"status":"failed_rolled_back"}\n', encoding="utf-8")
ignored = subprocess.run(
["git", "check-ignore", "--quiet", failure_evidence.name],
cwd=root,
check=False,
)
self.assertEqual(ignored.returncode, 0)
status = subprocess.run(
["git", "status", "--porcelain", "--untracked-files=normal"],
cwd=root,
capture_output=True,
check=True,
)
self.assertEqual(status.stdout, b"")
def test_fresh_cutover_requires_and_restores_a_pinned_prior_runtime(self) -> None:
prior_capture = PUBLIC_RUNTIME.index("$priorApiListenerProof =")
mutation = PUBLIC_RUNTIME.index('$freshFailureStage = "api_cutover"')
self.assertLess(prior_capture, mutation)
for expected in (
"requires one exact prior API listener identity",
"requires exactly one prior cloudflared process for transactional rollback",
"$freshPriorApiIdentity = Wait-ProcessIdentity",
"$freshPriorCloudflaredIdentity = Wait-ProcessIdentity",
"$freshEnvironmentSnapshot = Save-ManagedEnvironment",
"Restore-PriorPublicRuntime `",
"Start-PinnedPriorProcess `",
"Restored prior API identity drift",
"Restored prior cloudflared identity drift",
"$freshPromotionCommitted = $true",
):
with self.subTest(expected=expected):
self.assertIn(expected, PUBLIC_RUNTIME)
def test_fresh_transaction_has_no_engine_web_sidecar_or_dns_mutation(self) -> None:
contract = PUBLIC_RUNTIME[
PUBLIC_RUNTIME.index("function Assert-FreshPublicProvenanceContract") :
PUBLIC_RUNTIME.index("$freshMutationStarted = $false")
]
for expected in (
"if (-not $SkipEngineRestart)",
"if (-not $SkipWebRestart)",
"if ($RouteCloudflareDns)",
"$CanonicalPublicHealthUrl",
):
with self.subTest(expected=expected):
self.assertIn(expected, contract)
api_mutation = PUBLIC_RUNTIME.index('$freshFailureStage = "api_cutover"')
preflight = PUBLIC_RUNTIME[:api_mutation]
self.assertIn("requires exact healthy voice sidecars as an unchanged precondition", preflight)
self.assertIn("-Uri $CanonicalPublicHealthUrl", preflight)
self.assertIn("-Uri $CanonicalPublicVoiceHealthUrl", preflight)
self.assertIn("$health.engine -eq $true", preflight)
stt_start = PUBLIC_RUNTIME.index("& $WhisperStartScript `")
tts_start = PUBLIC_RUNTIME.index("& $MeloTtsStartScript `")
self.assertLess(
PUBLIC_RUNTIME.index("Public upload-root promotion will not mutate local_whisper"),
stt_start,
)
self.assertLess(
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[
PUBLIC_RUNTIME.index("function Restore-PriorPublicRuntime") :
PUBLIC_RUNTIME.index("function Stop-NodeByPortHint")
]
self.assertGreaterEqual(rollback.count("$health.engine -eq $true"), 2)
self.assertIn("Test-VoiceSidecarReady", rollback)
self.assertIn("Test-VoiceHealthContract", rollback)
success_gate = PUBLIC_RUNTIME.index('$freshFailureStage = "public_health_validation"')
receipt = PUBLIC_RUNTIME.index("$provenance = [ordered]@{")
committed = PUBLIC_RUNTIME.index("$freshPromotionCommitted = $true")
self.assertLess(success_gate, receipt)
self.assertLess(receipt, committed)
success_section = PUBLIC_RUNTIME[success_gate:receipt]
self.assertIn("-Uri $CanonicalPublicHealthUrl", success_section)
self.assertIn("-Uri $CanonicalPublicVoiceHealthUrl", success_section)
self.assertIn("-Uri $CanonicalPublicOpenApiUrl", success_section)
self.assertIn("Test-RequiredOpenApiPaths", success_section)
self.assertIn('"/admin/voice-runtime"', PUBLIC_RUNTIME)
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") :
PUBLIC_RUNTIME.index("function Assert-FreshPublicProvenanceContract")
]
for expected in (
'schema_version = "vignette.public-runtime-launch-failure.v1"',
'"failed_rolled_back"',
'"failed_rollback"',
"failure_stage = $FailureStage",
"attempted = $RollbackAttempted",
"succeeded = $RollbackSucceeded",
"Write-Utf8TextAtomically",
):
with self.subTest(expected=expected):
self.assertIn(expected, failure_writer)
self.assertNotIn("command_line", failure_writer)
self.assertNotIn("executable_path", failure_writer)
def test_receipt_is_direct_input_for_windows_topology_capture(self) -> None:
receipt = PUBLIC_RUNTIME.index(
'schema_version = "vignette.public-runtime-launch-provenance.v1"'
)
receipt_section = PUBLIC_RUNTIME[receipt:]
for expected in (
"git_commit = $ExpectedSourceCommit.ToLowerInvariant()",
"git_tree = $ExpectedSourceTree.ToLowerInvariant()",
"api_stopped_pids",
"cloudflared_stopped_pids",
"api_new_pid",
"cloudflared_new_pid",
"started_at_utc",
"executable_sha256",
"command_line_sha256",
"api_pid",
"api_executable_name",
"api_cwd",
"cloudflared_pid",
"cloudflared_executable_name",
"cloudflared_cwd",
"psutil_version",
):
with self.subTest(expected=expected):
self.assertIn(expected, receipt_section)
if __name__ == "__main__":
unittest.main()