vignette/scripts/test_start_public_runtime_contract.py
2026-08-09 18:22:03 +09:00

331 lines
15 KiB
Python

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()