#Requires -Version 5.1 [CmdletBinding()] param( [string] $NodePath ) Set-StrictMode -Version Latest $ErrorActionPreference = 'Stop' function Resolve-NodePath { param([AllowEmptyString()][string] $ExplicitPath) if (-not [string]::IsNullOrWhiteSpace($ExplicitPath)) { if (-not [IO.Path]::IsPathRooted($ExplicitPath) -or -not (Test-Path -LiteralPath $ExplicitPath -PathType Leaf)) { throw 'NodePath must identify an existing absolute node executable path.' } return [IO.Path]::GetFullPath($ExplicitPath) } $command = Get-Command node -CommandType Application -ErrorAction SilentlyContinue | Select-Object -First 1 if ($null -ne $command) { return $command.Source } $programFiles = [Environment]::GetFolderPath( [Environment+SpecialFolder]::ProgramFiles) $candidates = @( (Join-Path $programFiles 'Microsoft Visual Studio\18\Community\MSBuild\Microsoft\VisualStudio\NodeJs\node.exe'), (Join-Path $programFiles 'Microsoft Visual Studio\2022\Community\MSBuild\Microsoft\VisualStudio\NodeJs\node.exe') ) foreach ($candidate in $candidates) { if (Test-Path -LiteralPath $candidate -PathType Leaf) { return [IO.Path]::GetFullPath($candidate) } } throw 'A Node.js executable was not found. Pass its absolute path with -NodePath.' } $node = Resolve-NodePath -ExplicitPath $NodePath $repositoryRoot = [IO.Path]::GetFullPath((Join-Path $PSScriptRoot '..')) $scripts = @( (Join-Path $repositoryRoot 'Web\playout-safety.js'), (Join-Path $repositoryRoot 'Web\app.js') ) $test = Join-Path $repositoryRoot 'tests\Web\playout-safety.test.cjs' foreach ($script in $scripts) { & $node --check $script if ($LASTEXITCODE -ne 0) { throw "JavaScript syntax validation failed with exit code $LASTEXITCODE." } } $appScript = Get-Content -LiteralPath (Join-Path $repositoryRoot 'Web\app.js') -Raw -Encoding UTF8 $mainWindowScript = Get-Content -LiteralPath (Join-Path $repositoryRoot 'MainWindow.xaml.cs') -Raw -Encoding UTF8 $sceneCatalogMatches = [regex]::Matches( $appScript, 'builderKey:\s*"s[0-9]+"') if ($sceneCatalogMatches.Count -ne 35) { throw "Web scene catalog must contain exactly 35 builder entries; found $($sceneCatalogMatches.Count)." } $selectionPresetMatches = [regex]::Matches( $appScript, '(?m)^\s+s[0-9]+:\s+\[') if ($selectionPresetMatches.Count -ne 34 -or $appScript -notmatch 'Every reachable legacy builder must have a Web selection preset') { throw "Web scene selection presets must cover exactly 34 reachable builders; found $($selectionPresetMatches.Count)." } if ($appScript -notmatch 'reachableAliases\.size !== 45' -or $appScript -notmatch 'sceneCutAlias' -or $appScript -notmatch 'sceneAliases\(definition\)\.includes\(storedCode\)' -or $appScript -notmatch 'resolveStoredCatalogIndex') { throw 'Web scene selection must expose and preserve all 45 active cut aliases.' } if ($appScript -match 'payload\.cue' -or $appScript -notmatch 'payload\.playlist' -or $appScript -notmatch 'payload\.selectedIndex') { throw 'Web playout must send a PREPARE playlist snapshot and must not choose the NEXT cue.' } if ($appScript -notmatch 'const DEFAULT_FADE_DURATION = 6;' -or $appScript -notmatch 'fadeDuration: DEFAULT_FADE_DURATION') { throw 'Web playout default fade must match MainForm ComboDi.SelectedIndex 6.' } if ($appScript -notmatch 'normalizeScenePreviewFields' -or $appScript -notmatch 'currentPageItemCount' -or $appScript -notmatch 'renderSceneDataPreview') { throw 'Web playout must render bounded authoritative scene data and complete page state.' } if ($appScript -notmatch 'row-enabled' -or $appScript -notmatch 'item\.enabled = include\.checked' -or $appScript -notmatch 'isPlaylistSnapshotLocked' -or $appScript -notmatch 'pending: Boolean\(state\.playout\.pending\)' -or $appScript -notmatch 'outcomeUnknown: state\.playout\.sessionQuarantined') { throw 'Web playlist must expose the legacy active flag and freeze its native snapshot.' } if ($appScript -notmatch 'postNative\("playout-timeout-quarantine", \{ requestId, command \}\)' -or $appScript -notmatch 'browserCorrelationQuarantined') { throw 'Web response timeouts must latch the process-lifetime native quarantine.' } if ($mainWindowScript -notmatch 'OnNavigationStarting' -or $mainWindowScript -notmatch 'OnProcessFailed' -or $mainWindowScript -notmatch 'ReloadBrowserSafely' -or $mainWindowScript -notmatch 'QuarantineIfBrowserCorrelationCanBeLost' -or $mainWindowScript -notmatch 'Volatile\.Read\(ref _playoutCommandInFlight\)') { throw 'Reload, navigation, and WebView process failure must quarantine in-flight playout correlation.' } & $node --test $test if ($LASTEXITCODE -ne 0) { throw "Web playout tests failed with exit code $LASTEXITCODE." } [pscustomobject][ordered]@{ Status = 'Passed' SyntaxFiles = $scripts.Count SceneCatalogEntries = $sceneCatalogMatches.Count ReachableSelectionPresets = $selectionPresetMatches.Count TestFile = [IO.Path]::GetFileName($test) }