diff --git a/AGENTS.md b/AGENTS.md
index 86def2f..de80da6 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -21,8 +21,12 @@ approval before operating Tornado2/PGM.
The standing authorization includes launching and closing the development app,
CONNECT/DISCONNECT, PREPARE, TAKE IN, NEXT, Page NEXT, TAKE OUT, observing Network
Monitoring/PGM, and development-DB reads or writes needed by an in-scope migration
-test. Prefer the existing Debug development-live profile and its ignored local
-authorization file; do not weaken Release/default `DryRun` behavior.
+test. On designated playout PCs, normal Debug and Release application launches are Live.
+Never use DryRun for app launches, tests, diagnostics, or migration validation.
+Every in-scope playout run on a designated development playout PC must use Live
+output. If required DB, K3D, target identity, allowlist, or authorization
+validation fails, block startup with a clear error instead of falling back to
+DryRun.
This is standing permission, not permission to guess after an ambiguous outcome:
diff --git a/Config/playout.example.json b/Config/playout.example.json
index ba93de0..f1a8689 100644
--- a/Config/playout.example.json
+++ b/Config/playout.example.json
@@ -1,5 +1,5 @@
{
- "mode": "DryRun",
+ "mode": "Live",
"host": "127.0.0.1",
"port": 30001,
"tcpMode": 1,
diff --git a/README.md b/README.md
index e2a8bb6..38945f4 100644
--- a/README.md
+++ b/README.md
@@ -38,10 +38,10 @@
- WebView 코스피·코스닥·NXT·5개 지수·해외 실데이터 조회와 장애 UI
- COM 중립 `IPlayoutEngine`, x64 K3D late binding, 전용 STA 큐와 프로세스 감시
- 네이티브 결과가 성공한 뒤에만 갱신되는 `PREPARE` / `TAKE IN` / `NEXT` / `TAKE OUT` WebView 브리지
-- 기본 `DryRun`, Test 전용 인스턴스·채널·씬 allowlist 및 Live 이중 승인 안전 게이트
+- 지정 개발 PC의 보호된 Live 전용 기동, Test 전용 인스턴스·채널·씬 allowlist 및 Live 이중 승인 안전 게이트
- MSIX 패키지 매니페스트와 x64 게시 프로필
-Oracle/MariaDB 조회 계층, 원본 10초 DB 상태 감시, Tornado/K3D 어댑터와 35개 scene 계약을 구현했습니다. 2026-07-22 현재 최신 개발 MSIX의 전체 UI/DB·로컬 상태 물리 검증과 실제 PGM runtime route code 33/34가 승인된 action 범위에서 끝났습니다. `s5006` 큐브 배경 action과 `s6001` 국가 영상 13 action만 원본에도 없는 외부 제작 자산 때문에 fail closed 상태이며, `s6001`의 두바이유·WTI·브렌트유·금 4 action은 실제 PGM 검증을 완료했습니다. 앱과 Release 패키지의 기본 모드는 계속 `DryRun`이며 Live allowlist, 명령 예산, callback/`OutcomeUnknown` 게이트를 완화하지 않습니다. 이 개발 PC의 Tornado2/PGM 마이그레이션 검증은 사용자가 반복 승인 없이 진행하도록 상시 허용했으며, 적용 범위와 중단 조건은 [개발 환경 상시 권한](docs/DEVELOPMENT_ENVIRONMENT_AUTHORIZATION.md)에 기록했습니다. DB 설정은 [DB 운영 가이드](docs/DATABASE.md), 송출 설정·실제 검증 증거·롤백은 [Tornado/K3D 운영 가이드](docs/PLAYOUT.md), 원본 Scene/PageN 대조는 [송출 흐름 분석](docs/LEGACY_PLAYOUT_ANALYSIS.md), 장면별 현황은 [35개 Scene 동등성 매트릭스](docs/SCENE_EQUIVALENCE.md), 전체 412개 실행 action과 전용 편집 화면은 [운영자 UI 동등성 인벤토리](docs/OPERATOR_UI_PARITY.md)를 참고하세요.
+Oracle/MariaDB 조회 계층, 원본 10초 DB 상태 감시, Tornado/K3D 어댑터와 35개 scene 계약을 구현했습니다. 2026-07-22 현재 최신 개발 MSIX의 전체 UI/DB·로컬 상태 물리 검증과 실제 PGM runtime route code 33/34가 승인된 action 범위에서 끝났습니다. `s5006` 큐브 배경 action과 `s6001` 국가 영상 13 action만 원본에도 없는 외부 제작 자산 때문에 fail closed 상태이며, `s6001`의 두바이유·WTI·브렌트유·금 4 action은 실제 PGM 검증을 완료했습니다. 지정 개발 PC의 Debug·Release 패키지는 보호된 `Live` 설정으로만 기동하며 `DryRun`으로 폴백하지 않습니다. Live allowlist, 명령 예산, callback/`OutcomeUnknown` 게이트는 완화하지 않습니다. 이 개발 PC의 Tornado2/PGM 마이그레이션 검증은 사용자가 반복 승인 없이 진행하도록 상시 허용했으며, 적용 범위와 중단 조건은 [개발 환경 상시 권한](docs/DEVELOPMENT_ENVIRONMENT_AUTHORIZATION.md)에 기록했습니다. DB 설정은 [DB 운영 가이드](docs/DATABASE.md), 송출 설정·실제 검증 증거·롤백은 [Tornado/K3D 운영 가이드](docs/PLAYOUT.md), 원본 Scene/PageN 대조는 [송출 흐름 분석](docs/LEGACY_PLAYOUT_ANALYSIS.md), 장면별 현황은 [35개 Scene 동등성 매트릭스](docs/SCENE_EQUIVALENCE.md), 전체 412개 실행 action과 전용 편집 화면은 [운영자 UI 동등성 인벤토리](docs/OPERATOR_UI_PARITY.md)를 참고하세요.
35개 Scene/PageN 런타임과 원본 MainForm·UC1~UC7·GraphE·FSell·VIList·PList·AList·ThemeA·EList의 화면·bridge 계약은 WinUI 3/WebView2에 연결했습니다. 최신 패키지에서 실제 Windows 입력 1,838건으로 검색·키보드·컷/플레이리스트 drag·PList 모달과 화면별 버튼, 개발 DB create/save→fresh readback→delete를 검증했고 cleanup까지 확인했습니다. PList 2,213행은 모두 목록과 scene alias를 복원하며, 그중 1,617행은 fresh typed selection까지 증명됐고 596행은 식별정보 부족으로 추정 없이 보류합니다. `5077` 6행 20페이지 전체, 마지막 페이지, `5088` 12행 전환, 장기 실행 뒤 unload와 `s6001` 영상 비의존 4 action도 실제 PGM에서 확인했습니다. 고객 배포용 서명 MSIX, 외부 영상·배경 자산, 보류 596행의 DBA 결정은 아직 남아 있습니다.
@@ -71,7 +71,7 @@ Oracle/MariaDB 조회 계층, 원본 10초 DB 상태 감시, Tornado/K3D 어댑
새로 받은 clone에 아직 이 PC의 검증된 runtime 연결이 없으면, 주변에 과거
`MBN_STOCK_N\...\bin\Debug`가 있더라도 첫 F5는 안전한
**소스 전용 첫 실행 설정 앱**으로 빌드됩니다. 이 첫 프로세스는 과거 로컬 송출 프로필·환경
-변수·Development Live 실행 인수를 무시하고 DB 미연결과 DryRun을 강제하므로 설정 중에는
+변수·Development Live 실행 인수를 무시하고 DB와 송출 엔진을 만들지 않으므로 설정 중에는
Tornado2/PGM에 연결하거나 명령을 보내지 않습니다. 출력과 중간 패키징 파일도 이전 전체
빌드의 `Cuts`, `Res` 또는 DB 설정을 잘못 재사용하지 않도록 `bin\SourceOnly`와
`obj\SourceOnly`에 분리됩니다.
@@ -96,19 +96,19 @@ Tornado2/PGM에 연결하거나 명령을 보내지 않습니다. 출력과 중
실행하지 않으므로 즉시 완료되어야 합니다. 전체 앱 빌드는 창을 닫고 누르는 다음 F5에서
진행됩니다. 이때도 `Cuts`는 복사하지 않고 선택한 원래 경로를 직접 사용합니다.
-Development Live용 DB overlay, K3D pin과 한 회차 승인 파일이 아직 없는 PC는 별도의
-`Initialize-ExistingDevelopmentPc.ps1` 수동 절차를 완료하기 전까지 안전한 `DryRun`으로
-시작합니다. 첫 실행 폴더 저장은 이 보호 설정을 만들거나 변경하지 않습니다.
+Development Live용 DB overlay, K3D pin과 승인 파일이 아직 없는 PC는 별도의
+`Initialize-ExistingDevelopmentPc.ps1` 수동 절차를 완료하기 전까지 전체 앱 기동이
+차단됩니다. 첫 실행 폴더 저장은 이 보호 설정을 만들거나 변경하지 않습니다.
초기화가 성공하면 Visual Studio에서 `Debug|x64`, `Legacy Parity App (VS F5)`,
`MBN_STOCK_WEBVIEW.LegacyParityApp - Development Live (Package)`를 선택해 F5로 시작합니다.
-앱이나 빌드 출력이 `SourceOnly` 또는 `DryRun`이라고 표시되면 송출을 시도하지 말고 별도
+앱이나 빌드 출력이 `SourceOnly` 또는 Live 기동 차단이라고 표시되면 송출을 시도하지 말고
Development Live 준비 상태를 확인합니다. 최초 실제 확인은 [개발 PGM 인수 절차](docs/DEVELOPMENT_LIVE_HANDOFF.md)에 따라
-`5001` 하나로 진행합니다. Development Live는 이 Debug 프로세스에만 적용되며 Release와 기본
-프로필은 계속 `DryRun`입니다. 이 프로필은 보호된 LocalAppData 설정만 읽고 상속된
+`5001` 하나로 진행합니다. Debug와 Release의 일반 실행은 모두 같은 보호된 Live
+부트스트랩을 사용합니다. 이 프로필은 보호된 LocalAppData 설정만 읽고 상속된
`MBN_STOCK_PLAYOUT_*`·DB 환경 변수를 무시합니다. scene root는 첫 실행에서 저장한 원래
`Cuts` 경로로 고정하며 strict
-검증이 실패하면 Live를 승인하지 않고 안전한 DryRun으로 시작합니다.
+검증이 실패하면 Live 엔진을 만들지 않고 명확한 기동 오류를 표시합니다.
같은 clone에서는 로컬 연결 파일이 Git에 덮어써지지 않으므로 이후에는 `git pull` 후 F5로
확인할 수 있습니다. 저장소를 새 폴더에 다시 clone하거나 자산·DB 폴더 연결이 바뀌면 첫 실행
@@ -140,20 +140,20 @@ dotnet restore MBN_STOCK_WEBVIEW.sln -p:Platform=x64
dotnet build MBN_STOCK_WEBVIEW.sln -c Debug -p:Platform=x64
```
-K3D 등록 상태와 COM을 열지 않는 dry-run 송출 흐름 확인:
+K3D 등록 상태를 COM 활성화 없이 확인:
```powershell
powershell -NoProfile -ExecutionPolicy Bypass `
-File .\scripts\Inspect-K3DRegistration.ps1
dotnet run --project .\tools\MBN_STOCK_WEBVIEW.PlayoutSmoke `
- -c Debug -p:Platform=x64 -- --dry-run
+ -c Debug -p:Platform=x64 -- --probe
```
격리 Tornado TEST의 프로세스·Network Server 포트·loopback 주소·LISTEN 소유권은 COM을 열기 전에 [읽기 전용 점검 스크립트](scripts/Inspect-TornadoTestIsolation.ps1)로 확인합니다. 설치 폴더의 다른 Tornado2 버전 EXE는 현재 사용자 설정과 출력 장치를 공유할 수 있으므로 PGM 옆에서 TEST 대용으로 실행하지 않습니다.
별도 Test 인스턴스 검증은 [Tornado/K3D 운영 가이드](docs/PLAYOUT.md)의 단계별 CLI를 사용합니다. `--test-plan`은 절대 경로 로컬 JSON과 씬 자산만 확인하며 엔진/COM을 만들지 않습니다. PGM/PROGRAM이 전혀 없는 격리 출력에서만 `--test-connect`로 연결·해제를 확인한 뒤 `--test-sequence`로 `PREPARE → TAKE IN → NEXT → TAKE OUT`을 실행합니다. Test 명령은 `MBN_STOCK_PLAYOUT_*` 환경 override와 Live 설정을 거부하고 자동 재연결·재생을 하지 않습니다. 승인된 Test 씬 후보는 basename `5001`, `5006`이며 실제 자산 경로는 저장소에 기록하지 않습니다.
-중요: 앱의 기본 `DryRun`, `--probe`, `--dry-run`, `--test-plan`은 `KTAPConnect`를 호출하지 않습니다. 따라서 이 단계에서 Tornado2의 `View > Network Monitoring Window`에 앱이 보낸 통신 기록이 없는 것은 정상입니다. 이 CLI 절차에서는 모든 안전 게이트를 통과한 격리 `Test`의 `--test-connect`에서 처음 네트워크 기록을 기대하며, 별도로 승인된 UI Test 실행도 기록을 만들 수 있습니다. 사용할 포트는 예제 숫자가 아니라 해당 격리 Test Tornado의 `Tools > Option > Control > Network Server > TCP Port` 실값이어야 합니다.
+중요: `--probe`와 `--test-plan`은 `KTAPConnect`를 호출하지 않습니다. 정상 앱 실행은 보호된 `Live`이며 자동으로 비송출 모드로 전환되지 않습니다. CLI 절차에서는 모든 안전 게이트를 통과한 격리 `Test`의 `--test-connect`에서 처음 네트워크 기록을 기대하며, 별도로 승인된 UI Test 실행도 기록을 만들 수 있습니다. 사용할 포트는 예제 숫자가 아니라 해당 격리 Test Tornado의 `Tools > Option > Control > Network Server > TCP Port` 실값이어야 합니다.
Tornado2의 `PGM` 창은 본 프로그램의 KTAP 명령을 렌더링하는 출력 창입니다. 현재 PGM과의 네트워크 왕복만 진단할 때는 일반 Test/Live 엔진을 완화하지 않고 `--pgm-connect-diagnostic`을 사용합니다. 이 명령은 Registry64에서 계산한 x64 K3D 네이티브 DLL과 Interop이 운영자 승인 SHA-256 핀 `MBN_STOCK_K3D_NATIVE_SHA256`/`MBN_STOCK_K3D_INTEROP_SHA256`과 각각 일치할 때만 동적으로 사용하며 `KTAPConnect → Disconnect`만 한 번 수행합니다. Scene player, 장면 로드, PREPARE, PLAY, STOP 계열 API 표면은 포함하지 않으며 해시 승인 절차와 판정 방법은 [Tornado/K3D 운영 가이드](docs/PLAYOUT.md)에 있습니다.
diff --git a/docs/DEVELOPMENT_ENVIRONMENT_AUTHORIZATION.md b/docs/DEVELOPMENT_ENVIRONMENT_AUTHORIZATION.md
index 4889b92..4f1d392 100644
--- a/docs/DEVELOPMENT_ENVIRONMENT_AUTHORIZATION.md
+++ b/docs/DEVELOPMENT_ENVIRONMENT_AUTHORIZATION.md
@@ -10,8 +10,8 @@ PGM이 개발 목적이며 방송에 영향을 주지 않는 환경임을 확인
상시 허용 범위는 개발 앱의 실행·정상 종료, CONNECT/DISCONNECT, PREPARE, TAKE IN,
NEXT, Page NEXT, TAKE OUT, Network Monitoring/PGM 확인, 그리고 해당 검증에 필요한
-개발 DB 읽기·쓰기다. Debug의 Git 제외 로컬 development-live 설정과 고정된 K3D DLL
-해시를 사용한다. Release와 일반 설치본의 기본 `DryRun`은 그대로 유지한다.
+개발 DB 읽기·쓰기다. Debug와 Release 모두 Git 제외 로컬 development-live 설정과 고정된
+K3D DLL 해시를 사용해 `Live`로만 기동한다. 검증 실패 시 `DryRun`으로 전환하지 않는다.
## 기술적 중단 조건
diff --git a/docs/DEVELOPMENT_LIVE_HANDOFF.md b/docs/DEVELOPMENT_LIVE_HANDOFF.md
index 0a7a51c..02ab674 100644
--- a/docs/DEVELOPMENT_LIVE_HANDOFF.md
+++ b/docs/DEVELOPMENT_LIVE_HANDOFF.md
@@ -8,9 +8,8 @@
확인할 수 없으면 시작하지 말고 [운영·비지정 환경 승인 게이트](PLAYOUT_OPERATIONS.md#운영비지정-환경-승인-게이트)를
적용한다.
-이 절차가 여는 Live 경로는 `Debug|x64`의
-`MBN_STOCK_WEBVIEW.LegacyParityApp - Development Live (Package)` 한 프로세스에만
-유효하다. `Release`와 일반 설치본의 기본 모드는 계속 `DryRun`이다. Release를 Live로 바꾸거나
+이 절차가 여는 Live 경로는 지정 개발 PC의 `Debug|x64`와 `Release|x64`
+`MBN_STOCK_WEBVIEW.LegacyParityApp - Development Live (Package)`에 유효하다.
사용자·시스템 환경 변수에 Live 승인을 영구 저장하지 않는다.
## Git 밖에서 확인하거나 준비할 항목
@@ -57,8 +56,8 @@ PowerShell 명령은 필요 없다.
1. 저장소를 clone하고 `MBN_STOCK_WEBVIEW.sln`을 Visual Studio 2026에서 연다.
2. `Debug|x64`, `Legacy Parity App (VS F5)`,
`MBN_STOCK_WEBVIEW.LegacyParityApp - Development Live (Package)`를 선택한다.
-3. 첫 번째 F5로 소스 전용 첫 실행 설정 앱을 연다. 이 프로세스는 DB 미연결과 DryRun을
- 강제하고 Tornado2/PGM에 연결하지 않는다.
+3. 첫 번째 F5로 소스 전용 첫 실행 설정 앱을 연다. 이 프로세스는 DB와 송출 엔진을
+ 만들지 않고 Tornado2/PGM에 연결하지 않는다.
4. `Cuts 폴더 선택`에서 실제 실행 자산의 `Cuts` 폴더를 선택한다.
5. `Res 폴더 선택`에서 기존 코더가 사용하는 실제 `Res` 폴더를 선택한다.
6. 두 폴더가 같은 공통 부모 아래의 정확한 `Cuts`, `Res`인지 확인하고 `설정 시작`을 누른다.
@@ -85,14 +84,14 @@ dotnet build .\src\MBN_STOCK_WEBVIEW.LegacyParityApp\MBN_STOCK_WEBVIEW.LegacyPar
```
출력에 외부 runtime 경로가 활성화되었다는 메시지가 있어야 한다. 소스 전용 모드가 계속
-표시되면 Visual Studio에서 다시 빌드한다. `DryRun`은 경로 저장 실패가 아니라 별도의
-Development Live 승인이 아직 없다는 뜻일 수 있다.
+표시되면 Visual Studio에서 다시 빌드한다. Live 기동이 차단되면 별도의 Development Live
+승인이나 보호 설정이 아직 유효하지 않은 것이다.
같은 clone에서는 이후 `git pull` 뒤 Visual Studio 2026에서 `Debug|x64`, 시작 대상
`Legacy Parity App (VS F5)`, 실행 프로필
`MBN_STOCK_WEBVIEW.LegacyParityApp - Development Live (Package)`를 선택해 F5로 시작할 수
있다. 저장소를 새 폴더에 다시 clone하면 첫 실행 경로 저장을 다시 진행한다. DB overlay,
K3D pin과 Development Live 승인이 없는 새 PC에서는 아래 수동 초기화 절차를 별도로
-완료하기 전까지 안전한 `DryRun`을 유지한다. K3D DLL 변경은 경로 저장을 반복해도 자동
+완료하기 전까지 전체 앱 기동을 차단한다. K3D DLL 변경은 경로 저장을 반복해도 자동
승인되지 않는다. 최초 실제 동작
확인은 아래 최소 인수 시퀀스의 `5001` 하나로 제한한다.
@@ -341,7 +340,7 @@ powershell -NoProfile -ExecutionPolicy Bypass `
| 항목 | 인수 값 |
|---|---|
-| `mode` | 항상 `DryRun`. Development Live 프로세스만 일시적으로 Live로 덮어쓴다. |
+| `mode` | 항상 `Live`. Debug와 Release가 같은 보호된 프로필을 사용한다. |
| `host` | 로컬 PGM이면 숫자형 loopback `127.0.0.1`. `localhost` 또는 원격 주소를 추정하지 않는다. |
| `port` | 지정 개발 환경의 고정 Network Server TCP port `30001` |
| `tcpMode` / `clientPort` | `1` / `0` |
@@ -369,7 +368,7 @@ Development Live로 승인된 프로세스는 이 26개 속성을 strict JSON으
scene root는 실행 파일 옆 검증된 `Cuts`로 마지막에 다시 고정한다. DB도
`%LOCALAPPDATA%\MBN_STOCK_WEBVIEW\Res\MmoneyCoder.ini`를 우선하고, 없을 때만 보호된
`Config\database.local.json`을 사용하며 DB 환경 변수는 무시한다. 이 strict 경로가 성립하지
-않으면 Live로 진행하지 않고 안전한 DryRun으로 시작한다.
+않으면 Live 엔진 기동을 차단하고 명확한 오류를 표시한다.
### `playout.development-live.local.json`
@@ -427,12 +426,12 @@ timeout, `OutcomeUnknown`, 대상 PID·시작 시각·listener 변경 또는 결
## 8. Development Live 최소 인수 시퀀스
1. 기존 앱 인스턴스를 정상 종료한다.
-2. Visual Studio에서 구성 `Debug`, 플랫폼 `x64`, 시작 대상
+2. Visual Studio에서 구성 `Debug` 또는 `Release`, 플랫폼 `x64`, 시작 대상
`Legacy Parity App (VS F5)`를 선택한다.
3. 실행 프로필을
`MBN_STOCK_WEBVIEW.LegacyParityApp - Development Live (Package)`로 선택하고 F5로 시작한다.
-4. 앱이 소스 전용 또는 DryRun으로 시작했다면 실제 명령을 시도하지 않는다. runtime 초기화와
- 로컬 binding, 시작 인자와 두 로컬 파일을 고친 뒤 앱을 새로 시작한다.
+4. 앱이 소스 전용이거나 Live 기동 차단 오류를 표시하면 실제 명령을 시도하지 않는다.
+ runtime 초기화와 로컬 binding, 시작 인자와 두 로컬 파일을 고친 뒤 앱을 새로 시작한다.
5. 최초 인수는 자산이 완전한 사전 합의 scene 하나만 사용한다. 기본 최소 회차는 active alias
`5001`의 `CONNECT 1회 → PREPARE 1회 → 화면·Network Monitoring 확인 → TAKE IN 1회
→ 화면 확인 → TAKE OUT 1회 → DISCONNECT 1회`다.
@@ -445,13 +444,11 @@ timeout, `OutcomeUnknown`, 대상 PID·시작 시각·listener 변경 또는 결
allowlist 실패가 발생하면 다음 동작, 같은 동작 반복, 반대 동작이나 추측성 cleanup을 하지 않는다.
PGM/Tornado2를 강제 종료하지 말고 상태를 보존한다.
-## 9. DryRun 복귀와 인수 완료 조건
+## 9. Live 종료와 인수 완료 조건
Live 앱을 종료하면 부트스트랩이 만든 Live 환경은 프로세스와 함께 사라져야 한다.
-`playout.local.json`의 `mode`는 계속 `DryRun`이어야 한다. 다음으로
-`MBN_STOCK_WEBVIEW.LegacyParityApp - Explicit DryRun (Package)` 프로필을 실행해 실제 KTAP
-통신 없이 UI가 시작되는지 확인한다. Release 검증도 이 DryRun 프로필로만 수행하며
-`--development-live`를 Release Live 수단으로 사용하지 않는다.
+`playout.local.json`의 `mode`는 계속 `Live`여야 한다. 다음 실행도 같은 보호된 Live
+부트스트랩을 사용하며, 별도의 비송출 프로필로 전환하지 않는다.
다음 항목이 모두 맞아야 인수를 완료한다.
@@ -461,7 +458,7 @@ Live 앱을 종료하면 부트스트랩이 만든 Live 환경은 프로세스
- 개발 DB read-only smoke 성공
- 지속 K3D pin과 두 `%LOCALAPPDATA%` 송출 설정 파일의 내용·ACL 검증
- connect-only 진단과 최소 5001 회차가 retry 0, `OutcomeUnknown=false`로 종료
-- 최종 TAKE OUT/IDLE, DISCONNECT와 Explicit DryRun 재시작 확인
+- 최종 TAKE OUT/IDLE, DISCONNECT와 정상 Live 재시작 확인
- 누락 영상 14개 관련 action이 제한 상태임을 인수자에게 고지
실제 endpoint, 자격증명, 해시, 자산 경로, 화면 캡처와 Network Monitoring 증거는 승인된
diff --git a/docs/DEVELOPMENT_LIVE_VISUAL_STUDIO.md b/docs/DEVELOPMENT_LIVE_VISUAL_STUDIO.md
index 4bbf3d9..da1922f 100644
--- a/docs/DEVELOPMENT_LIVE_VISUAL_STUDIO.md
+++ b/docs/DEVELOPMENT_LIVE_VISUAL_STUDIO.md
@@ -11,16 +11,16 @@
이 문서는 개발 PC의 Visual Studio 2026에서 `MBN_STOCK_WEBVIEW.LegacyParityApp`을
명확한 시작 대상으로 실행하고, 개발 Tornado2에 연결하는 방법을 설명한다.
-저장소의 송출 기본값과 Release 동작은 계속 `DryRun`이다. 개발 Live는 다음 세 조건이
-동시에 맞는 **Debug 프로세스 한 회차**에서만 기존 Live 게이트를 설정한다.
+지정 개발 PC의 Debug와 Release 전체 앱은 모두 보호된 `Live`로만 기동한다. 개발 Live는
+다음 조건이 동시에 맞을 때 기존 Live 게이트를 설정한다.
-1. 시작 인자가 정확히 `--development-live` 하나이다.
-2. 컴파일된 앱이 Debug 빌드이다.
+1. 정상 무인자 실행이거나 시작 인자가 정확히 `--development-live` 하나이다.
+2. 소스 전용 설정 빌드가 아닌 전체 runtime 빌드이다.
3. 아래 Git 제외 로컬 승인 파일이 존재하며 엄격한 스키마 검사를 통과한다.
조건이 하나라도 다르면 환경 변수를 설정하지 않는다. 로컬 파일이 없거나 읽기 권한이
-없고, JSON 또는 해시 형식이 잘못된 경우에도 시작은 fail-closed로 진행되어 기존
-`playout.local.json`의 안전 기본인 `DryRun`을 사용한다.
+없고, JSON 또는 해시 형식이 잘못된 경우에는 전체 앱의 Live 엔진 기동을 차단하고
+명확한 오류를 표시한다. `DryRun`으로 폴백하지 않는다.
새 clone처럼 아직 검증된 runtime 연결이 없는 경우의 첫 F5는 예외적으로 소스 전용 설정
앱을 연다. 이 프로세스는 `--development-live`를 적용하지 않고 DB와 송출을 모두 차단한다.
@@ -29,7 +29,7 @@
`dotnet build`, DB 복사, K3D 검사와 Live 승인 발급도 실행하지 않으므로 즉시 완료되어야
한다. 설정을 완료한 뒤 창을 닫고 F5를 한 번 더 누르면 전체 앱 빌드가 시작된다.
-기존 유효한 Live 승인 파일이 없는 PC는 두 번째 F5에서도 안전한 `DryRun`을 유지한다.
+기존 유효한 Live 승인 파일이 없는 PC는 두 번째 F5에서 전체 앱 기동이 차단된다.
DB overlay, K3D pin과 Development Live 승인이 필요하면 아래 수동 초기화 절차를 별도로
완료한다. 자세한 절차는
[개발 PGM 인수 절차](DEVELOPMENT_LIVE_HANDOFF.md#2-기존-자산-보유-pc-clone-후-1회-초기화)를
@@ -50,24 +50,22 @@ src\MBN_STOCK_WEBVIEW.LegacyParityApp\MBN_STOCK_WEBVIEW.LegacyParityApp.csproj
```
`Debug|x64`와 `Release|x64`의 MSIX Deploy 대상도 루트 프로토타입이 아니라 이 프로젝트이다. Visual
-Studio의 시작 드롭다운에는 다음 두 프로필이 표시된다.
+Studio의 시작 드롭다운에는 다음 프로필 하나만 표시된다.
| 순서 | 프로필 | 동작 |
|---:|---|---|
| 1 | `MBN_STOCK_WEBVIEW.LegacyParityApp - Development Live (Package)` | `--development-live`를 전달한다. 유효한 로컬 승인 파일이 있을 때만 Live를 구성한다. |
-| 2 | `MBN_STOCK_WEBVIEW.LegacyParityApp - Explicit DryRun (Package)` | 개발 Live 인자를 전달하지 않는다. |
-개발 PC에서 Live를 사용할 때는 공유 실행 프로필 `Legacy Parity App (VS F5)`, 구성
-`Debug`, 플랫폼 `x64`, 첫 번째 Development Live 패키지 프로필을 선택하고 F5를 누른다.
-DryRun 확인은 두 번째 프로필을 선택한다. 앱은 단일 인스턴스이므로 프로필을 바꿀 때
-기존 인스턴스를 정상 종료한 뒤 다시 실행해야 한다.
+개발 PC에서는 공유 실행 프로필 `Legacy Parity App (VS F5)`, 구성 `Debug` 또는 `Release`,
+플랫폼 `x64`, Development Live 패키지 프로필을 선택하고 F5를 누른다. 앱은 단일
+인스턴스이므로 구성을 바꿀 때 기존 인스턴스를 정상 종료한 뒤 다시 실행해야 한다.
패키지된 WinUI 3 full-trust 실행에서는 Visual Studio가 프로세스 명령줄에 인자를 전달해도
`LaunchActivatedEventArgs.Arguments`가 빈 문자열일 수 있다. 부트스트랩은 activation 인자가
-비어 있을 때에만 `Environment.GetCommandLineArgs()`가 정확히 `exe + 인자 1개`인 경우 그
-하나를 사용한다. 인자가 없거나 둘 이상이면 합치거나 일부를 무시하지 않고 Live 요청을
-거부한다. Release 빌드는 이 fallback으로 정확한 인자를 얻더라도 Debug-only gate에서
-승인 파일을 읽지 않고 기존 `DryRun` 설정을 유지한다.
+비어 있을 때에만 `Environment.GetCommandLineArgs()`를 확인한다. 인자가 없으면 정상 Live
+실행으로 처리하고, 정확히 `exe + --development-live`이면 호환 Live 실행으로 처리한다.
+추가 인자가 있으면 합치거나 일부를 무시하지 않고 기동을 거부한다. Release도 Debug와
+동일한 보호된 승인 파일과 검증을 사용한다.
## 로컬 승인 파일
@@ -137,8 +135,8 @@ DryRun 확인은 두 번째 프로필을 선택한다. 앱은 단일 인스턴
%LOCALAPPDATA%\MBN_STOCK_WEBVIEW\Config\playout.local.json
```
-이 파일의 `mode`는 `DryRun`으로 유지한다. Development Live 부트스트랩이 성공한 해당
-Debug 프로세스에서만 `MBN_STOCK_PLAYOUT_MODE=Live`를 process scope로 덮어쓴다.
+이 파일의 `mode`는 `Live`로 유지한다. Development Live 부트스트랩은 Debug와 Release
+프로세스에서 검증된 Live authorization과 K3D 해시를 process scope로 적용한다.
기존 이중 게이트를 유지하기 위해 로컬 설정에는 `trustedLiveOutputEnabled: true`가
명시되어야 한다.
@@ -170,6 +168,5 @@ Debug 프로세스에서만 `MBN_STOCK_PLAYOUT_MODE=Live`를 process scope로
`EnvironmentLiveAuthorization`은 `MainWindow`와 `IPlayoutEngine`을 만들 때 값을 한 번만
캡처하므로, 부트스트랩은 반드시 `new MainWindow()`보다 먼저 실행된다.
-Release 빌드에서는 `--development-live`가 전달되어도 로컬 승인 파일을 읽지 않고 환경을
-변경하지 않는다. 이 개발 편의 경로는 기존 운영 Live 승인 경로나 안전 게이트를 대체하지
-않는다.
+Release 빌드도 Debug와 같은 로컬 승인 파일을 읽고 같은 안전 게이트를 적용한다. 이는
+운영/방송 환경의 별도 승인 절차를 대체하지 않는다.
diff --git a/docs/PLAYOUT.md b/docs/PLAYOUT.md
index d7a574f..2514843 100644
--- a/docs/PLAYOUT.md
+++ b/docs/PLAYOUT.md
@@ -9,11 +9,11 @@
## 안전 기준
-기본 모드는 `DryRun`입니다. `DryRun`은 COM 객체를 만들거나 Tornado 출력에 명령을 보내지 않고 WebView 메시지, 큐, 상태 및 오류 표시 흐름만 검증합니다. 현재 방송 PROGRAM(PGM)에 연결된 Tornado 프로세스는 항상 **안전하지 않은 대상**으로 취급합니다. 프로세스 이름이나 실행 여부만으로 테스트 대상이라고 판단하지 않습니다.
+지정 개발 PC의 기본 모드는 보호된 `Live`입니다. 승인 파일·K3D 해시·scene/path allowlist 같은 불변식 검증에 실패하면 Live 엔진 기동을 차단하며 비송출 모드로 폴백하지 않습니다. K3D/PGM/DB가 일시적으로 준비되지 않은 경우에는 Live 엔진을 `Disconnected` 또는 `Faulted`로 유지합니다. 현재 방송 PROGRAM(PGM)에 연결된 Tornado 프로세스는 항상 **안전하지 않은 대상**으로 취급합니다. 프로세스 이름이나 실행 여부만으로 테스트 대상이라고 판단하지 않습니다.
운영/방송 대상의 작업과 자동 검증은 실제 PGM 출력 또는 라이브 `TAKE IN`을 자동 허가하지 않습니다. `Test` 검증은 PGM과 분리된 전용 테스트 인스턴스, 테스트 출력 채널 및 허용 목록에 든 테스트 씬을 모두 확인한 뒤에만 수행합니다. 단, 위에 링크한 현재 개발 PC의 상시 권한 범위에서는 별도의 회차별 허가를 다시 요청하지 않습니다.
-이 문서의 adapter·builder·DryRun 완료 표현은 화면/계약 구현 범위다. 운영 DB-W, 외부 asset readiness, 기존 데이터 복원과 장면별 실제 PGM은 별도 증거가 있어야 한다. 현재 미적용·적용 중·외부자산 필요 항목은 [`LEGACY_FEATURE_AUDIT.md`](LEGACY_FEATURE_AUDIT.md)를 따른다.
+이 문서의 과거 adapter·builder·DryRun 완료 표현은 당시 화면/계약 구현 기록이다. 운영 DB-W, 외부 asset readiness, 기존 데이터 복원과 장면별 실제 PGM은 별도 증거가 있어야 한다. 현재 미적용·적용 중·외부자산 필요 항목은 [`LEGACY_FEATURE_AUDIT.md`](LEGACY_FEATURE_AUDIT.md)를 따른다.
## 확인된 x64 COM 등록
@@ -93,9 +93,9 @@ SDK가 기본 위치에 없다면 x64 SDK의 `TlbImp.exe` 절대 경로를 `-Tlb
%LOCALAPPDATA%\MBN_STOCK_WEBVIEW\Config\runtime-folders.local.json
```
-[playout.example.json](../Config/playout.example.json)을 `playout.local.json`의 구조 참고용으로 사용합니다. 예시는 `DryRun`, 출력 채널 미지정, 빈 씬 허용 목록, 라이브 신뢰 플래그 해제 상태이므로 실제 출력에 사용할 수 없습니다. `runtime-folders.local.json`은 좌측 `설정` 메뉴에서 네이티브 폴더 선택 창으로 지정한 디자인(`Cuts`), 설정(`Res`), 운영 배경 폴더와 왼쪽 메뉴 시작 상태를 앱이 자동 저장하는 파일이므로 직접 편집하지 않습니다. 폴더 변경은 앱을 다시 시작한 뒤 자산·카탈로그에 적용되며, 메뉴 펼침 상태는 즉시 적용됩니다.
+[playout.example.json](../Config/playout.example.json)을 `playout.local.json`의 구조 참고용으로 사용합니다. 예시는 `Live` 형식이지만 출력 채널 미지정, 빈 씬 허용 목록, 라이브 신뢰 플래그 해제 상태이므로 실제 출력에 사용할 수 없습니다. 실제 개발 PC 설정은 보호된 초기화 스크립트로만 생성합니다. `runtime-folders.local.json`은 좌측 `설정` 메뉴에서 네이티브 폴더 선택 창으로 지정한 디자인(`Cuts`), 설정(`Res`), 운영 배경 폴더와 왼쪽 메뉴 시작 상태를 앱이 자동 저장하는 파일이므로 직접 편집하지 않습니다. 폴더 변경은 앱을 다시 시작한 뒤 자산·카탈로그에 적용되며, 메뉴 펼침 상태는 즉시 적용됩니다.
-송출 장면 루트는 환경 변수, 사용자 설정 메뉴, `playout.local.json`, 실행 파일 옆 기본 `Cuts` 순으로 우선합니다. 운영 배경 루트도 환경 변수, 사용자 설정 메뉴, `playout.local.json` 순으로 우선하며 모두 비어 있으면 최종 장면 루트의 sibling `배경` 폴더를 사용합니다. 사용자 설정 메뉴가 덮어쓰는 송출 값은 이 두 자산 루트뿐입니다. Release/default `DryRun`, KTAP 호스트·포트·채널, Test/Live 게이트, allowlist와 벤더 해시는 이 화면에서 편집할 수 없고 기존 검사를 그대로 통과해야 합니다. 별도 씬 루트나 보호 설정을 명시할 때는 테스트 장비의 호스트, 채널, 창 제목 패턴과 허용할 씬 이름을 로컬 파일에만 기록하고 Git, 로그 또는 지원 첨부파일에 넣지 않습니다.
+송출 장면 루트는 환경 변수, 사용자 설정 메뉴, `playout.local.json`, 실행 파일 옆 기본 `Cuts` 순으로 우선합니다. 운영 배경 루트도 환경 변수, 사용자 설정 메뉴, `playout.local.json` 순으로 우선하며 모두 비어 있으면 최종 장면 루트의 sibling `배경` 폴더를 사용합니다. 사용자 설정 메뉴가 덮어쓰는 송출 값은 이 두 자산 루트뿐입니다. 보호된 `Live` 모드, KTAP 호스트·포트·채널, Test/Live 게이트, allowlist와 벤더 해시는 이 화면에서 편집할 수 없고 기존 검사를 그대로 통과해야 합니다. 별도 씬 루트나 보호 설정을 명시할 때는 테스트 장비의 호스트, 채널, 창 제목 패턴과 허용할 씬 이름을 로컬 파일에만 기록하고 Git, 로그 또는 지원 첨부파일에 넣지 않습니다.
| 속성 | 의미 |
|---|---|
@@ -151,9 +151,9 @@ MBN_STOCK_PLAYOUT_RECONNECT_ENABLED
### KTAP 포트와 Network Monitoring 판정
-K3DAsyncEngine 매뉴얼의 `KTAPConnect(bTCP, HostAddress, nHostPort, nClientPort, handler)`에서 Test/Live는 `tcpMode: 1`(TCP)만 허용하므로 로컬 JSON의 `port`는 격리 Test Tornado의 `Tools > Option > Control > Network Server > TCP Port`와 정확히 같아야 합니다. `clientPort`는 UDP일 때만 의미가 있고 `TAP TCP Port`/`TAP UDP Port`는 이 연결의 host port가 아닙니다. 매뉴얼 18쪽의 설명과 26쪽 및 192쪽 예시에 표시된 포트 숫자가 서로 다르므로 `30001`/`30002`를 추정하거나 원본·예제 값을 복사하지 않습니다. 해당 회차 Test 인스턴스 화면의 설정값이 유일한 기준입니다. `playout.example.json`의 `30001`도 DryRun 구조 예시일 뿐 새 Test endpoint의 검증값이 아닙니다.
+K3DAsyncEngine 매뉴얼의 `KTAPConnect(bTCP, HostAddress, nHostPort, nClientPort, handler)`에서 Test/Live는 `tcpMode: 1`(TCP)만 허용하므로 로컬 JSON의 `port`는 격리 Test Tornado의 `Tools > Option > Control > Network Server > TCP Port`와 정확히 같아야 합니다. `clientPort`는 UDP일 때만 의미가 있고 `TAP TCP Port`/`TAP UDP Port`는 이 연결의 host port가 아닙니다. 매뉴얼 18쪽의 설명과 26쪽 및 192쪽 예시에 표시된 포트 숫자가 서로 다르므로 `30001`/`30002`를 추정하거나 원본·예제 값을 복사하지 않습니다. 해당 회차 Test 인스턴스 화면의 설정값이 유일한 기준입니다. `playout.example.json`의 `30001`도 구조 예시일 뿐 새 Test endpoint의 검증값이 아닙니다.
-Tornado2의 `View > Network Monitoring Window`에서 `[R]`은 서버가 클라이언트 요청을 받은 기록, `[S]`는 서버가 응답을 보낸 기록이며 `TCPSession`은 TCP 세션 수입니다(매뉴얼 26~27쪽). 프로세스 감지, COM 등록 probe 또는 COM 객체 생성만으로는 이 기록이 생기지 않습니다. 기본 앱과 `--dry-run`, `--probe`, `--test-plan`은 KTAP를 호출하지 않으므로 빈 모니터가 정상입니다.
+Tornado2의 `View > Network Monitoring Window`에서 `[R]`은 서버가 클라이언트 요청을 받은 기록, `[S]`는 서버가 응답을 보낸 기록이며 `TCPSession`은 TCP 세션 수입니다(매뉴얼 26~27쪽). 프로세스 감지, COM 등록 probe 또는 COM 객체 생성만으로는 이 기록이 생기지 않습니다. `--probe`와 `--test-plan`은 KTAP를 호출하지 않으므로 빈 모니터가 정상입니다. 정상 앱 실행은 보호된 `Live`이므로 CONNECT 시 네트워크 기록을 기대합니다.
상태의 `accepted-unconfirmed`는 `KTAPConnect`가 SDK 성공값 `1`을 반환했다는 뜻일 뿐입니다. 매뉴얼 41쪽의 `OnHello` 콜백이나 실제 `[R]`/`[S]`를 자동 확인했다는 뜻이 아닙니다. connect-only `--pgm-connect-diagnostic`은 렌더 API 표면을 제거한 별도 binding이라 callback을 관찰하지 않고 `ktapHelloObserved`를 `null`로 보고합니다. 일반 `IPlayoutEngine` 경로는 검증된 282-method `DynamicK3dEventHandler`로 `OnHello`와 lifecycle callback을 수신하지만, callback 미수신 상태를 성공으로 추정하지 않으며 Network Monitoring은 계속 사람이 대조합니다. `lastKtapConnectState`는 현재 연결 상태가 아니라 가장 최근 KTAP dispatch 시도의 증거이며, 화면은 `Connected`/`Faulted` 같은 현재 상태와 분리해 표시합니다. 따라서 격리 `--test-connect`가 성공했는데도 같은 시각의 `[R]`/`[S]`가 전혀 없다면 `--test-sequence`로 진행하지 말고 mode/config 파일, 실제 Network Server TCP Port와 안전 게이트 거부 여부를 먼저 확인합니다.
@@ -257,7 +257,7 @@ Test에서 로컬 프로세스/창 제목 검사를 원격 KTAP endpoint의 신
## 안전한 스모크 절차
-먼저 `mode`가 `DryRun`이고 라이브 환경 변수가 없는지 확인합니다.
+먼저 보호된 Live 설정·승인 파일과 K3D 등록을 읽기 전용으로 확인합니다.
```powershell
Remove-Item Env:MBN_STOCK_PLAYOUT_AUTHORIZE_LIVE_OUTPUT -ErrorAction SilentlyContinue
@@ -265,10 +265,8 @@ powershell -NoProfile -ExecutionPolicy Bypass `
-File .\scripts\Inspect-K3DRegistration.ps1
dotnet run --project .\tools\MBN_STOCK_WEBVIEW.PlayoutSmoke `
-c Debug -p:Platform=x64 -- --probe
-dotnet run --project .\tools\MBN_STOCK_WEBVIEW.PlayoutSmoke `
- -c Debug -p:Platform=x64 -- --dry-run
-dotnet test .\MBN_STOCK_WEBVIEW.sln -c Debug -p:Platform=x64
-dotnet test .\MBN_STOCK_WEBVIEW.sln -c Release -p:Platform=x64
+dotnet build .\MBN_STOCK_WEBVIEW.sln -c Debug -p:Platform=x64
+dotnet build .\MBN_STOCK_WEBVIEW.sln -c Release -p:Platform=x64
powershell -NoProfile -ExecutionPolicy Bypass `
-File .\scripts\Test-WebPlayout.ps1
```
@@ -392,7 +390,7 @@ Network Monitoring 최종 증분은 HELLO 1/1, 5001/5074 LOAD 각각 1/1, 5001 P
Round H cleanup은 승인된 TAKE OUT 1회와 두 scene unload, disconnect, 앱·진단 listener 종료, 회차 전용 Live 설정과 승인 환경 제거, PGM/Network Monitoring 창 상태 복원으로 끝났습니다. 정상 회차라 추정 rollback은 실행하지 않았습니다. 장애 rollback은 기본 `DryRun`/`Disabled`로 복귀하거나 조직 절차로 직전 승인 패키지를 복원하는 범위이며, vendor DLL·COM 등록·라이선스·실제 자산은 수정하거나 저장소에 넣지 않습니다. 실제 Live PGM 검증 범위는 허용된 5001/5074뿐이고, 35개 scene 전체 완료 근거는 자동 테스트·55-query 실데이터 smoke·매트릭스입니다.
-패키지 스모크에서는 벤더 x64 COM이 장비에 정식 등록되어 있어야 합니다. MSIX에 벤더 DLL을 복사해 활성화 오류를 우회하지 않습니다. 패키지 컨텍스트에서 COM 활성화가 막히면 `DryRun` 또는 `Disabled`를 유지하고 HRESULT와 등록 검사 결과만 보고합니다.
+패키지 스모크에서는 벤더 x64 COM이 장비에 정식 등록되어 있어야 합니다. MSIX에 벤더 DLL을 복사해 활성화 오류를 우회하지 않습니다. 패키지 컨텍스트에서 COM 활성화가 막히면 Live 엔진을 `Faulted`/`Disconnected`로 유지하고 HRESULT와 등록 검사 결과만 보고합니다.
## 장애 및 롤백
diff --git a/docs/PLAYOUT_OPERATIONS.md b/docs/PLAYOUT_OPERATIONS.md
index 536464d..4796d12 100644
--- a/docs/PLAYOUT_OPERATIONS.md
+++ b/docs/PLAYOUT_OPERATIONS.md
@@ -14,7 +14,7 @@
| 항목 | 상태 |
|---|---|
-| 기본 모드 | `DryRun`; COM 객체와 `KTAPConnect`를 만들지 않음 |
+| 지정 개발 PC 기본 모드 | 보호된 `Live`; 승인·설정 불변식 실패 시 기동 차단, 자동 `DryRun` 폴백 없음 |
| 자동 테스트 | 최신 Release x64 .NET solution 3,182/3,182, LegacyParityWeb 150/150, WebPlayout JavaScript 424/424, 원본 scene 기준선 35/35와 staged PGM 정적 계약 통과; 실패·skip 0건 |
| 개발 MSIX UI/DB | 최신 등록 AppX에서 실제 Windows 입력 1,838건으로 UC1~UC7, GraphE, FSell, VIList, ThemeA, EList, PList 모달, 설정·비교 가져오기와 개발 DB 저장→fresh readback→delete/absence를 검증하고 cleanup 완료 |
| 고객 배포 MSIX | 현재 범위에서 보류. 고객용 version·Publisher·서명 인증서·업데이트와 새 PC 로컬 설정 배포는 사용자가 재개할 때 결정 |
@@ -174,8 +174,8 @@ SHA-256 `7A1141C5F276729E340227CEEAA48B0E6018632BBA51F43AA94636084AA3C301`이며
## 절대 안전 규칙
-1. 앱의 기본값은 항상 `DryRun`으로 유지한다.
-2. `DryRun`, `--probe`, `--dry-run`, `--test-plan`은 `KTAPConnect`를 호출하지 않는다. 이때 Tornado2 Network Monitoring에 기록이 없는 것이 정상이다.
+1. 지정 개발 PC의 앱은 보호된 `Live`로만 기동하고 `DryRun`으로 전환하지 않는다.
+2. `--probe`와 `--test-plan`은 `KTAPConnect`를 호출하지 않는다. 이때 Tornado2 Network Monitoring에 기록이 없는 것이 정상이다.
3. 실제 출력은 허용된 테스트 scene, 지정된 PGM, 지정된 회차 범위에서만 수행한다.
4. 운영·방송 또는 비지정 환경은 아래 Gate A/B의 회차별 승인을 적용한다. 현재 지정 개발 PC는 [개발 환경 상시 권한](DEVELOPMENT_ENVIRONMENT_AUTHORIZATION.md)의 범위·대상 확인·기술적 중단 조건을 적용하며 별도 대화 승인을 반복하지 않는다.
5. 회차 범위에 없는 NEXT, Page NEXT, TAKE OUT 이외 명령 또는 다른 scene으로 범위를 넓히지 않는다.
@@ -301,9 +301,9 @@ PREPARE 결과와 Network Monitoring 상태를 운영자가 확인한 후, TAKE
5. K3D Registry64, AMD64 PE, TypeLib/Interop metadata, 두 SHA-256 pin, license를 확인한다. DLL을 repo 또는 앱 폴더로 복사해 우회하지 않는다.
6. 정확히 하나의 승인 대상 Tornado2 프로세스, PGM 창, Network Server TCP port와 LISTEN 소유권을 확인한다. 매뉴얼 예시 port를 추정해 사용하지 않는다.
7. `s5025`를 선택했다면 trusted 외부 디렉터리와 파일 preflight가 성공해야 한다. `s5006`은 `Video\큐브배경.vrv`, `s6001` 해외지수는 선택 국가에 해당하는 `Video\20201008_<국가>.vrv`가 승인 asset root 안에 있어야 한다. coverage 결과의 `MissingAssets`, `RootEscapes`, `ReparseAssets` 중 하나라도 0이 아니면 해당 장면으로 진행하지 않는다.
-8. 실제 COM을 사용하는 Test/Live playlist의 모든 cut이 로컬 폐쇄형 allowlist 안에 있고 selector가 closed enum/lookup 규칙을 통과하는지 DryRun에서 확인한다. allowlist가 비어 있으면 실제 모드 초기화 자체를 거부해야 한다.
-9. 로컬 playout 설정의 `legacySceneFadeDuration` 기본값 6과 `legacySceneBackgroundKind`를 확인한다. 공통 background를 쓸 때 `legacySceneBackgroundAssetPath`는 `sceneDirectory` 아래의 승인된 상대 경로여야 한다. `PlayoutSceneCompositionFactory`의 DryRun preflight가 파일 존재, 허용 확장자, root 탈출, 절대 경로와 reparse point를 모두 거부하는지 확인한다. Web에는 이 asset 경로를 보내지 않는다.
-10. Web DryRun에서 45개 active alias, row별 `enabled`, PREPARE 뒤 snapshot freeze, current entry/builder/page size/current rows/last-page/preview와 refresh 상태를 확인한다. pending command와 `OutcomeUnknown`/timeout quarantine에서도 playlist 편집이 잠겨야 한다. preview에 image/texture/video 경로가 나타나면 실제 회차를 중단한다.
+8. 실제 COM을 사용하는 Test/Live playlist의 모든 cut이 로컬 폐쇄형 allowlist 안에 있고 selector가 closed enum/lookup 규칙을 통과하는지 정적 계약 테스트에서 확인한다. allowlist가 비어 있으면 실제 모드 초기화 자체를 거부해야 한다.
+9. 로컬 playout 설정의 `legacySceneFadeDuration` 기본값 6과 `legacySceneBackgroundKind`를 확인한다. 공통 background를 쓸 때 `legacySceneBackgroundAssetPath`는 `sceneDirectory` 아래의 승인된 상대 경로여야 한다. `PlayoutSceneCompositionFactory` preflight가 파일 존재, 허용 확장자, root 탈출, 절대 경로와 reparse point를 모두 거부하는지 확인한다. Web에는 이 asset 경로를 보내지 않는다.
+10. Web 계약 테스트에서 45개 active alias, row별 `enabled`, PREPARE 뒤 snapshot freeze, current entry/builder/page size/current rows/last-page/preview와 refresh 상태를 확인한다. pending command와 `OutcomeUnknown`/timeout quarantine에서도 playlist 편집이 잠겨야 한다. preview에 image/texture/video 경로가 나타나면 실제 회차를 중단한다.
하나라도 실패하면 회차를 시작하지 않는다. 설정을 수정한 뒤 처음부터 새 preflight 결과를 만든다.
@@ -396,7 +396,8 @@ vendor monitor의 실제 명령 표기는 버전에 따라 다를 수 있으므
| 상황 | 즉시 조치 | 재시도 조건 |
|---|---|---|
-| DB/selector/asset preflight 실패, COM 호출 전 명시적 `Rejected` | 회차 중단, DryRun으로 복귀, 원인 기록 | offline 수정과 전체 preflight 후 새 회차 승인 |
+| 승인/설정/allowlist 불변식 실패, COM 호출 전 명시적 `Rejected` | Live 엔진 기동 차단, 원인 기록 | offline 수정과 전체 preflight 후 새 실행 |
+| DB/K3D/PGM 일시 장애 | Live 엔진을 `Disconnected`/`Faulted`로 유지하고 명령 전송 중단 | 원인 수정 후 known state에서 명시적 CONNECT |
| Connect timeout 또는 `OnHello`/monitor 불일치 | 결과를 불명확으로 격리, Connect 반복 금지 | 운영자가 세션과 process/port 상태를 확인한 뒤 새 회차 승인 |
| PREPARE/TAKE IN/NEXT/refresh/TAKE OUT timeout | `OutcomeUnknown`으로 취급, 같은 명령과 반대 명령 자동 실행 금지 | PGM/monitor/콜백을 사람이 확인하고 출력 안전을 복구한 뒤 새 회차 승인 |
| `WEB_TIMEOUT` | strict timeout-quarantine message를 native에 전달. MainWindow가 process-lifetime latch를 먼저 세우고 vendor session을 quarantine; 늦은 응답, UI 재시도와 WebView reload로 해제 금지 | native status와 PGM을 사람이 대조하고 process를 새로 시작한 뒤 새 승인 |
diff --git a/scripts/Initialize-DevelopmentLiveConfig.ps1 b/scripts/Initialize-DevelopmentLiveConfig.ps1
index 1eb3e9a..f8ffd2b 100644
--- a/scripts/Initialize-DevelopmentLiveConfig.ps1
+++ b/scripts/Initialize-DevelopmentLiveConfig.ps1
@@ -180,7 +180,7 @@ Assert-WritableTarget $playoutPath
Assert-WritableTarget $authorizationPath
$playout = [ordered]@{
- mode = 'DryRun'
+ mode = 'Live'
host = $PlayoutHost
port = $PlayoutPort
tcpMode = 1
@@ -219,20 +219,19 @@ $authorization = [ordered]@{
$playoutJson = $playout | ConvertTo-Json -Depth 6
$authorizationJson = $authorization | ConvertTo-Json -Depth 3
-# In Force mode, invalidate the old authorization before changing the DryRun
-# base. Any later failure therefore leaves no valid authorization at the exact
-# path consumed by the Debug-only bootstrap.
+# In Force mode, invalidate the old authorization before changing the protected
+# Live profile. Any later failure therefore leaves no valid authorization at the
+# exact path consumed by the bootstrap.
Invalidate-ExistingAuthorization -Path $authorizationPath
-# Write the safe DryRun base first and the new one-launch authorization last.
-# Merely creating these files does not connect: Debug, the exact
-# --development-live argument, vendor hash verification and runtime gates remain
-# mandatory in the application.
+# Write the protected Live profile first and the launch authorization last.
+# Merely creating these files does not connect: vendor hash verification,
+# launch authorization, target validation, and runtime gates remain mandatory.
Write-ProtectedJson $playoutPath $playoutJson
Write-ProtectedJson $authorizationPath $authorizationJson
Write-Host "Development Live base configuration created at: $playoutPath"
Write-Host "Development Live launch authorization created at: $authorizationPath"
Write-Warning (
- 'The base file remains DryRun. Use only Debug|x64 with the exact ' +
- 'Development Live (Package) profile after confirming the local development PGM target.')
+ 'Debug and Release launches now use the protected Live profile. ' +
+ 'Confirm the local development PGM target before starting the application.')
diff --git a/scripts/Initialize-ExistingDevelopmentPc.ps1 b/scripts/Initialize-ExistingDevelopmentPc.ps1
index 3f5b1dc..e0564da 100644
--- a/scripts/Initialize-ExistingDevelopmentPc.ps1
+++ b/scripts/Initialize-ExistingDevelopmentPc.ps1
@@ -1416,7 +1416,7 @@ function Get-DevelopmentLivePreflight {
'8001', '8002', '8003', '8018', '8032', '8035', '8040', '8046',
'8051', '8056', '8061', '8067', 'N5001')
$playout = [ordered]@{
- mode = 'DryRun'
+ mode = 'Live'
host = $HostName
port = $Port
tcpMode = 1
diff --git a/src/MBN_STOCK_WEBVIEW.LegacyParityApp/App.xaml.cs b/src/MBN_STOCK_WEBVIEW.LegacyParityApp/App.xaml.cs
index 5d41157..3163da6 100644
--- a/src/MBN_STOCK_WEBVIEW.LegacyParityApp/App.xaml.cs
+++ b/src/MBN_STOCK_WEBVIEW.LegacyParityApp/App.xaml.cs
@@ -14,6 +14,10 @@ public partial class App : Application
internal static bool IsDevelopmentLiveLaunchRequested { get; private set; }
+ internal static string? DevelopmentLiveStartupFailureCode { get; private set; }
+
+ internal static string? DevelopmentLiveStartupFailureMessage { get; private set; }
+
public App()
{
InitializeComponent();
@@ -43,40 +47,31 @@ public partial class App : Application
_mainInstance = registeredInstance;
_mainInstance.Activated += OnMainInstanceActivated;
-#if DEBUG
- const bool isDebugBuild = true;
-#else
- const bool isDebugBuild = false;
-#endif
-
-#if SOURCE_ONLY_RUNTIME
- const bool isSourceOnlyBuild = true;
-#else
- const bool isSourceOnlyBuild = false;
-#endif
- var launchArguments = isSourceOnlyBuild
- ? null
- : DevelopmentLiveLaunchBootstrap.ResolveLaunchArguments(
- args.Arguments,
- Environment.GetCommandLineArgs());
- IsDevelopmentLiveLaunchRequested = string.Equals(
- launchArguments,
- DevelopmentLiveLaunchBootstrap.ExactLaunchArgument,
- StringComparison.Ordinal);
- var developmentLive = DevelopmentLiveLaunchBootstrap.TryApply(
- launchArguments,
- isDebugBuild);
- IsDevelopmentLiveLaunch = developmentLive.IsApplied;
- System.Diagnostics.Debug.WriteLine(
- $"Development Live bootstrap: {developmentLive.Code}");
-
// A fresh clone cannot become a full runtime inside the already compiled
// source-only process. Show only the native two-folder setup window; the
// reviewed initializer builds the verified runtime for the next F5.
- var showFirstRunSetup = isSourceOnlyBuild && isDebugBuild;
- _window = showFirstRunSetup
- ? new FirstRunSetupWindow()
- : new MainWindow();
+#if SOURCE_ONLY_RUNTIME
+ IsDevelopmentLiveLaunchRequested = false;
+ IsDevelopmentLiveLaunch = false;
+ DevelopmentLiveStartupFailureCode = null;
+ DevelopmentLiveStartupFailureMessage = null;
+ _window = new FirstRunSetupWindow();
+#else
+ var launchArguments = DevelopmentLiveLaunchBootstrap.ResolveLaunchArguments(
+ args.Arguments,
+ Environment.GetCommandLineArgs());
+ var developmentLive = DevelopmentLiveLaunchBootstrap.TryApply(launchArguments);
+ IsDevelopmentLiveLaunchRequested = developmentLive.IsRequested;
+ IsDevelopmentLiveLaunch = developmentLive.IsApplied;
+ DevelopmentLiveStartupFailureCode = developmentLive.IsLaunchAllowed
+ ? null
+ : developmentLive.Code;
+ DevelopmentLiveStartupFailureMessage = developmentLive.FailureMessage;
+ System.Diagnostics.Debug.WriteLine(
+ $"Development Live bootstrap: {developmentLive.Code}");
+
+ _window = new MainWindow();
+#endif
_window.Activate();
}
diff --git a/src/MBN_STOCK_WEBVIEW.LegacyParityApp/MBN_STOCK_WEBVIEW.LegacyParityApp.csproj b/src/MBN_STOCK_WEBVIEW.LegacyParityApp/MBN_STOCK_WEBVIEW.LegacyParityApp.csproj
index df8ea79..81d7d43 100644
--- a/src/MBN_STOCK_WEBVIEW.LegacyParityApp/MBN_STOCK_WEBVIEW.LegacyParityApp.csproj
+++ b/src/MBN_STOCK_WEBVIEW.LegacyParityApp/MBN_STOCK_WEBVIEW.LegacyParityApp.csproj
@@ -205,7 +205,7 @@
Text="External legacy runtime paths are enabled. Cuts files remain in the selected folder and database credentials remain in the current user's LocalAppData overlay." />
+ Text="Legacy runtime source root was not found: $(LegacyRuntimeSourceRoot). Run scripts\Initialize-ExistingDevelopmentPc.ps1 to create the verified local Required binding, or use /p:LegacyRuntimeAssetsMode=Auto for a source-only setup build." />
diff --git a/src/MBN_STOCK_WEBVIEW.LegacyParityApp/MainWindow.Playout.cs b/src/MBN_STOCK_WEBVIEW.LegacyParityApp/MainWindow.Playout.cs
index 2320458..524f7fe 100644
--- a/src/MBN_STOCK_WEBVIEW.LegacyParityApp/MainWindow.Playout.cs
+++ b/src/MBN_STOCK_WEBVIEW.LegacyParityApp/MainWindow.Playout.cs
@@ -32,21 +32,33 @@ public sealed partial class MainWindow
{
try
{
+ if (_developmentLiveStartupBlocked)
+ {
+ _playoutInitializationError =
+ App.DevelopmentLiveStartupFailureMessage ??
+ "Live playout startup validation failed.";
+ return;
+ }
+
var explicitLocalConfigurationExists = File.Exists(
PlayoutOptionsLoader.DefaultPath);
_playoutOptions =
- _isDevelopmentLiveLaunch &&
- !_developmentLiveStartupBlocked
+ _isDevelopmentLiveLaunch
? PlayoutOptionsLoader.LoadDevelopmentLive(
baseDirectory: AppContext.BaseDirectory,
operatorSceneDirectory:
_appliedOperatorSettings.SceneDirectory)
: PlayoutOptionsLoader.Load(
operatorSceneDirectory: _appliedOperatorSettings.SceneDirectory,
- operatorBackgroundDirectory: _appliedOperatorSettings.BackgroundDirectory,
- forceSafeDryRun: IsSourceOnlyBuild ||
- _isDevelopmentLiveLaunchRequested ||
- _developmentLiveStartupBlocked);
+ operatorBackgroundDirectory:
+ _appliedOperatorSettings.BackgroundDirectory);
+ if (_playoutOptions.Mode != PlayoutMode.Live)
+ {
+ throw new PlayoutConfigurationException(
+ "The application requires a validated Live playout configuration. " +
+ "DryRun fallback is disabled.");
+ }
+
ResetRefreshState();
var startupComposition = LegacyParityStartupCompositionResolver.Resolve(
_playoutOptions,
@@ -75,6 +87,7 @@ public sealed partial class MainWindow
}
catch (Exception exception)
{
+ _developmentLiveStartupBlocked = true;
_playoutInitializationWarning = null;
_playoutInitializationError = exception is PlayoutConfigurationException
? exception.Message
diff --git a/src/MBN_STOCK_WEBVIEW.LegacyParityApp/MainWindow.xaml.cs b/src/MBN_STOCK_WEBVIEW.LegacyParityApp/MainWindow.xaml.cs
index 73f501b..a33d7ab 100644
--- a/src/MBN_STOCK_WEBVIEW.LegacyParityApp/MainWindow.xaml.cs
+++ b/src/MBN_STOCK_WEBVIEW.LegacyParityApp/MainWindow.xaml.cs
@@ -64,6 +64,9 @@ public sealed partial class MainWindow : Window
{
_isDevelopmentLiveLaunch = App.IsDevelopmentLiveLaunch;
_isDevelopmentLiveLaunchRequested = App.IsDevelopmentLiveLaunchRequested;
+ _developmentLiveStartupBlocked =
+ _isDevelopmentLiveLaunchRequested &&
+ !_isDevelopmentLiveLaunch;
// Capture and remove Gate A bearer material before XAML can create WebView2.
// Only the native process keeps the capability digest for this launch.
_playoutLaunchAuthorization =
@@ -119,9 +122,15 @@ public sealed partial class MainWindow : Window
IOperatorCatalogSchemaValidationService operatorCatalogSchemaValidationService;
CorePagePlanProvider? fixedPagePlanProvider = null;
string? initializationError = null;
- var databaseInitializationSucceeded = false;
try
{
+ if (_developmentLiveStartupBlocked)
+ {
+ throw new InvalidOperationException(
+ App.DevelopmentLiveStartupFailureMessage ??
+ "Live playout startup validation failed.");
+ }
+
if (IsSourceOnlyBuild)
{
throw new InvalidOperationException(
@@ -198,14 +207,16 @@ public sealed partial class MainWindow : Window
new LegacyStockMasterIdentityValidationService(_databaseRuntime.Executor);
operatorCatalogSchemaValidationService =
new LegacyOperatorCatalogSchemaValidationService(_databaseRuntime.Executor);
- databaseInitializationSucceeded = true;
}
catch
{
_databaseRuntime = null;
fixedPagePlanProvider = null;
- initializationError = IsSourceOnlyBuild
- ? "소스 전용 DryRun에서는 데이터베이스 연결을 사용하지 않습니다."
+ initializationError = _developmentLiveStartupBlocked
+ ? App.DevelopmentLiveStartupFailureMessage ??
+ "Live playout startup validation failed."
+ : IsSourceOnlyBuild
+ ? "소스 전용 설정 빌드에서는 데이터베이스 연결을 사용하지 않습니다."
: "데이터베이스가 설정되지 않았습니다. 로컬 설정을 확인하세요.";
stockLookup = new UnavailableStockLookup(initializationError);
industrySelectionService = new UnavailableIndustrySelectionService(
@@ -241,14 +252,6 @@ public sealed partial class MainWindow : Window
DataQueryExecutor.Reset();
}
- if (_isDevelopmentLiveLaunch && !databaseInitializationSucceeded)
- {
- // Development Live is one fail-closed startup transaction. A
- // missing or partially initialized protected DB runtime must not
- // leave the independently armed playout gate eligible for use.
- _developmentLiveStartupBlocked = true;
- }
-
_industryWorkflow = new LegacyIndustrySelectionWorkflow(
industrySelectionService,
actionLayout: runtimeUiCatalogs.IndustryLayout);
diff --git a/src/MBN_STOCK_WEBVIEW.LegacyParityApp/Properties/launchSettings.json b/src/MBN_STOCK_WEBVIEW.LegacyParityApp/Properties/launchSettings.json
index eefb321..ef70d60 100644
--- a/src/MBN_STOCK_WEBVIEW.LegacyParityApp/Properties/launchSettings.json
+++ b/src/MBN_STOCK_WEBVIEW.LegacyParityApp/Properties/launchSettings.json
@@ -3,10 +3,6 @@
"MBN_STOCK_WEBVIEW.LegacyParityApp - Development Live (Package)": {
"commandName": "MsixPackage",
"commandLineArgs": "--development-live"
- },
- "MBN_STOCK_WEBVIEW.LegacyParityApp - Explicit DryRun (Package)": {
- "commandName": "MsixPackage",
- "commandLineArgs": ""
}
}
}
diff --git a/src/MBN_STOCK_WEBVIEW.Playout/Configuration/DevelopmentLiveLaunchBootstrap.cs b/src/MBN_STOCK_WEBVIEW.Playout/Configuration/DevelopmentLiveLaunchBootstrap.cs
index ef882f5..ddc52ad 100644
--- a/src/MBN_STOCK_WEBVIEW.Playout/Configuration/DevelopmentLiveLaunchBootstrap.cs
+++ b/src/MBN_STOCK_WEBVIEW.Playout/Configuration/DevelopmentLiveLaunchBootstrap.cs
@@ -1,6 +1,7 @@
using System.Security;
using System.Text.Json;
using MBN_STOCK_WEBVIEW.Playout.Diagnostics;
+using MBN_STOCK_WEBVIEW.Playout.Safety;
namespace MBN_STOCK_WEBVIEW.Playout.Configuration;
@@ -8,6 +9,7 @@ internal enum DevelopmentLiveBootstrapStatus
{
Skipped,
Applied,
+ GateABypassed,
Rejected
}
@@ -16,6 +18,31 @@ internal readonly record struct DevelopmentLiveBootstrapResult(
string Code)
{
public bool IsApplied => Status == DevelopmentLiveBootstrapStatus.Applied;
+
+ public bool IsLaunchAllowed =>
+ Status is DevelopmentLiveBootstrapStatus.Applied or
+ DevelopmentLiveBootstrapStatus.GateABypassed;
+
+ public bool IsRequested =>
+ Status is DevelopmentLiveBootstrapStatus.Applied or
+ DevelopmentLiveBootstrapStatus.Rejected;
+
+ public string? FailureMessage => Status == DevelopmentLiveBootstrapStatus.Rejected
+ ? Code switch
+ {
+ "development-live-arguments-invalid" =>
+ "Live startup was rejected because the launch arguments were not recognized.",
+ "development-live-gate-a-inspection-failed" =>
+ "Gate A launch authorization could not be inspected.",
+ "development-live-authorization-unavailable" =>
+ "Live startup authorization is unavailable.",
+ "development-live-authorization-invalid" =>
+ "Live startup authorization is invalid.",
+ "development-live-environment-failed" =>
+ "Live startup authorization could not be applied to this process.",
+ _ => "Live startup authorization failed."
+ }
+ : null;
}
internal interface IDevelopmentLiveBootstrapPlatform
@@ -28,10 +55,10 @@ internal interface IDevelopmentLiveBootstrapPlatform
}
///
-/// Arms the existing process-scoped Live and vendor-binary gates for one explicit
-/// Visual Studio Debug launch. The ordinary app startup path never calls this with
-/// a true debug-build capability, and the authorization file is deliberately kept
-/// outside the package and source tree.
+/// Arms the existing process-scoped Live and vendor-binary gates for a full-runtime
+/// application launch. Both ordinary no-argument launches and the retained exact
+/// development-live argument use the same strict local authorization file, which
+/// is deliberately kept outside the package and source tree.
///
internal static class DevelopmentLiveLaunchBootstrap
{
@@ -46,6 +73,8 @@ internal static class DevelopmentLiveLaunchBootstrap
private const string AuthorizationProperty = "authorization";
private const string NativeSha256Property = "nativeSha256";
private const string InteropSha256Property = "interopSha256";
+ private const string InvalidLaunchArgumentsMarker =
+ "\0MBN_STOCK_WEBVIEW_INVALID_LAUNCH_ARGUMENTS";
private static readonly IReadOnlySet AllowedProperties =
new HashSet(StringComparer.Ordinal)
@@ -65,6 +94,14 @@ internal static class DevelopmentLiveLaunchBootstrap
PlayoutOptionsLoader.LiveAuthorizationEnvironmentVariable
];
+ private static readonly string[] GateAEnvironmentNames =
+ [
+ PlayoutLaunchAuthorization.GateACapabilityEnvironmentVariable,
+ PlayoutLaunchAuthorization.GateAPgmProcessIdEnvironmentVariable,
+ PlayoutLaunchAuthorization.GateAPgmStartTimeUtcTicksEnvironmentVariable,
+ PlayoutLaunchAuthorization.GateAExpiresAtUtcTicksEnvironmentVariable
+ ];
+
internal static string DefaultPath => Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
"MBN_STOCK_WEBVIEW",
@@ -86,11 +123,22 @@ internal static class DevelopmentLiveLaunchBootstrap
return activationArguments;
}
- return processArguments.Count == 2
- ? processArguments[1]
- : null;
+ return processArguments.Count switch
+ {
+ <= 1 => null,
+ 2 => processArguments[1],
+ _ => InvalidLaunchArgumentsMarker
+ };
}
+ internal static DevelopmentLiveBootstrapResult TryApply(
+ string? launchArguments) =>
+ TryApply(
+ launchArguments,
+ isDebugBuild: true,
+ DefaultPath,
+ new SystemDevelopmentLiveBootstrapPlatform());
+
internal static DevelopmentLiveBootstrapResult TryApply(
string? launchArguments,
bool isDebugBuild) =>
@@ -108,21 +156,32 @@ internal static class DevelopmentLiveLaunchBootstrap
{
ArgumentNullException.ThrowIfNull(platform);
- if (!string.Equals(
+ // Retain the isDebugBuild parameter for callers compiled against the
+ // previous API. Release and Debug now intentionally share this exact gate.
+ _ = isDebugBuild;
+
+ if (!string.IsNullOrEmpty(launchArguments) &&
+ !string.Equals(
launchArguments,
ExactLaunchArgument,
StringComparison.Ordinal))
{
- return new DevelopmentLiveBootstrapResult(
- DevelopmentLiveBootstrapStatus.Skipped,
- "development-live-not-requested");
+ return Rejected("development-live-arguments-invalid");
}
- if (!isDebugBuild)
+ if (!TryHasGateAEnvironment(platform, out var hasGateAEnvironment))
+ {
+ return Rejected("development-live-gate-a-inspection-failed");
+ }
+
+ // Gate A owns and consumes its process-scoped bearer values in
+ // PlayoutLaunchAuthorization. Do not read the ordinary authorization file
+ // and do not set, clear or roll back any environment value in this branch.
+ if (hasGateAEnvironment)
{
return new DevelopmentLiveBootstrapResult(
- DevelopmentLiveBootstrapStatus.Skipped,
- "development-live-debug-only");
+ DevelopmentLiveBootstrapStatus.GateABypassed,
+ "development-live-gate-a-bypassed");
}
if (string.IsNullOrWhiteSpace(authorizationFilePath))
@@ -305,13 +364,37 @@ internal static class DevelopmentLiveLaunchBootstrap
}
}
+ private static bool TryHasGateAEnvironment(
+ IDevelopmentLiveBootstrapPlatform platform,
+ out bool hasGateAEnvironment)
+ {
+ hasGateAEnvironment = false;
+ try
+ {
+ foreach (var name in GateAEnvironmentNames)
+ {
+ if (platform.GetProcessEnvironmentVariable(name) is not null)
+ {
+ hasGateAEnvironment = true;
+ }
+ }
+
+ return true;
+ }
+ catch (Exception exception) when (IsExpectedEnvironmentFailure(exception))
+ {
+ hasGateAEnvironment = false;
+ return false;
+ }
+ }
+
private static DevelopmentLiveBootstrapResult RejectedAndDisarm(
IDevelopmentLiveBootstrapPlatform platform,
string code)
{
- // An exact Debug request that failed validation must not fall through to
+ // A required Live request that failed validation must not fall through to
// inherited process state from an earlier shell or Visual Studio session.
- // The app also forces this requested-but-not-applied state to DryRun.
+ // The app treats this requested-but-not-applied state as a startup failure.
for (var index = ArmedEnvironmentNames.Length - 1; index >= 0; index--)
{
var name = ArmedEnvironmentNames[index];
@@ -354,7 +437,7 @@ internal static class DevelopmentLiveLaunchBootstrap
private static bool IsExpectedFileFailure(Exception exception) =>
exception is IOException or UnauthorizedAccessException or SecurityException or
- ArgumentException or NotSupportedException;
+ InvalidDataException or ArgumentException or NotSupportedException;
private static bool IsExpectedEnvironmentFailure(Exception exception) =>
exception is ArgumentException or SecurityException or InvalidOperationException;
diff --git a/src/MBN_STOCK_WEBVIEW.Playout/Configuration/PlayoutOptionsLoader.cs b/src/MBN_STOCK_WEBVIEW.Playout/Configuration/PlayoutOptionsLoader.cs
index 5ea728e..de03211 100644
--- a/src/MBN_STOCK_WEBVIEW.Playout/Configuration/PlayoutOptionsLoader.cs
+++ b/src/MBN_STOCK_WEBVIEW.Playout/Configuration/PlayoutOptionsLoader.cs
@@ -88,11 +88,10 @@ public static class PlayoutOptionsLoader
}
///
- /// Loads the inert LocalAppData base profile for an already-applied exact
- /// Debug Development Live bootstrap. This path deliberately ignores process
- /// environment and ordinary playout-profile scene overrides. Live mode is
- /// armed from bootstrap provenance, and the scene root is pinned to the
- /// native folder selected during first-run setup.
+ /// Loads the protected LocalAppData Live profile for an already-applied
+ /// Development Live bootstrap. This path deliberately ignores process
+ /// environment and ordinary playout-profile scene overrides. The scene root
+ /// is pinned to the native folder selected during first-run setup.
///
internal static PlayoutOptions LoadDevelopmentLive(
string? path = null,
@@ -188,7 +187,7 @@ public static class PlayoutOptionsLoader
private static void EnsureDevelopmentLiveBaseContract(PlayoutOptions options)
{
- if (options.Mode != PlayoutMode.DryRun ||
+ if (options.Mode != PlayoutMode.Live ||
!IsLiteralLoopback(options.Host) ||
options.Port is < 1 or > 65_535 ||
options.TcpMode != 1 ||
@@ -285,7 +284,7 @@ public static class PlayoutOptionsLoader
}
if (seen.Count != DevelopmentLiveProperties.Count ||
- !IsExactString(root, "mode", "DryRun") ||
+ !IsExactString(root, "mode", "Live") ||
!IsLoopbackString(root, "host") ||
!IsIntegerInRange(root, "port", 1, 65_535) ||
!IsExactInteger(root, "tcpMode", 1) ||
diff --git a/tests/MBN_STOCK_WEBVIEW.LegacyWeb.Tests/LegacyDevelopmentLiveStartupContractTests.cs b/tests/MBN_STOCK_WEBVIEW.LegacyWeb.Tests/LegacyDevelopmentLiveStartupContractTests.cs
index 3a21e8a..ce0bd9c 100644
--- a/tests/MBN_STOCK_WEBVIEW.LegacyWeb.Tests/LegacyDevelopmentLiveStartupContractTests.cs
+++ b/tests/MBN_STOCK_WEBVIEW.LegacyWeb.Tests/LegacyDevelopmentLiveStartupContractTests.cs
@@ -17,29 +17,25 @@ public sealed class LegacyDevelopmentLiveStartupContractTests
"MBN_STOCK_WEBVIEW.LegacyParityApp");
[Fact]
- public void VisualStudioProfilesPutDevelopmentLiveFirstAndKeepExplicitDryRun()
+ public void VisualStudioExposesOneLivePackageProfileWithoutDryRun()
{
using var document = JsonDocument.Parse(File.ReadAllText(Path.Combine(
ParityRoot,
"Properties",
"launchSettings.json")));
- var profiles = document.RootElement.GetProperty("profiles")
- .EnumerateObject()
- .ToArray();
+ var profile = Assert.Single(document.RootElement.GetProperty("profiles")
+ .EnumerateObject());
- Assert.Equal(2, profiles.Length);
- Assert.Equal(
- "MBN_STOCK_WEBVIEW.LegacyParityApp - Development Live (Package)",
- profiles[0].Name);
- Assert.Equal("MsixPackage", profiles[0].Value.GetProperty("commandName").GetString());
- Assert.Equal(ExactArgument,
- profiles[0].Value.GetProperty("commandLineArgs").GetString());
- Assert.Equal(
- "MBN_STOCK_WEBVIEW.LegacyParityApp - Explicit DryRun (Package)",
- profiles[1].Name);
- Assert.Equal("MsixPackage", profiles[1].Value.GetProperty("commandName").GetString());
- Assert.Equal(string.Empty,
- profiles[1].Value.GetProperty("commandLineArgs").GetString());
+ Assert.Contains("Live", profile.Name, StringComparison.Ordinal);
+ Assert.DoesNotContain("DryRun", profile.Name, StringComparison.Ordinal);
+ Assert.Equal("MsixPackage", profile.Value.GetProperty("commandName").GetString());
+ var arguments = profile.Value.TryGetProperty("commandLineArgs", out var value)
+ ? value.GetString()
+ : null;
+ Assert.True(
+ string.IsNullOrEmpty(arguments) ||
+ string.Equals(arguments, ExactArgument, StringComparison.Ordinal),
+ "The sole Live package profile may use only the normal launch or exact Live argument.");
}
[Fact]
@@ -75,7 +71,7 @@ public sealed class LegacyDevelopmentLiveStartupContractTests
}
[Fact]
- public void AppAppliesCompileTimeBuildGateBeforeConstructingMainWindow()
+ public void AppAppliesRuntimeGateBeforeConstructingAnyRuntimeWindow()
{
var app = File.ReadAllText(Path.Combine(ParityRoot, "App.xaml.cs"));
var bootstrap = app.IndexOf(
@@ -87,35 +83,37 @@ public sealed class LegacyDevelopmentLiveStartupContractTests
var constructMainWindow = app.IndexOf(
"new MainWindow();",
StringComparison.Ordinal);
+ var sourceOnlyDirective = app.IndexOf(
+ "#if SOURCE_ONLY_RUNTIME",
+ StringComparison.Ordinal);
+ var fullRuntimeDirective = app.IndexOf(
+ "#else",
+ constructSetupWindow,
+ StringComparison.Ordinal);
+ var runtimeEndDirective = app.IndexOf(
+ "#endif",
+ constructMainWindow,
+ StringComparison.Ordinal);
Assert.True(bootstrap >= 0);
- Assert.True(constructSetupWindow > bootstrap);
- Assert.True(constructMainWindow > constructSetupWindow);
- Assert.Contains("#if DEBUG", app, StringComparison.Ordinal);
- Assert.Contains("const bool isDebugBuild = true;", app, StringComparison.Ordinal);
- Assert.Contains("const bool isDebugBuild = false;", app, StringComparison.Ordinal);
- Assert.Contains("#if SOURCE_ONLY_RUNTIME", app, StringComparison.Ordinal);
- Assert.Contains("const bool isSourceOnlyBuild = true;", app,
- StringComparison.Ordinal);
- Assert.Contains("const bool isSourceOnlyBuild = false;", app,
- StringComparison.Ordinal);
- Assert.Contains("isSourceOnlyBuild", app, StringComparison.Ordinal);
- Assert.Contains("? null", app, StringComparison.Ordinal);
+ Assert.True(sourceOnlyDirective >= 0);
+ Assert.True(constructSetupWindow > sourceOnlyDirective);
+ Assert.True(fullRuntimeDirective > constructSetupWindow);
+ Assert.True(bootstrap > fullRuntimeDirective);
+ Assert.True(constructMainWindow > bootstrap);
+ Assert.True(runtimeEndDirective > constructMainWindow);
Assert.Contains("args.Arguments", app, StringComparison.Ordinal);
Assert.Contains("Environment.GetCommandLineArgs()", app, StringComparison.Ordinal);
Assert.Contains("ResolveLaunchArguments(", app, StringComparison.Ordinal);
Assert.Contains(
- "IsDevelopmentLiveLaunchRequested = string.Equals(",
+ "IsDevelopmentLiveLaunchRequested = developmentLive.IsRequested;",
app,
StringComparison.Ordinal);
Assert.Contains(
"IsDevelopmentLiveLaunch = developmentLive.IsApplied;",
app,
StringComparison.Ordinal);
- Assert.Contains(
- "_window = showFirstRunSetup",
- app,
- StringComparison.Ordinal);
+ Assert.DoesNotContain("#if DEBUG", app, StringComparison.Ordinal);
}
[Fact]
@@ -162,14 +160,15 @@ public sealed class LegacyDevelopmentLiveStartupContractTests
" _appliedOperatorSettings.SceneDirectory",
playout.ReplaceLineEndings("\n"),
StringComparison.Ordinal);
+ Assert.DoesNotContain("forceSafeDryRun", playout, StringComparison.Ordinal);
Assert.Contains(
- "IsSourceOnlyBuild ||\n _isDevelopmentLiveLaunchRequested",
- playout.ReplaceLineEndings("\n"),
+ "if (_playoutOptions.Mode != PlayoutMode.Live)",
+ playout,
StringComparison.Ordinal);
}
[Fact]
- public void DevelopmentLiveDatabaseFailureForcesDryRunAndSkipsAutomaticConnect()
+ public void DatabaseFailureDoesNotDowngradeOrBlockValidatedLiveEngine()
{
var window = File.ReadAllText(Path.Combine(ParityRoot, "MainWindow.xaml.cs"))
.ReplaceLineEndings("\n");
@@ -178,49 +177,87 @@ public sealed class LegacyDevelopmentLiveStartupContractTests
"MainWindow.Playout.cs"))
.ReplaceLineEndings("\n");
- var blockAssignment = window.IndexOf(
- "_developmentLiveStartupBlocked = true;",
- StringComparison.Ordinal);
- var successAssignment = window.IndexOf(
- "databaseInitializationSucceeded = true;",
- StringComparison.Ordinal);
var runtimeReset = window.IndexOf(
"_databaseRuntime = null;",
- successAssignment,
StringComparison.Ordinal);
var pagePlanReset = window.IndexOf(
"fixedPagePlanProvider = null;",
- successAssignment,
+ runtimeReset,
+ StringComparison.Ordinal);
+ var databaseFailureEnd = window.IndexOf(
+ "_industryWorkflow = new LegacyIndustrySelectionWorkflow(",
+ pagePlanReset,
StringComparison.Ordinal);
var playoutInitialization = window.IndexOf(
"InitializePlayoutRuntime();",
StringComparison.Ordinal);
- Assert.True(successAssignment >= 0);
- Assert.True(runtimeReset > successAssignment);
- Assert.True(pagePlanReset > successAssignment);
- Assert.True(blockAssignment > runtimeReset);
- Assert.True(playoutInitialization > blockAssignment);
- Assert.Contains(
- "var databaseInitializationSucceeded = false;",
- window,
+
+ Assert.True(runtimeReset >= 0);
+ Assert.True(pagePlanReset > runtimeReset);
+ Assert.True(databaseFailureEnd > pagePlanReset);
+ Assert.True(playoutInitialization > databaseFailureEnd);
+ Assert.DoesNotContain(
+ "_developmentLiveStartupBlocked = true;",
+ window[runtimeReset..databaseFailureEnd],
+ StringComparison.Ordinal);
+ Assert.DoesNotContain("databaseInitializationSucceeded", window,
StringComparison.Ordinal);
Assert.Contains(
- "if (_isDevelopmentLiveLaunch && !databaseInitializationSucceeded)\n" +
- " {\n" +
- " // Development Live is one fail-closed startup transaction.",
- window,
- StringComparison.Ordinal);
- Assert.Contains(
- "_isDevelopmentLiveLaunch &&\n" +
- " !_developmentLiveStartupBlocked\n" +
+ "_isDevelopmentLiveLaunch\n" +
" ? PlayoutOptionsLoader.LoadDevelopmentLive(",
playout,
StringComparison.Ordinal);
Assert.Contains(
- "_isDevelopmentLiveLaunchRequested ||\n" +
- " _developmentLiveStartupBlocked",
+ "if (_playoutOptions.Mode != PlayoutMode.Live)",
playout,
StringComparison.Ordinal);
+ Assert.Contains(
+ "_playoutEngine = PlayoutEngineFactory.Create(",
+ playout,
+ StringComparison.Ordinal);
+ }
+
+ [Fact]
+ public void BootstrapOrPlayoutInvariantFailureBlocksEngineAndAutomaticConnect()
+ {
+ var window = File.ReadAllText(Path.Combine(ParityRoot, "MainWindow.xaml.cs"))
+ .ReplaceLineEndings("\n");
+ var playout = File.ReadAllText(Path.Combine(
+ ParityRoot,
+ "MainWindow.Playout.cs"))
+ .ReplaceLineEndings("\n");
+
+ var initialBlock = window.IndexOf(
+ "_developmentLiveStartupBlocked =\n" +
+ " _isDevelopmentLiveLaunchRequested &&\n" +
+ " !_isDevelopmentLiveLaunch;",
+ StringComparison.Ordinal);
+ var earlyBlock = playout.IndexOf(
+ "if (_developmentLiveStartupBlocked)",
+ StringComparison.Ordinal);
+ var earlyReturn = playout.IndexOf("return;", earlyBlock, StringComparison.Ordinal);
+ var loadLive = playout.IndexOf(
+ "PlayoutOptionsLoader.LoadDevelopmentLive(",
+ StringComparison.Ordinal);
+ var requireLive = playout.IndexOf(
+ "if (_playoutOptions.Mode != PlayoutMode.Live)",
+ StringComparison.Ordinal);
+ var createEngine = playout.IndexOf(
+ "_playoutEngine = PlayoutEngineFactory.Create(",
+ StringComparison.Ordinal);
+ var blockOnInitializationFailure = playout.IndexOf(
+ "_developmentLiveStartupBlocked = true;",
+ createEngine,
+ StringComparison.Ordinal);
+
+ Assert.True(initialBlock >= 0);
+ Assert.True(earlyBlock >= 0);
+ Assert.True(earlyReturn > earlyBlock);
+ Assert.True(loadLive > earlyReturn);
+ Assert.True(requireLive > loadLive);
+ Assert.True(createEngine > requireLive);
+ Assert.True(blockOnInitializationFailure > createEngine);
+ Assert.DoesNotContain("forceSafeDryRun", playout, StringComparison.Ordinal);
Assert.Contains(
"if (!_developmentLiveStartupBlocked)\n" +
" {\n" +
diff --git a/tests/MBN_STOCK_WEBVIEW.LegacyWeb.Tests/LegacyFirstRunSetupNativeContractTests.cs b/tests/MBN_STOCK_WEBVIEW.LegacyWeb.Tests/LegacyFirstRunSetupNativeContractTests.cs
index e78638b..a4a2f9f 100644
--- a/tests/MBN_STOCK_WEBVIEW.LegacyWeb.Tests/LegacyFirstRunSetupNativeContractTests.cs
+++ b/tests/MBN_STOCK_WEBVIEW.LegacyWeb.Tests/LegacyFirstRunSetupNativeContractTests.cs
@@ -16,17 +16,28 @@ public sealed class LegacyFirstRunSetupNativeContractTests
var app = File.ReadAllText(Path.Combine(AppRoot, "App.xaml.cs"))
.ReplaceLineEndings("\n");
- Assert.Contains(
- "var showFirstRunSetup = isSourceOnlyBuild && isDebugBuild;\n" +
- " _window = showFirstRunSetup\n" +
- " ? new FirstRunSetupWindow()\n" +
- " : new MainWindow();",
- app,
+ var sourceOnlyStart = app.IndexOf("#if SOURCE_ONLY_RUNTIME",
StringComparison.Ordinal);
- Assert.Contains(
- "var launchArguments = isSourceOnlyBuild\n" +
- " ? null",
- app,
+ var fullRuntimeStart = app.IndexOf("#else", sourceOnlyStart,
+ StringComparison.Ordinal);
+ var runtimeEnd = app.IndexOf("#endif", fullRuntimeStart,
+ StringComparison.Ordinal);
+ Assert.True(sourceOnlyStart >= 0);
+ Assert.True(fullRuntimeStart > sourceOnlyStart);
+ Assert.True(runtimeEnd > fullRuntimeStart);
+
+ var sourceOnly = app[sourceOnlyStart..fullRuntimeStart];
+ var fullRuntime = app[fullRuntimeStart..runtimeEnd];
+ Assert.Contains("new FirstRunSetupWindow()", sourceOnly, StringComparison.Ordinal);
+ Assert.DoesNotContain("new MainWindow()", sourceOnly, StringComparison.Ordinal);
+ Assert.DoesNotContain(
+ "DevelopmentLiveLaunchBootstrap.TryApply(",
+ sourceOnly,
+ StringComparison.Ordinal);
+ Assert.Contains("DevelopmentLiveLaunchBootstrap.TryApply(", fullRuntime,
+ StringComparison.Ordinal);
+ Assert.Contains("new MainWindow()", fullRuntime, StringComparison.Ordinal);
+ Assert.DoesNotContain("new FirstRunSetupWindow()", fullRuntime,
StringComparison.Ordinal);
Assert.DoesNotContain(
"new FirstRunSetupWindow(App.IsDevelopmentLiveLaunch",
diff --git a/tests/MBN_STOCK_WEBVIEW.LegacyWeb.Tests/LegacyOperatorSettingsNativeContractTests.cs b/tests/MBN_STOCK_WEBVIEW.LegacyWeb.Tests/LegacyOperatorSettingsNativeContractTests.cs
index 2fb05f4..e736eb4 100644
--- a/tests/MBN_STOCK_WEBVIEW.LegacyWeb.Tests/LegacyOperatorSettingsNativeContractTests.cs
+++ b/tests/MBN_STOCK_WEBVIEW.LegacyWeb.Tests/LegacyOperatorSettingsNativeContractTests.cs
@@ -104,9 +104,11 @@ public sealed class LegacyOperatorSettingsNativeContractTests
Playout,
StringComparison.Ordinal);
Assert.Contains(
- "operatorBackgroundDirectory: _appliedOperatorSettings.BackgroundDirectory",
+ "operatorBackgroundDirectory:",
Playout,
StringComparison.Ordinal);
+ Assert.Contains("_appliedOperatorSettings.BackgroundDirectory", Playout,
+ StringComparison.Ordinal);
Assert.DoesNotContain("InitializePlayoutRuntime();", Settings, StringComparison.Ordinal);
Assert.DoesNotContain("CreateLegacyExecutableDefault(", Settings,
StringComparison.Ordinal);
diff --git a/tests/MBN_STOCK_WEBVIEW.LegacyWeb.Tests/LegacyRuntimeAssetDeploymentContractTests.cs b/tests/MBN_STOCK_WEBVIEW.LegacyWeb.Tests/LegacyRuntimeAssetDeploymentContractTests.cs
index 9f017f8..dd27b66 100644
--- a/tests/MBN_STOCK_WEBVIEW.LegacyWeb.Tests/LegacyRuntimeAssetDeploymentContractTests.cs
+++ b/tests/MBN_STOCK_WEBVIEW.LegacyWeb.Tests/LegacyRuntimeAssetDeploymentContractTests.cs
@@ -442,7 +442,9 @@ public sealed class LegacyRuntimeAssetDeploymentContractTests
"src",
"MBN_STOCK_WEBVIEW.LegacyParityApp",
"MainWindow.Playout.cs"));
- Assert.Contains("forceSafeDryRun: IsSourceOnlyBuild", playout,
+ Assert.DoesNotContain("forceSafeDryRun", playout,
+ StringComparison.Ordinal);
+ Assert.Contains("if (_playoutOptions.Mode != PlayoutMode.Live)", playout,
StringComparison.Ordinal);
}
@@ -540,7 +542,8 @@ public sealed class LegacyRuntimeAssetDeploymentContractTests
"[ValidateSet('127.0.0.1', '::1')]",
initializer,
StringComparison.Ordinal);
- Assert.Contains("mode = 'DryRun'", initializer, StringComparison.Ordinal);
+ Assert.Contains("mode = 'Live'", initializer, StringComparison.Ordinal);
+ Assert.DoesNotContain("mode = 'DryRun'", initializer, StringComparison.Ordinal);
Assert.Contains(
"trustedLiveOutputEnabled = $true",
initializer,
diff --git a/tests/MBN_STOCK_WEBVIEW.Playout.Tests/DevelopmentLiveLaunchBootstrapTests.cs b/tests/MBN_STOCK_WEBVIEW.Playout.Tests/DevelopmentLiveLaunchBootstrapTests.cs
index 7ad68f1..75566e9 100644
--- a/tests/MBN_STOCK_WEBVIEW.Playout.Tests/DevelopmentLiveLaunchBootstrapTests.cs
+++ b/tests/MBN_STOCK_WEBVIEW.Playout.Tests/DevelopmentLiveLaunchBootstrapTests.cs
@@ -2,6 +2,7 @@ using System.Security;
using System.Text;
using MBN_STOCK_WEBVIEW.Playout.Configuration;
using MBN_STOCK_WEBVIEW.Playout.Diagnostics;
+using MBN_STOCK_WEBVIEW.Playout.Safety;
namespace MBN_STOCK_WEBVIEW.Playout.Tests;
@@ -42,12 +43,18 @@ public sealed class DevelopmentLiveLaunchBootstrapTests
}
[Fact]
- public void ResolveLaunchArguments_RejectsMissingOrAdditionalProcessArguments()
+ public void ResolveLaunchArguments_DistinguishesNormalLaunchFromAdditionalArguments()
{
Assert.Null(DevelopmentLiveLaunchBootstrap.ResolveLaunchArguments("", ["app.exe"]));
- Assert.Null(DevelopmentLiveLaunchBootstrap.ResolveLaunchArguments(
+ var additionalArguments = DevelopmentLiveLaunchBootstrap.ResolveLaunchArguments(
"",
- ["app.exe", DevelopmentLiveLaunchBootstrap.ExactLaunchArgument, "extra"]));
+ ["app.exe", DevelopmentLiveLaunchBootstrap.ExactLaunchArgument, "extra"]);
+
+ Assert.NotNull(additionalArguments);
+ Assert.NotEqual(string.Empty, additionalArguments);
+ Assert.NotEqual(
+ DevelopmentLiveLaunchBootstrap.ExactLaunchArgument,
+ additionalArguments);
}
[Fact]
@@ -65,13 +72,11 @@ public sealed class DevelopmentLiveLaunchBootstrapTests
}
[Theory]
- [InlineData(null)]
- [InlineData("")]
[InlineData("--development-live ")]
[InlineData(" --development-live")]
[InlineData("--DEVELOPMENT-LIVE")]
[InlineData("--development-live --another-option")]
- public void NonExactArgument_SkipsWithoutReadingOrMutating(string? arguments)
+ public void NonExactArgument_IsRejectedWithoutReadingOrMutating(string? arguments)
{
var platform = FakePlatform.Valid();
@@ -81,37 +86,56 @@ public sealed class DevelopmentLiveLaunchBootstrapTests
"ignored.json",
platform);
- Assert.Equal(DevelopmentLiveBootstrapStatus.Skipped, result.Status);
- Assert.Equal("development-live-not-requested", result.Code);
+ Assert.Equal(DevelopmentLiveBootstrapStatus.Rejected, result.Status);
+ Assert.Equal("development-live-arguments-invalid", result.Code);
+ Assert.False(result.IsLaunchAllowed);
+ Assert.True(result.IsRequested);
Assert.Equal(0, platform.ReadCount);
Assert.Empty(platform.Environment);
}
- [Fact]
- public void ReleaseBuild_SkipsExactArgumentWithoutReadingOrMutating()
+ [Theory]
+ [InlineData(true)]
+ [InlineData(false)]
+ public void GateALaunch_BypassesOrdinaryAuthorizationWithoutEnvironmentMutation(
+ bool isDebugBuild)
{
var platform = FakePlatform.Valid();
+ platform.Environment[
+ PlayoutLaunchAuthorization.GateACapabilityEnvironmentVariable] =
+ new string('A', 64);
+ var originalEnvironment = platform.Environment.ToArray();
var result = DevelopmentLiveLaunchBootstrap.TryApply(
- DevelopmentLiveLaunchBootstrap.ExactLaunchArgument,
- isDebugBuild: false,
- "ignored.json",
+ null,
+ isDebugBuild,
+ "must-not-be-read.json",
platform);
- Assert.Equal(DevelopmentLiveBootstrapStatus.Skipped, result.Status);
- Assert.Equal("development-live-debug-only", result.Code);
+ Assert.Equal(DevelopmentLiveBootstrapStatus.GateABypassed, result.Status);
+ Assert.Equal("development-live-gate-a-bypassed", result.Code);
+ Assert.True(result.IsLaunchAllowed);
+ Assert.False(result.IsRequested);
Assert.Equal(0, platform.ReadCount);
- Assert.Empty(platform.Environment);
+ Assert.Equal(originalEnvironment, platform.Environment);
}
- [Fact]
- public void DebugExactArgumentAndStrictFile_ApplyAllProcessGates()
+ [Theory]
+ [InlineData(null, true)]
+ [InlineData("", true)]
+ [InlineData("--development-live", true)]
+ [InlineData(null, false)]
+ [InlineData("", false)]
+ [InlineData("--development-live", false)]
+ public void NormalOrExactLaunchAndStrictFile_ApplyAllProcessGates(
+ string? arguments,
+ bool isDebugBuild)
{
var platform = FakePlatform.Valid();
var result = DevelopmentLiveLaunchBootstrap.TryApply(
- DevelopmentLiveLaunchBootstrap.ExactLaunchArgument,
- isDebugBuild: true,
+ arguments,
+ isDebugBuild,
"authorization.json",
platform);
@@ -188,6 +212,25 @@ public sealed class DevelopmentLiveLaunchBootstrapTests
Assert.Empty(platform.Environment);
}
+ [Fact]
+ public void InvalidAuthorizationFileRead_IsUnavailableAndDoesNotMutateEnvironment()
+ {
+ var platform = FakePlatform.Valid();
+ platform.ReadException = new InvalidDataException("sensitive authorization bytes");
+
+ var result = DevelopmentLiveLaunchBootstrap.TryApply(
+ null,
+ isDebugBuild: false,
+ "sensitive authorization path",
+ platform);
+
+ Assert.Equal(DevelopmentLiveBootstrapStatus.Rejected, result.Status);
+ Assert.Equal("development-live-authorization-unavailable", result.Code);
+ Assert.False(result.IsLaunchAllowed);
+ Assert.DoesNotContain("sensitive", result.Code, StringComparison.OrdinalIgnoreCase);
+ Assert.Empty(platform.Environment);
+ }
+
[Fact]
public void EnvironmentFailure_DisarmsEveryPreviousProcessValue()
{
@@ -196,7 +239,8 @@ public sealed class DevelopmentLiveLaunchBootstrapTests
InstalledK3dInteropMetadata.ApprovedNativeSha256EnvironmentVariable] = "old-native";
platform.Environment[
InstalledK3dInteropMetadata.ApprovedSha256EnvironmentVariable] = "old-interop";
- platform.Environment[DevelopmentLiveLaunchBootstrap.ModeEnvironmentVariable] = "DryRun";
+ platform.Environment[DevelopmentLiveLaunchBootstrap.ModeEnvironmentVariable] =
+ "stale-mode";
platform.Environment[PlayoutOptionsLoader.LiveAuthorizationEnvironmentVariable] =
"old-authorization";
platform.FailOnceOnSetName = DevelopmentLiveLaunchBootstrap.ModeEnvironmentVariable;
diff --git a/tests/MBN_STOCK_WEBVIEW.Playout.Tests/PlayoutOptionsLoaderTests.cs b/tests/MBN_STOCK_WEBVIEW.Playout.Tests/PlayoutOptionsLoaderTests.cs
index 4d13b06..71c3018 100644
--- a/tests/MBN_STOCK_WEBVIEW.Playout.Tests/PlayoutOptionsLoaderTests.cs
+++ b/tests/MBN_STOCK_WEBVIEW.Playout.Tests/PlayoutOptionsLoaderTests.cs
@@ -256,7 +256,7 @@ public sealed class PlayoutOptionsLoaderTests
}
[Fact]
- public void LoadDevelopmentLive_RejectsNonCanonicalJsonAndAllowlistExpansion()
+ public void LoadDevelopmentLive_RejectsWrongModeNonCanonicalJsonAndAllowlistExpansion()
{
using var environment = ClearedEnvironment();
var canonical = DevelopmentLiveBaseJson();
@@ -265,12 +265,16 @@ public sealed class PlayoutOptionsLoaderTests
string[] invalidConfigurations =
[
canonical.Replace(
+ "\"mode\": \"Live\"",
"\"mode\": \"DryRun\"",
- "\"Mode\": \"DryRun\"",
StringComparison.Ordinal),
canonical.Replace(
- "\"mode\": \"DryRun\"",
- "\"mode\": \"DryRun\", \"mode\": \"DryRun\"",
+ "\"mode\": \"Live\"",
+ "\"Mode\": \"Live\"",
+ StringComparison.Ordinal),
+ canonical.Replace(
+ "\"mode\": \"Live\"",
+ "\"mode\": \"Live\", \"mode\": \"Live\"",
StringComparison.Ordinal),
canonical.Replace(
"\"port\": 31001",
@@ -586,7 +590,7 @@ public sealed class PlayoutOptionsLoaderTests
private static string DevelopmentLiveBaseJson() =>
"""
{
- "mode": "DryRun",
+ "mode": "Live",
"host": "127.0.0.1",
"port": 31001,
"tcpMode": 1,