from __future__ import annotations import unittest import shutil import subprocess 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_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"', "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)", "[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)", "[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' }} 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("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) 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 Stop-NodeByPortHint") ] 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_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("chr(0).join(p.cmdline())", identity) self.assertIn("command_line_sha256 = $commandLineSha256", identity) 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()