diff --git a/scripts/boot-public-runtime.ps1 b/scripts/boot-public-runtime.ps1 index ff9b33c..8704cd8 100644 --- a/scripts/boot-public-runtime.ps1 +++ b/scripts/boot-public-runtime.ps1 @@ -240,8 +240,15 @@ function Test-VoiceSidecarStack { "--tts-language", "KR", "--timeout-seconds", "5" ) - & $Python @probeArgs 1>$null 2>$null - return $LASTEXITCODE -eq 0 + $previousErrorActionPreference = $ErrorActionPreference + try { + $ErrorActionPreference = "Continue" + & $Python @probeArgs 1>$null 2>$null + $probeExit = $LASTEXITCODE + } finally { + $ErrorActionPreference = $previousErrorActionPreference + } + return $probeExit -eq 0 } Write-BootLog "================ boot start ================" diff --git a/scripts/start-public-runtime.ps1 b/scripts/start-public-runtime.ps1 index 3f5c735..e6c3efe 100644 --- a/scripts/start-public-runtime.ps1 +++ b/scripts/start-public-runtime.ps1 @@ -139,8 +139,15 @@ function Test-VoiceSidecarReady { "--tts-language", $MeloTtsLanguage, "--timeout-seconds", "5" ) - & $Python @probeArgs 1>$null 2>$null - return $LASTEXITCODE -eq 0 + $previousErrorActionPreference = $ErrorActionPreference + try { + $ErrorActionPreference = "Continue" + & $Python @probeArgs 1>$null 2>$null + $probeExit = $LASTEXITCODE + } finally { + $ErrorActionPreference = $previousErrorActionPreference + } + return $probeExit -eq 0 } function Wait-VoiceSidecarReady { diff --git a/scripts/test_public_runtime_watchdog_provenance.py b/scripts/test_public_runtime_watchdog_provenance.py index 86dee61..4807e16 100644 --- a/scripts/test_public_runtime_watchdog_provenance.py +++ b/scripts/test_public_runtime_watchdog_provenance.py @@ -24,6 +24,7 @@ INSTALLER_SOURCE = INSTALLER.read_text(encoding="utf-8") BOOT_SOURCE = BOOT.read_text(encoding="utf-8") BOOT_REGISTER_SOURCE = BOOT_REGISTER.read_text(encoding="utf-8") HIDDEN_TRIGGER_SOURCE = HIDDEN_TRIGGER.read_text(encoding="utf-8") +START_SOURCE = START.read_text(encoding="utf-8") RUNBOOK_SOURCE = RUNBOOK.read_text(encoding="utf-8") LOCAL_DEVELOPMENT_SOURCE = LOCAL_DEVELOPMENT.read_text(encoding="utf-8") @@ -264,6 +265,79 @@ if ($result.Detail -ne 'exact readiness probe failed') { msg=f"stdout={completed.stdout}\nstderr={completed.stderr}", ) + def test_boolean_sidecar_probes_survive_native_stderr_under_powershell_51(self) -> None: + powershell = shutil.which("powershell.exe") + python = shutil.which("python.exe") + if powershell is None or python is None: + self.skipTest("Windows PowerShell 5.1 and Python are required") + + contracts = ( + ( + START_SOURCE[ + START_SOURCE.index("function Test-VoiceSidecarReady") : + START_SOURCE.index("function Wait-VoiceSidecarReady") + ].strip(), + "$result = Test-VoiceSidecarReady -Component 'stt'", + "start", + ), + ( + BOOT_SOURCE[ + BOOT_SOURCE.index("function Test-VoiceSidecarStack") : + BOOT_SOURCE.index('Write-BootLog "================ boot start') + ].strip(), + "$result = Test-VoiceSidecarStack", + "boot", + ), + ) + python_escaped = python.replace("'", "''") + probe_escaped = str(VOICE_PROBE).replace("'", "''") + + with tempfile.TemporaryDirectory() as temporary_directory: + for contract, invocation, role in contracts: + harness = Path(temporary_directory) / f"{role}-probe-contract.ps1" + harness.write_text( + "$ErrorActionPreference = 'Stop'\n" + + f"$Python = '{python_escaped}'\n" + + f"$VoiceSidecarProbe = '{probe_escaped}'\n" + + f"$voiceSidecarProbe = '{probe_escaped}'\n" + + "$WhisperPort = 1\n" + + "$MeloTtsPort = 2\n" + + "$WhisperModel = 'small'\n" + + "$WhisperLanguage = 'ko'\n" + + "$WhisperDevice = 'cpu'\n" + + "$MeloTtsModel = 'melotts-korean'\n" + + "$MeloTtsLanguage = 'KR'\n" + + contract + + "\n" + + invocation + + "\nif ($result) { throw 'unreachable sidecars were accepted' }\n", + 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, + ) + with self.subTest(role=role): + self.assertEqual( + completed.returncode, + 0, + msg=f"stdout={completed.stdout}\nstderr={completed.stderr}", + ) + def test_hidden_trigger_never_executes_a_workspace_script_directly(self) -> None: for expected in ( "VignettePublicRuntimeWatchdog",