from __future__ import annotations import hashlib import shutil import subprocess import tempfile import unittest from pathlib import Path SCRIPTS = Path(__file__).resolve().parent WATCHDOG = SCRIPTS / "watch-public-runtime.ps1" INSTALLER = SCRIPTS / "install-public-runtime-task.ps1" BOOT = SCRIPTS / "boot-public-runtime.ps1" BOOT_REGISTER = SCRIPTS / "register-boot-task.ps1" HIDDEN_TRIGGER = SCRIPTS / "watch-public-runtime-hidden.vbs" TASK_LAUNCHER = SCRIPTS / "watch-public-runtime-task.vbs" START = SCRIPTS / "start-public-runtime.ps1" VOICE_PROBE = SCRIPTS / "probe-public-voice-sidecars.py" UPLOAD_ROOT_CONTRACT = SCRIPTS / "public-runtime-upload-root.ps1" UPLOAD_ROOT_PROBE = SCRIPTS / "probe-public-runtime-upload-root.py" UPLOAD_MANIFEST_PROBE = SCRIPTS / "validate-public-runtime-upload-manifest.py" DATABASE_IDENTITY = SCRIPTS / "public_runtime_database_identity.py" REPO_ROOT = SCRIPTS.parent RUNBOOK = REPO_ROOT / "docs" / "ops" / "public-runtime-watchdog.md" LOCAL_DEVELOPMENT = REPO_ROOT / "docs" / "guides" / "local-development.md" WATCHDOG_SOURCE = WATCHDOG.read_text(encoding="utf-8") 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") def sha256(path: Path) -> str: return hashlib.sha256(path.read_bytes()).hexdigest() class PublicRuntimeWatchdogProvenanceTest(unittest.TestCase): def test_watchdog_requires_exact_source_and_script_pins(self) -> None: for expected in ( "[string]$StableSourceRoot", "[string]$ExpectedSourceCommit", "[string]$ExpectedSourceTree", "[string]$ExpectedWatchdogSha256", "[string]$ExpectedStartScriptSha256", 'symbolic-ref --quiet HEAD', '"status", "--porcelain=v1", "--untracked-files=normal"', '"ls-files", "--error-unmatch"', ): with self.subTest(expected=expected): self.assertIn(expected, WATCHDOG_SOURCE) self.assertGreaterEqual(WATCHDOG_SOURCE.count("[Parameter(Mandatory = $true)]"), 5) def test_provenance_gate_precedes_health_and_runtime_recovery(self) -> None: gate_comment = WATCHDOG_SOURCE.index( "# health probe, failcount 기록, 프로세스 재기동보다 먼저" ) gate = WATCHDOG_SOURCE.index("Assert-StableSourceProvenance", gate_comment) health = WATCHDOG_SOURCE.index("$checks = @(") restart = WATCHDOG_SOURCE.index("& $startScript @startArgs") failcount_write = WATCHDOG_SOURCE.index("Set-FailCount 0", health) self.assertLess(gate, health) self.assertLess(gate, restart) self.assertLess(gate, failcount_write) def test_installer_pins_a_detached_clean_release_in_task_action(self) -> None: for expected in ( "[Parameter(Mandatory = $true)]", "[string]$StableSourceRoot", "symbolic-ref --quiet HEAD", '"status", "--porcelain=v1", "--untracked-files=normal"', '"-ExpectedSourceCommit $sourceCommit"', '"-ExpectedSourceTree $sourceTree"', '"-ExpectedWatchdogSha256 $watchdogSha256"', '"-ExpectedStartScriptSha256 $startScriptSha256"', '"-UserUploadDir `"$resolvedUserUploadDir`""', "-WorkingDirectory $resolvedSourceRoot", "Watchdog installer is not executing from the pinned stable source root", ): with self.subTest(expected=expected): self.assertIn(expected, INSTALLER_SOURCE) dirty_gate = INSTALLER_SOURCE.index("Stable source is not clean") task_registration = INSTALLER_SOURCE.index("Register-ScheduledTask") self.assertLess(dirty_gate, task_registration) def test_boot_recovery_uses_the_same_gate_before_docker_or_process_mutation(self) -> None: for expected in ( "[string]$StableSourceRoot", "[string]$ExpectedSourceCommit", "[string]$ExpectedSourceTree", "[string]$ExpectedBootScriptSha256", "[string]$ExpectedStartScriptSha256", "symbolic-ref --quiet HEAD", '"status", "--porcelain=v1", "--untracked-files=normal"', ): with self.subTest(expected=expected): self.assertIn(expected, BOOT_SOURCE) gate_comment = BOOT_SOURCE.index( "# Docker/DB/process mutation보다 먼저 stable source를 매 실행 재검증한다." ) gate = BOOT_SOURCE.index("Assert-StableSourceProvenance", gate_comment) docker_mutation = BOOT_SOURCE.index("docker.exe update") runtime_recovery = BOOT_SOURCE.index("& $startScript @startArgs |") self.assertLess(gate, docker_mutation) self.assertLess(gate, runtime_recovery) def test_boot_task_registration_pins_the_same_detached_release(self) -> None: for expected in ( "[string]$StableSourceRoot", "symbolic-ref --quiet HEAD", '"status", "--porcelain=v1", "--untracked-files=normal"', '"-ExpectedSourceCommit $sourceCommit"', '"-ExpectedSourceTree $sourceTree"', '"-ExpectedBootScriptSha256 $bootScriptSha256"', '"-ExpectedStartScriptSha256 $startScriptSha256"', '"-UserUploadDir `"$resolvedUserUploadDir`""', "-WorkingDirectory $resolvedSourceRoot", "Boot task registrar is not executing from the pinned stable source root", ): with self.subTest(expected=expected): self.assertIn(expected, BOOT_REGISTER_SOURCE) dirty_gate = BOOT_REGISTER_SOURCE.index("Stable source is not clean") task_registration = BOOT_REGISTER_SOURCE.index("Register-ScheduledTask") self.assertLess(dirty_gate, task_registration) def test_task_limits_cover_a_full_cold_start_without_overlap(self) -> None: self.assertIn("-ExecutionTimeLimit (New-TimeSpan -Minutes 60)", BOOT_REGISTER_SOURCE) self.assertIn("-MultipleInstances IgnoreNew", BOOT_REGISTER_SOURCE) self.assertIn("-ExecutionTimeLimit (New-TimeSpan -Minutes 60)", INSTALLER_SOURCE) self.assertIn("-MultipleInstances IgnoreNew", INSTALLER_SOURCE) self.assertNotIn("-RestartCount", INSTALLER_SOURCE) self.assertNotIn("-RestartInterval", INSTALLER_SOURCE) # 최초 health 진단 + DB 복구 + engine/voice/API/web의 각 bounded wait와 # process-stop 여유를 모두 합쳐도 작업 스케줄러 60분보다 5분 이상 짧다. initial_health_seconds = 210 recovery_seconds = 60 + 90 + (300 * 2) + 180 + 90 + 600 + 30 + (15 * 4) safety_margin_seconds = 300 task_limit_seconds = 60 * 60 self.assertLess( initial_health_seconds + recovery_seconds + safety_margin_seconds, task_limit_seconds, ) boot_prerequisite_seconds = 360 + 90 self.assertLess( boot_prerequisite_seconds + recovery_seconds + safety_margin_seconds, task_limit_seconds, ) def test_hard_down_bypasses_transient_failure_debounce(self) -> None: hard_down = WATCHDOG_SOURCE.index("$hardDown =") debounce = WATCHDOG_SOURCE.index( "if ($failCount -lt $FailuresBeforeRestart -and -not $hardDown)" ) restart = WATCHDOG_SOURCE.index("& $startScript @startArgs") self.assertLess(hard_down, debounce) self.assertLess(debounce, restart) self.assertIn('($FailedNames -contains "cloudflared")', WATCHDOG_SOURCE) self.assertIn('($FailedNames -contains "api")', WATCHDOG_SOURCE) self.assertIn('($FailedNames -contains "web-preview")', WATCHDOG_SOURCE) self.assertIn('($FailedNames -contains "voice-sidecars")', WATCHDOG_SOURCE) self.assertIn( "immediate restart: public runtime hard-down detected", WATCHDOG_SOURCE, ) def test_hard_down_classifier_distinguishes_total_and_transient_failures(self) -> None: powershell = shutil.which("powershell.exe") if powershell is None: self.skipTest("Windows PowerShell 5.1 is not available") classifier = WATCHDOG_SOURCE[ WATCHDOG_SOURCE.index("function Test-PublicRuntimeHardDown") : WATCHDOG_SOURCE.index("# engine 판정은", WATCHDOG_SOURCE.index("function Test-PublicRuntimeHardDown")) ].strip() with tempfile.TemporaryDirectory() as temporary_directory: harness = Path(temporary_directory) / "hard-down.ps1" harness.write_text( classifier + r''' if (-not (Test-PublicRuntimeHardDown -FailedNames @('cloudflared'))) { exit 2 } if (-not (Test-PublicRuntimeHardDown -FailedNames @('api','web-preview','voice-sidecars'))) { exit 3 } if (Test-PublicRuntimeHardDown -FailedNames @('engine')) { exit 4 } 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_check_only_exits_before_failcount_log_or_recovery_mutation(self) -> None: failed = WATCHDOG_SOURCE.index("$failed = @(") check_only = WATCHDOG_SOURCE.index("if ($CheckOnly)", failed) inner_health = WATCHDOG_SOURCE.index("if ($failed.Count -eq 0)", check_only) normal_health = WATCHDOG_SOURCE.index( "if ($failed.Count -eq 0)", inner_health + 1 ) fail_count = WATCHDOG_SOURCE.index("$failCount =", check_only) restart = WATCHDOG_SOURCE.index("& $startScript @startArgs", fail_count) self.assertLess(check_only, fail_count) self.assertLess(check_only, restart) check_only_contract = WATCHDOG_SOURCE[check_only:normal_health] self.assertIn("Write-Output", check_only_contract) self.assertNotIn("Set-FailCount", check_only_contract) self.assertNotIn("Write-WatchdogLog", check_only_contract) self.assertNotIn("Restore-DatabaseContainer", check_only_contract) def test_boot_streams_start_script_progress_without_nested_powershell_buffering(self) -> None: self.assertIn("& $startScript @startArgs |", BOOT_SOURCE) self.assertIn('Write-BootLog (" pub> " + $_)', BOOT_SOURCE) self.assertIn("start-public-runtime.ps1 failed", BOOT_SOURCE) self.assertNotIn("$out = & powershell.exe", BOOT_SOURCE) def test_watchdog_and_boot_probe_exact_local_voice_stack(self) -> None: for source in (WATCHDOG_SOURCE, BOOT_SOURCE): for expected in ( '"--component", "all"', '"--stt-provider", "local_whisper"', '"--stt-model", "small"', '"--stt-device", "cpu"', '"--tts-provider", "melotts"', '"--tts-model", "melotts-korean"', '"scripts\\probe-public-voice-sidecars.py"', "Test-VoiceSidecarStack", ): with self.subTest(source=source[:32], expected=expected): self.assertIn(expected, source) self.assertIn("(Test-VoiceSidecarStack)", WATCHDOG_SOURCE) self.assertIn("$voiceSidecarsAfter = Test-VoiceSidecarStack", WATCHDOG_SOURCE) self.assertIn('-Name "voice-api"', WATCHDOG_SOURCE) self.assertIn("$voiceApiAfter = Test-JsonHealth", WATCHDOG_SOURCE) boot_skip_gate = BOOT_SOURCE[ BOOT_SOURCE.index("$webHealthy = Test-WebPreviewHealthy") : BOOT_SOURCE.index("$startArgs = @(") ] for expected in ( "(Test-ApiControlPlaneHealthy)", "(Test-EngineHealthy)", "(Test-VoiceApiHealthy)", "(Test-VoiceSidecarStack)", "(Test-PublicRuntimeApiUploadRoot `", ): with self.subTest(expected=expected): self.assertIn(expected, boot_skip_gate) self.assertGreaterEqual(BOOT_SOURCE.count("Test-VoiceApiHealthy"), 3) self.assertIn('"-WhisperPort", $WhisperPort', BOOT_SOURCE) self.assertIn('"-MeloTtsPort", $MeloTtsPort', BOOT_SOURCE) # web preview는 살아 있을 때만 스킵한다. 무조건 -SkipWebRestart면 재부팅 후 # boot 경로로 web이 복구되지 않는다. self.assertIn("$webHealthy = Test-WebPreviewHealthy", BOOT_SOURCE) self.assertIn('if ($webHealthy) {\n $startArgs += "-SkipWebRestart"', BOOT_SOURCE) self.assertIn("scripts/probe-public-voice-sidecars.py", INSTALLER_SOURCE) self.assertIn("scripts/probe-public-voice-sidecars.py", BOOT_REGISTER_SOURCE) def test_old_openai_api_is_not_healthy_when_exact_sidecars_are_ready(self) -> None: powershell = shutil.which("powershell.exe") if powershell is None: self.skipTest("Windows PowerShell 5.1 is not available") watchdog_contract = WATCHDOG_SOURCE[ WATCHDOG_SOURCE.index("function Test-VoiceApiReady") : WATCHDOG_SOURCE.index("function Test-VoiceSidecarStack") ].strip() boot_contract = BOOT_SOURCE[ BOOT_SOURCE.index("function Test-VoiceApiReady") : BOOT_SOURCE.index("function Test-VoiceApiHealthy") ].strip() self.assertEqual(watchdog_contract, boot_contract) with tempfile.TemporaryDirectory() as temporary_directory: harness = Path(temporary_directory) / "voice-api-contract.ps1" harness.write_text( watchdog_contract + r''' $openAi = [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} } if (Test-VoiceApiReady -Health $openAi) { throw 'old OpenAI API was accepted' } $local = [pscustomobject]@{ status='ok';available=$true;stt_available=$true;tts_available=$true stt_provider='local_whisper';stt_model='small' tts_provider='melotts';tts_model='melotts-korean' limits=[pscustomobject]@{uvicorn_ws_max_queue=4} } if (-not (Test-VoiceApiReady -Health $local)) { throw 'exact local API was rejected' } $local.limits.uvicorn_ws_max_queue = 5 if (Test-VoiceApiReady -Health $local) { throw 'wrong websocket queue was accepted' } ''', 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_failed_native_sidecar_probe_returns_unhealthy_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") watchdog_contract = WATCHDOG_SOURCE[ WATCHDOG_SOURCE.index("function Test-VoiceSidecarStack") : WATCHDOG_SOURCE.index("function Test-CloudflaredProcess") ].strip() with tempfile.TemporaryDirectory() as temporary_directory: harness = Path(temporary_directory) / "voice-sidecar-failure-contract.ps1" python_escaped = python.replace("'", "''") probe_escaped = str(VOICE_PROBE).replace("'", "''") harness.write_text( "$ErrorActionPreference = 'Stop'\n" + f"$Python = '{python_escaped}'\n" + f"$voiceSidecarProbe = '{probe_escaped}'\n" + "$WhisperPort = 1\n" + "$MeloTtsPort = 2\n" + watchdog_contract + r''' $result = Test-VoiceSidecarStack if ($result.Ok) { throw 'unreachable sidecars were accepted' } if ($result.Detail -ne 'exact readiness probe failed') { throw "unexpected detail: $($result.Detail)" } ''', 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_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", "-StableSourceRoot", "-ExpectedSourceCommit", "-ExpectedSourceTree", "-ExpectedWatchdogSha256", "-ExpectedStartScriptSha256", "-UserUploadDir", "WorkingDirectory", "Pinned watchdog script path does not match its working directory", "Pinned watchdog source root does not match its working directory", "Start-ScheduledTask", "Legacy shared-worktree watchdog action is forbidden", ): with self.subTest(expected=expected): self.assertIn(expected, HIDDEN_TRIGGER_SOURCE) self.assertIn( "Join-Path $root 'scripts\\watch-public-runtime.ps1'", HIDDEN_TRIGGER_SOURCE, ) self.assertNotIn('objShell.Run "powershell.exe', HIDDEN_TRIGGER_SOURCE) self.assertNotIn("D:\\workspace\\vignette", HIDDEN_TRIGGER_SOURCE) marker_gate = HIDDEN_TRIGGER_SOURCE.index("foreach($marker in $required)") task_trigger = HIDDEN_TRIGGER_SOURCE.rindex("Start-ScheduledTask") self.assertLess(marker_gate, task_trigger) def test_active_docs_only_show_the_pinned_release_workflow(self) -> None: stale_commands = ( "powershell -NoProfile -ExecutionPolicy Bypass -File scripts\\watch-public-runtime.ps1 -CheckOnly", "powershell -NoProfile -ExecutionPolicy Bypass -File scripts\\install-public-runtime-task.ps1 -RunNow", ) for source in (RUNBOOK_SOURCE, LOCAL_DEVELOPMENT_SOURCE): for stale in stale_commands: with self.subTest(document=source[:32], stale=stale): self.assertNotIn(stale, source) for expected in ( "detached HEAD", "-StableSourceRoot", "-ExpectedSourceCommit", "-ExpectedSourceTree", "-ExpectedWatchdogSha256", "-ExpectedStartScriptSha256", "-UserUploadDir", "--porcelain=v1 --untracked-files=normal", ): with self.subTest(expected=expected): self.assertIn(expected, RUNBOOK_SOURCE) self.assertIn("-StableSourceRoot", LOCAL_DEVELOPMENT_SOURCE) self.assertIn("watch-public-runtime-hidden.vbs", LOCAL_DEVELOPMENT_SOURCE) @unittest.skipUnless(shutil.which("git.exe"), "git.exe is unavailable") @unittest.skipUnless(shutil.which("powershell.exe"), "Windows PowerShell 5.1 is unavailable") def test_dirty_detached_release_fails_before_health_or_runtime_mutation(self) -> None: with tempfile.TemporaryDirectory(prefix="vignette-watchdog-") as temp: root = Path(temp) scripts = root / "scripts" scripts.mkdir() copied_watchdog = scripts / WATCHDOG.name copied_installer = scripts / INSTALLER.name copied_boot = scripts / BOOT.name copied_boot_register = scripts / BOOT_REGISTER.name copied_start = scripts / START.name copied_voice_probe = scripts / VOICE_PROBE.name copied_upload_contract = scripts / UPLOAD_ROOT_CONTRACT.name copied_upload_probe = scripts / UPLOAD_ROOT_PROBE.name copied_upload_manifest_probe = scripts / UPLOAD_MANIFEST_PROBE.name copied_database_identity = scripts / DATABASE_IDENTITY.name copied_task_launcher = scripts / TASK_LAUNCHER.name shutil.copy2(WATCHDOG, copied_watchdog) shutil.copy2(INSTALLER, copied_installer) shutil.copy2(BOOT, copied_boot) shutil.copy2(BOOT_REGISTER, copied_boot_register) shutil.copy2(START, copied_start) shutil.copy2(VOICE_PROBE, copied_voice_probe) shutil.copy2(UPLOAD_ROOT_CONTRACT, copied_upload_contract) shutil.copy2(UPLOAD_ROOT_PROBE, copied_upload_probe) shutil.copy2(UPLOAD_MANIFEST_PROBE, copied_upload_manifest_probe) shutil.copy2(DATABASE_IDENTITY, copied_database_identity) shutil.copy2(TASK_LAUNCHER, copied_task_launcher) self._git(root, "init") self._git(root, "config", "user.name", "Watchdog Contract Test") self._git(root, "config", "user.email", "watchdog-test@example.invalid") self._git( root, "add", "scripts/watch-public-runtime.ps1", "scripts/install-public-runtime-task.ps1", "scripts/boot-public-runtime.ps1", "scripts/register-boot-task.ps1", "scripts/start-public-runtime.ps1", "scripts/probe-public-voice-sidecars.py", "scripts/public-runtime-upload-root.ps1", "scripts/probe-public-runtime-upload-root.py", "scripts/validate-public-runtime-upload-manifest.py", "scripts/public_runtime_database_identity.py", "scripts/watch-public-runtime-task.vbs", ) self._git(root, "commit", "-m", "watchdog fixture") self._git(root, "checkout", "--detach") commit = self._git(root, "rev-parse", "HEAD").stdout.strip() tree = self._git(root, "rev-parse", "HEAD^{tree}").stdout.strip() watchdog_hash = sha256(copied_watchdog) boot_hash = sha256(copied_boot) start_hash = sha256(copied_start) upload_root = Path( tempfile.mkdtemp(prefix="vignette-watchdog-uploads-") ) self.addCleanup(shutil.rmtree, upload_root, True) with copied_start.open("ab") as stream: stream.write(b"\n# dirty fixture\n") completed = subprocess.run( [ shutil.which("powershell.exe") or "powershell.exe", "-NoProfile", "-ExecutionPolicy", "Bypass", "-File", str(copied_watchdog), "-StableSourceRoot", str(root), "-ExpectedSourceCommit", commit, "-ExpectedSourceTree", tree, "-ExpectedWatchdogSha256", watchdog_hash, "-ExpectedStartScriptSha256", start_hash, "-UserUploadDir", str(upload_root), "-UserUploadManifestPath", str(root / "private-manifest.json"), "-ExpectedUserUploadManifestSha256", "e" * 64, "-UserUploadWriteFreezePath", str(root / "private-freeze.json"), "-CheckOnly", "-SkipPublicHealth", "-SkipCloudflaredRestart", ], check=False, capture_output=True, text=True, encoding="utf-8", errors="replace", timeout=30, ) detail = f"{completed.stdout}\n{completed.stderr}" self.assertNotEqual(completed.returncode, 0, detail) self.assertIn("Stable source is not clean", detail) self.assertFalse((root / "public-runtime-watchdog.failcount").exists()) self.assertFalse((root / "public-runtime-watchdog.log").exists()) boot_completed = subprocess.run( [ shutil.which("powershell.exe") or "powershell.exe", "-NoProfile", "-ExecutionPolicy", "Bypass", "-File", str(copied_boot), "-StableSourceRoot", str(root), "-ExpectedSourceCommit", commit, "-ExpectedSourceTree", tree, "-ExpectedBootScriptSha256", boot_hash, "-ExpectedStartScriptSha256", start_hash, "-UserUploadDir", str(upload_root), "-UserUploadManifestPath", str(root / "private-manifest.json"), "-ExpectedUserUploadManifestSha256", "e" * 64, "-UserUploadWriteFreezePath", str(root / "private-freeze.json"), ], check=False, capture_output=True, text=True, encoding="utf-8", errors="replace", timeout=30, ) boot_detail = f"{boot_completed.stdout}\n{boot_completed.stderr}" self.assertNotEqual(boot_completed.returncode, 0, boot_detail) self.assertIn("Stable source is not clean", boot_detail) self.assertFalse((root / "boot-public-runtime.log").exists()) for registrar in (copied_installer, copied_boot_register): register_completed = subprocess.run( [ shutil.which("powershell.exe") or "powershell.exe", "-NoProfile", "-ExecutionPolicy", "Bypass", "-File", str(registrar), "-StableSourceRoot", str(root), "-UserUploadDir", str(upload_root), "-UserUploadManifestPath", str(root / "private-manifest.json"), "-ExpectedUserUploadManifestSha256", "e" * 64, "-UserUploadWriteFreezePath", str(root / "private-freeze.json"), ], check=False, capture_output=True, text=True, encoding="utf-8", errors="replace", timeout=30, ) register_detail = ( f"{register_completed.stdout}\n{register_completed.stderr}" ) with self.subTest(registrar=registrar.name): self.assertNotEqual(register_completed.returncode, 0, register_detail) self.assertIn("Stable source is not clean", register_detail) def test_windows_powershell_51_parser_accepts_scripts(self) -> None: powershell = shutil.which("powershell.exe") if powershell is None: self.skipTest("Windows PowerShell 5.1 is unavailable") for script in (WATCHDOG, INSTALLER, BOOT, BOOT_REGISTER, UPLOAD_ROOT_CONTRACT): escaped = str(script.resolve()).replace("'", "''") command = ( "$tokens=$null; $errors=$null; " "[System.Management.Automation.Language.Parser]::ParseFile(" f"'{escaped}', [ref]$tokens, [ref]$errors) | Out-Null; " "if ($errors.Count -gt 0) { $errors | ForEach-Object { Write-Error $_ }; exit 1 }; " "exit 0" ) completed = subprocess.run( [powershell, "-NoProfile", "-Command", command], check=False, capture_output=True, text=True, encoding="utf-8", errors="replace", ) with self.subTest(script=script.name): self.assertEqual(completed.returncode, 0, completed.stderr) def test_windows_script_host_accepts_hidden_trigger_syntax(self) -> None: cscript = shutil.which("cscript.exe") if cscript is None: self.skipTest("Windows Script Host is unavailable") completed = subprocess.run( [cscript, "//nologo", str(HIDDEN_TRIGGER), "syntax-only"], check=False, capture_output=True, text=True, encoding="utf-8", errors="replace", ) self.assertEqual(completed.returncode, 0, completed.stderr) def test_hidden_trigger_command_accepts_only_a_structurally_pinned_task(self) -> None: cscript = shutil.which("cscript.exe") powershell = shutil.which("powershell.exe") if cscript is None or powershell is None: self.skipTest("Windows Script Host or PowerShell 5.1 is unavailable") rendered = subprocess.run( [cscript, "//nologo", str(HIDDEN_TRIGGER), "print-command"], check=False, capture_output=True, text=True, encoding="utf-8", errors="replace", ) self.assertEqual(rendered.returncode, 0, rendered.stderr) prefix = ( "powershell.exe -NoProfile -ExecutionPolicy Bypass " '-WindowStyle Hidden -Command "' ) wrapped = rendered.stdout.strip() self.assertTrue(wrapped.startswith(prefix), wrapped) self.assertTrue(wrapped.endswith('"'), wrapped) command = wrapped[len(prefix) : -1].replace('""', '"') root = r"C:\Pinned Vignette Release" # 액션은 wscript 런처를 거친다 — powershell.exe 직접 실행은 conhost 창이 번쩍인다. arguments = ( f'"{root}\\scripts\\watch-public-runtime-task.vbs" ' f'-File "{root}\\scripts\\watch-public-runtime.ps1" ' f'-StableSourceRoot "{root}" ' f"-ExpectedSourceCommit {'a' * 40} " f"-ExpectedSourceTree {'b' * 40} " f"-ExpectedWatchdogSha256 {'c' * 64} " f"-ExpectedStartScriptSha256 {'d' * 64} " f'-UserUploadDir "C:\\Persistent Vignette Uploads" ' f'-UserUploadManifestPath "C:\\Private Vignette State\\manifest.json" ' f"-ExpectedUserUploadManifestSha256 {'e' * 64} " f'-UserUploadWriteFreezePath "C:\\Private Vignette State\\freeze.json"' ) fixture = ( "$script:watchdogTriggered=$false;" "function Get-ScheduledTask { param($TaskName,$ErrorAction) " "$action=[pscustomobject]@{" "Execute='C:\\Windows\\System32\\wscript.exe';" f"WorkingDirectory='{root}';Arguments='{arguments}'" "};[pscustomobject]@{Actions=@($action)}};" "function Start-ScheduledTask { param($TaskName) " "$script:watchdogTriggered=$true };" f"{command};" "if(-not $script:watchdogTriggered){exit 9};exit 0" ) checked = subprocess.run( [powershell, "-NoProfile", "-Command", fixture], check=False, capture_output=True, text=True, encoding="utf-8", errors="replace", ) self.assertEqual(checked.returncode, 0, checked.stderr) def _git(self, root: Path, *args: str) -> subprocess.CompletedProcess[str]: completed = subprocess.run( [shutil.which("git.exe") or "git.exe", "-C", str(root), *args], check=False, capture_output=True, text=True, encoding="utf-8", errors="replace", ) self.assertEqual(completed.returncode, 0, completed.stderr) return completed if __name__ == "__main__": unittest.main()