feat: initialize existing development playout PCs

This commit is contained in:
2026-07-26 23:11:29 +09:00
parent 8b628908f0
commit 483b2785a0
18 changed files with 4029 additions and 74 deletions

View File

@@ -178,6 +178,127 @@ public sealed class LegacyIniDatabaseOptionsLoaderTests
Assert.Equal("selected.maria.invalid", runtime.Options.MariaDb.Host);
}
[Fact]
public void Runtime_DevelopmentLiveUsesOnlyExactLocalAppDataIni()
{
using var localApplicationData = TemporaryDirectory.Create();
var resourceDirectory = Path.Combine(
localApplicationData.Path,
"MBN_STOCK_WEBVIEW",
"Res");
Directory.CreateDirectory(resourceDirectory);
File.WriteAllBytes(
Path.Combine(resourceDirectory, "MmoneyCoder.ini"),
TemporaryLegacyIni.ValidUtf8Bytes());
const string environmentName = "MBN_STOCK_ORACLE_HOST";
var originalEnvironment = Environment.GetEnvironmentVariable(environmentName);
DatabaseRuntime runtime;
try
{
Environment.SetEnvironmentVariable(
environmentName,
"stale-environment.invalid");
runtime = DatabaseRuntime.CreateDevelopmentLiveLocalOverride(
localApplicationData.Path);
}
finally
{
Environment.SetEnvironmentVariable(
environmentName,
originalEnvironment);
}
Assert.Equal("oracle.dev.invalid", runtime.Options.Oracle.Host);
Assert.Equal("maria.dev.invalid", runtime.Options.MariaDb.Host);
}
[Fact]
public void Runtime_DevelopmentLiveFailsClosedWhenBothProtectedProfilesAreMissing()
{
using var localApplicationData = TemporaryDirectory.Create();
Directory.CreateDirectory(Path.Combine(
localApplicationData.Path,
"MBN_STOCK_WEBVIEW",
"Config"));
Assert.Throws<DatabaseConfigurationException>(
() => DatabaseRuntime.CreateDevelopmentLiveLocalOverride(
localApplicationData.Path));
}
[Fact]
public void Runtime_DevelopmentLiveFallsBackToProtectedLocalAppDataJson()
{
using var localApplicationData = TemporaryDirectory.Create();
var configurationDirectory = Path.Combine(
localApplicationData.Path,
"MBN_STOCK_WEBVIEW",
"Config");
Directory.CreateDirectory(configurationDirectory);
File.WriteAllText(
Path.Combine(configurationDirectory, "database.local.json"),
"""
{
"oracle": {
"host": "json.oracle.invalid",
"port": 1521,
"sid": "DEVORA",
"serviceName": null,
"userName": "oracle-user",
"password": "oracle-password"
},
"mariaDb": {
"host": "json.maria.invalid",
"port": 3306,
"database": "dev_stock",
"userName": "maria-user",
"password": "maria-password",
"tlsMode": "Required"
}
}
""");
var runtime = DatabaseRuntime.CreateDevelopmentLiveLocalOverride(
localApplicationData.Path);
Assert.Equal("json.oracle.invalid", runtime.Options.Oracle.Host);
Assert.Equal("json.maria.invalid", runtime.Options.MariaDb.Host);
Assert.Equal(MariaDbTlsMode.Required, runtime.Options.MariaDb.TlsMode);
}
[Fact]
public void Runtime_DevelopmentLiveRejectsNonOrdinaryIniTarget()
{
using var localApplicationData = TemporaryDirectory.Create();
var iniDirectory = Path.Combine(
localApplicationData.Path,
"MBN_STOCK_WEBVIEW",
"Res",
"MmoneyCoder.ini");
Directory.CreateDirectory(iniDirectory);
Assert.Throws<DatabaseConfigurationException>(
() => DatabaseRuntime.CreateDevelopmentLiveLocalOverride(
localApplicationData.Path));
}
[Fact]
public void Runtime_DevelopmentLiveRejectsNonOrdinaryJsonTarget()
{
using var localApplicationData = TemporaryDirectory.Create();
var jsonDirectory = Path.Combine(
localApplicationData.Path,
"MBN_STOCK_WEBVIEW",
"Config",
"database.local.json");
Directory.CreateDirectory(jsonDirectory);
Assert.Throws<DatabaseConfigurationException>(
() => DatabaseRuntime.CreateDevelopmentLiveLocalOverride(
localApplicationData.Path));
}
[Fact]
public void Load_OptionalRealLegacyIni_IsConfiguredWithoutPrintingValues()
{

View File

@@ -98,6 +98,113 @@ public sealed class LegacyDevelopmentLiveStartupContractTests
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(",
app,
StringComparison.Ordinal);
Assert.Contains(
"IsDevelopmentLiveLaunch = developmentLive.IsApplied;",
app,
StringComparison.Ordinal);
}
[Fact]
public void AppliedDevelopmentLiveUsesOnlyProtectedStartupInputs()
{
var window = File.ReadAllText(Path.Combine(ParityRoot, "MainWindow.xaml.cs"));
var playout = File.ReadAllText(Path.Combine(
ParityRoot,
"MainWindow.Playout.cs"));
Assert.Contains(
"_isDevelopmentLiveLaunch = App.IsDevelopmentLiveLaunch;",
window,
StringComparison.Ordinal);
Assert.Contains(
"_isDevelopmentLiveLaunchRequested = App.IsDevelopmentLiveLaunchRequested;",
window,
StringComparison.Ordinal);
Assert.Contains(
"? LegacyRuntimeStockCutMenuLoader.ResolveFromBaseDirectory(",
window,
StringComparison.Ordinal);
Assert.Contains(
"? null\n : _appliedOperatorSettings.ResourceDirectory;",
window.ReplaceLineEndings("\n"),
StringComparison.Ordinal);
Assert.Contains(
"DatabaseRuntime.CreateDevelopmentLiveLocalOverride()",
window,
StringComparison.Ordinal);
Assert.Contains(
"? PlayoutOptionsLoader.LoadDevelopmentLive(",
playout,
StringComparison.Ordinal);
Assert.Contains(
"IsSourceOnlyBuild ||\n _isDevelopmentLiveLaunchRequested",
playout.ReplaceLineEndings("\n"),
StringComparison.Ordinal);
}
[Fact]
public void DevelopmentLiveDatabaseFailureForcesDryRunAndSkipsAutomaticConnect()
{
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 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,
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,
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" +
" ? PlayoutOptionsLoader.LoadDevelopmentLive(",
playout,
StringComparison.Ordinal);
Assert.Contains(
"_isDevelopmentLiveLaunchRequested ||\n" +
" _developmentLiveStartupBlocked",
playout,
StringComparison.Ordinal);
Assert.Contains(
"if (!_developmentLiveStartupBlocked)\n" +
" {\n" +
" await ConnectPlayoutAsync(_lifetimeCancellation.Token);",
window,
StringComparison.Ordinal);
}
[Fact]

View File

@@ -333,7 +333,10 @@ public sealed class LegacyRuntimeAssetDeploymentContractTests
element.Attribute("Name")?.Value,
"ValidateLegacyRuntimeAssets",
StringComparison.Ordinal));
var sourceOnlyMessage = Assert.Single(validationTarget.Elements("Message"));
var sourceOnlyMessage = Assert.Single(
validationTarget.Elements("Message"),
message => (message.Attribute("Condition")?.Value ?? string.Empty)
.Contains("$(LegacyRuntimeAssetsMode)", StringComparison.Ordinal));
Assert.Contains(
"$(LegacyRuntimeAssetsMode)",
sourceOnlyMessage.Attribute("Condition")?.Value,
@@ -342,6 +345,21 @@ public sealed class LegacyRuntimeAssetDeploymentContractTests
"source-only DryRun",
sourceOnlyMessage.Attribute("Text")?.Value,
StringComparison.Ordinal);
Assert.Contains(
"Initialize-ExistingDevelopmentPc.ps1",
sourceOnlyMessage.Attribute("Text")?.Value,
StringComparison.Ordinal);
var fullRuntimeMessage = Assert.Single(
validationTarget.Elements("Message"),
message => (message.Attribute("Text")?.Value ?? string.Empty)
.Contains(
"Verified full legacy runtime assets are enabled",
StringComparison.Ordinal));
Assert.Contains(
"Verified full legacy runtime assets are enabled",
fullRuntimeMessage.Attribute("Text")?.Value,
StringComparison.Ordinal);
var runtimeValidationErrors = validationTarget.Elements("Error")
.Where(error => (error.Attribute("Condition")?.Value ?? string.Empty)
@@ -353,6 +371,11 @@ public sealed class LegacyRuntimeAssetDeploymentContractTests
"$(LegacyRuntimeAssetsValidationEnabled)",
error.Attribute("Condition")?.Value,
StringComparison.Ordinal));
Assert.Contains(
runtimeValidationErrors,
error => (error.Attribute("Text")?.Value ?? string.Empty).Contains(
"Initialize-ExistingDevelopmentPc.ps1",
StringComparison.Ordinal));
string[] externalContentIncludes =
[
@@ -602,6 +625,377 @@ public sealed class LegacyRuntimeAssetDeploymentContractTests
StringComparison.Ordinal);
}
[Fact]
public void ExistingDevelopmentPcInitializerUsesLocalAssetsWithoutPerformingLiveActions()
{
var initializer = File.ReadAllText(Path.Combine(
RepositoryRoot,
"scripts",
"Initialize-ExistingDevelopmentPc.ps1"));
Assert.Contains("[string] $LegacyRuntimeSourceRoot", initializer,
StringComparison.Ordinal);
Assert.Contains("[switch] $NoFolderPicker", initializer,
StringComparison.Ordinal);
Assert.Contains(
@"..\MBN_STOCK_N\MBN_STOCK_N\bin\Debug",
initializer,
StringComparison.Ordinal);
Assert.Contains(
"Test-DirectCutsAndResRootCandidate",
initializer,
StringComparison.Ordinal);
Assert.Contains(
"-Path $siblingCandidate",
initializer,
StringComparison.Ordinal);
Assert.Contains(
"System.Windows.Forms.FolderBrowserDialog",
initializer,
StringComparison.Ordinal);
Assert.Contains(
"$selectedLeaf.Equals('Cuts'",
initializer,
StringComparison.Ordinal);
Assert.Contains(
"$selectedLeaf.Equals('Res'",
initializer,
StringComparison.Ordinal);
Assert.Contains(
"single common parent of Cuts and Res",
initializer,
StringComparison.Ordinal);
Assert.Contains(
"must be on a ready local fixed drive",
initializer,
StringComparison.Ordinal);
Assert.Contains(
"must be outside and must not overlap this repository",
initializer,
StringComparison.Ordinal);
Assert.Contains("Assert-NoReparsePointInTree", initializer,
StringComparison.Ordinal);
Assert.Contains("'New-LegacyRuntimeBundle.ps1'", initializer,
StringComparison.Ordinal);
Assert.Contains("'Initialize-LegacyRuntimeBundle.ps1'", initializer,
StringComparison.Ordinal);
Assert.Contains(
"$existingRuntime.ManifestSha256.Equals(",
initializer,
StringComparison.Ordinal);
Assert.Contains(
"Assert-InstalledRuntimeMatchesManifest",
initializer,
StringComparison.Ordinal);
Assert.Contains(
"Installed runtime bundle contains an unexpected file",
initializer,
StringComparison.Ordinal);
Assert.Contains("[switch] $ReplaceRuntimeBinding", initializer,
StringComparison.Ordinal);
Assert.Contains("Set-RuntimeBindingAtomically", initializer,
StringComparison.Ordinal);
Assert.Contains(
"[IO.File]::Replace($temporary, $destination, $backup)",
initializer,
StringComparison.Ordinal);
var shadowInstall = initializer.IndexOf(
"-RepositoryRoot $shadowRepositoryRoot",
StringComparison.Ordinal);
var bindingReplace = initializer.IndexOf(
"Set-RuntimeBindingAtomically",
shadowInstall,
StringComparison.Ordinal);
Assert.True(shadowInstall >= 0);
Assert.True(bindingReplace > shadowInstall);
Assert.Contains("[string] $DatabaseIniPath", initializer,
StringComparison.Ordinal);
Assert.Contains("[IO.Path]::IsPathRooted($DatabaseIniPath)", initializer,
StringComparison.Ordinal);
Assert.Contains("[switch] $SkipDatabaseProfile", initializer,
StringComparison.Ordinal);
Assert.Contains("[switch] $ReplaceDatabaseProfile", initializer,
StringComparison.Ordinal);
Assert.Contains(@"Res\MmoneyCoder.ini", initializer,
StringComparison.Ordinal);
Assert.Contains("$maximumDatabaseIniLength = 1MB", initializer,
StringComparison.Ordinal);
Assert.Contains("Copy-DatabaseProfileAtomically", initializer,
StringComparison.Ordinal);
Assert.Contains(
"[IO.File]::Replace($temporaryPath, $Destination, $backupPath)",
initializer,
StringComparison.Ordinal);
Assert.Contains("SetAccessRuleProtection($true, $false)", initializer,
StringComparison.Ordinal);
Assert.Contains("'Inspect-K3DRegistration.ps1'", initializer,
StringComparison.Ordinal);
Assert.Contains("Get-VerifiedK3DRegistration -Reports $registrationReports",
initializer,
StringComparison.Ordinal);
Assert.Contains(
"[string] $registration.RegistryHive -cne 'HKLM'",
initializer,
StringComparison.Ordinal);
Assert.Contains(
"[string] $registration.RegistryView -cne 'Registry64'",
initializer,
StringComparison.Ordinal);
Assert.Contains(
"[string] $registration.CurrentUserOverrides -cne 'None'",
initializer,
StringComparison.Ordinal);
Assert.Contains(
"[string] $classReport.ThreadingModel -cne 'Apartment'",
initializer,
StringComparison.Ordinal);
Assert.DoesNotContain("Activator.CreateInstance", initializer,
StringComparison.Ordinal);
Assert.DoesNotContain("Connect(", initializer, StringComparison.Ordinal);
Assert.DoesNotContain("TAKE IN", initializer, StringComparison.Ordinal);
Assert.DoesNotContain("TAKE OUT", initializer, StringComparison.Ordinal);
Assert.Contains("[switch] $ConfigureDevelopmentLive", initializer,
StringComparison.Ordinal);
Assert.Contains("[switch] $ReplaceLiveConfig", initializer,
StringComparison.Ordinal);
Assert.Contains(
"-ConfigureDevelopmentLive requires the default Debug x64 build verification.",
initializer,
StringComparison.Ordinal);
Assert.Contains(
"Independent native and interop SHA-256 approvals are both required.",
initializer,
StringComparison.Ordinal);
Assert.Contains(
"Get-FileHash -LiteralPath $nativePath -Algorithm SHA256",
initializer,
StringComparison.Ordinal);
Assert.Contains(
"Get-FileHash -LiteralPath $interopPath -Algorithm SHA256",
initializer,
StringComparison.Ordinal);
Assert.Contains(
"$PlayoutHost -cne '127.0.0.1'",
initializer,
StringComparison.Ordinal);
Assert.Contains(
"Existing Development Live files differ from the exact requested",
initializer,
StringComparison.Ordinal);
Assert.Contains("& dotnet build $ProjectPath -c Debug '-p:Platform=x64'",
initializer,
StringComparison.Ordinal);
Assert.Contains(
"'-getProperty:LegacyRuntimeAssetsEnabled'",
initializer,
StringComparison.Ordinal);
Assert.Contains(
"unexpectedly resolved as SourceOnly",
initializer,
StringComparison.Ordinal);
Assert.Contains("Debug x64 payload count mismatch", initializer,
StringComparison.Ordinal);
Assert.Contains(
"Credential-bearing MmoneyCoder.ini entered the Debug build output.",
initializer,
StringComparison.Ordinal);
var runtimeWrite = initializer.LastIndexOf(
"Set-RuntimeBindingAtomically",
StringComparison.Ordinal);
var databaseWrite = initializer.LastIndexOf(
"Copy-DatabaseProfileAtomically",
StringComparison.Ordinal);
var build = initializer.LastIndexOf(
"Invoke-DebugBuildAndVerify",
StringComparison.Ordinal);
var cleanup = initializer.LastIndexOf(
"-Path $rollbackRoot",
StringComparison.Ordinal);
var liveWrite = initializer.LastIndexOf(
"& $liveInitializerPath @liveArguments",
StringComparison.Ordinal);
Assert.True(runtimeWrite >= 0);
Assert.True(databaseWrite > runtimeWrite);
Assert.True(build > databaseWrite);
Assert.True(cleanup > build);
Assert.True(liveWrite > cleanup);
}
[Fact]
public void ExistingDevelopmentPcInitializerRollsBackAndReissuesLiveFailClosed()
{
var initializer = File.ReadAllText(Path.Combine(
RepositoryRoot,
"scripts",
"Initialize-ExistingDevelopmentPc.ps1"));
Assert.Contains("function New-FileRollbackSnapshot", initializer,
StringComparison.Ordinal);
Assert.Contains("function Restore-FileRollbackSnapshot", initializer,
StringComparison.Ordinal);
Assert.Contains("OriginalBytes = [IO.File]::ReadAllBytes($backupPath)",
initializer,
StringComparison.Ordinal);
Assert.Contains("AccessSddl = $accessSddl", initializer,
StringComparison.Ordinal);
Assert.Contains(
"$Description rollback backup failed its immediate ACL/hash verification.",
initializer,
StringComparison.Ordinal);
Assert.Contains(
"Previous database profile backup failed immediate ACL/hash verification.",
initializer,
StringComparison.Ordinal);
Assert.Contains(
"$Description rollback did not restore the exact bytes and ACL.",
initializer,
StringComparison.Ordinal);
Assert.Contains(
"Set-UserOnlyDirectoryAcl -Path $rollbackRoot",
initializer,
StringComparison.Ordinal);
var databaseMutation = initializer.LastIndexOf(
"$databaseMutationStarted = $true",
StringComparison.Ordinal);
var databaseWrite = initializer.LastIndexOf(
"Copy-DatabaseProfileAtomically",
StringComparison.Ordinal);
var databaseRollback = initializer.LastIndexOf(
"-Snapshot $databaseSnapshot",
StringComparison.Ordinal);
var runtimeMutation = initializer.LastIndexOf(
"$runtimeBindingMutationStarted = $true",
StringComparison.Ordinal);
var runtimeWrite = initializer.LastIndexOf(
"Set-RuntimeBindingAtomically",
StringComparison.Ordinal);
var runtimeRollback = initializer.LastIndexOf(
"-Snapshot $propsSnapshot",
StringComparison.Ordinal);
Assert.True(databaseMutation >= 0 && databaseMutation < databaseWrite);
Assert.True(runtimeMutation >= 0 && runtimeMutation < runtimeWrite);
Assert.True(databaseRollback > databaseWrite);
Assert.True(runtimeRollback > runtimeWrite);
Assert.Contains("Original failure:", initializer, StringComparison.Ordinal);
Assert.Contains("Secondary recovery failure(s):", initializer,
StringComparison.Ordinal);
Assert.Contains("function Assert-ExactObjectProperties", initializer,
StringComparison.Ordinal);
Assert.Contains(
"-Description 'Runtime bundle creation result'",
initializer,
StringComparison.Ordinal);
Assert.Contains(
"-Description 'Runtime bundle initialization result'",
initializer,
StringComparison.Ordinal);
Assert.Contains(
"-Description 'K3D registration inspection result'",
initializer,
StringComparison.Ordinal);
Assert.Contains(
"Runtime bundle creation result paths escaped or changed the exact output contract.",
initializer,
StringComparison.Ordinal);
Assert.Contains(
"Runtime bundle initialization result paths or hashes violate containment.",
initializer,
StringComparison.Ordinal);
Assert.Contains("function Get-VerifiedRuntimeManifest", initializer,
StringComparison.Ordinal);
Assert.Contains(
"duplicate/extra/case/type/format changes are rejected.",
initializer,
StringComparison.Ordinal);
Assert.Contains(
"function Assert-DebugRuntimePayloadMatchesManifest",
initializer,
StringComparison.Ordinal);
Assert.Contains(
"Debug x64 runtime payload differs from the manifest:",
initializer,
StringComparison.Ordinal);
Assert.Contains(
"Debug x64 runtime output contains an unexpected file:",
initializer,
StringComparison.Ordinal);
Assert.Contains(
"Debug x64 runtime output does not match the exact manifest file set.",
initializer,
StringComparison.Ordinal);
Assert.Contains(
"-Root $targetRoot",
initializer,
StringComparison.Ordinal);
Assert.Contains("function Test-ExactUtf8Text", initializer,
StringComparison.Ordinal);
Assert.Contains(
"case changes, type coercion, reordered properties",
initializer,
StringComparison.Ordinal);
Assert.DoesNotContain("Get-JsonContractText", initializer,
StringComparison.Ordinal);
var livePreflightStart = initializer.IndexOf(
"function Get-DevelopmentLivePreflight",
StringComparison.Ordinal);
var livePreflightEnd = initializer.IndexOf(
"function Copy-DatabaseProfileAtomically",
livePreflightStart,
StringComparison.Ordinal);
Assert.True(livePreflightStart >= 0 && livePreflightEnd > livePreflightStart);
var livePreflightFunction = initializer[livePreflightStart..livePreflightEnd];
Assert.DoesNotContain("NeedsWrite = $false", livePreflightFunction,
StringComparison.Ordinal);
Assert.Contains("DevelopmentLiveReused = $false", initializer,
StringComparison.Ordinal);
var firstPersistentLiveInvalidation = initializer.IndexOf(
"# Invalidate the previous one-launch authorization before database/runtime",
StringComparison.Ordinal);
var setupDirectoryCreation = initializer.IndexOf(
"[IO.Directory]::CreateDirectory($setupParent)",
StringComparison.Ordinal);
var buildVerification = initializer.LastIndexOf(
"Invoke-DebugBuildAndVerify",
StringComparison.Ordinal);
var setupCleanup = initializer.LastIndexOf(
"-Path $rollbackRoot",
StringComparison.Ordinal);
var liveIssue = initializer.LastIndexOf(
"$liveResults = @(& $liveInitializerPath @liveArguments)",
StringComparison.Ordinal);
var liveJsonVerification = initializer.LastIndexOf(
"-ExpectedJson ([string] $livePreflight.AuthorizationJson)",
StringComparison.Ordinal);
var failClosedLiveRemoval = initializer.LastIndexOf(
"Remove-DevelopmentLiveAuthorizationFailClosed",
StringComparison.Ordinal);
Assert.True(firstPersistentLiveInvalidation >= 0);
Assert.True(firstPersistentLiveInvalidation < setupDirectoryCreation);
Assert.True(buildVerification < setupCleanup);
Assert.True(setupCleanup < liveIssue);
Assert.True(liveIssue < liveJsonVerification);
Assert.True(liveJsonVerification < failClosedLiveRemoval);
Assert.Contains(
"Development Live initializer returned unexpected success-stream output.",
initializer,
StringComparison.Ordinal);
Assert.Contains(
"$Description failed its exact JSON/ACL verification.",
initializer,
StringComparison.Ordinal);
}
[Fact]
public void RepositoryDoesNotContainCopiedLegacyRuntimeTrees()
{

View File

@@ -189,7 +189,7 @@ public sealed class DevelopmentLiveLaunchBootstrapTests
}
[Fact]
public void EnvironmentFailure_RestoresEveryPreviousProcessValue()
public void EnvironmentFailure_DisarmsEveryPreviousProcessValue()
{
var platform = FakePlatform.Valid();
platform.Environment[
@@ -209,20 +209,32 @@ public sealed class DevelopmentLiveLaunchBootstrapTests
Assert.Equal(DevelopmentLiveBootstrapStatus.Rejected, result.Status);
Assert.Equal("development-live-environment-failed", result.Code);
Assert.Equal(
"old-native",
platform.Environment[
InstalledK3dInteropMetadata.ApprovedNativeSha256EnvironmentVariable]);
Assert.Equal(
"old-interop",
platform.Environment[
InstalledK3dInteropMetadata.ApprovedSha256EnvironmentVariable]);
Assert.Equal(
"DryRun",
platform.Environment[DevelopmentLiveLaunchBootstrap.ModeEnvironmentVariable]);
Assert.Equal(
"old-authorization",
platform.Environment[PlayoutOptionsLoader.LiveAuthorizationEnvironmentVariable]);
Assert.Empty(platform.Environment);
}
[Fact]
public void RejectedExactDebugRequest_DisarmsInheritedLiveGates()
{
var platform = new FakePlatform(Encoding.UTF8.GetBytes("{}"));
platform.Environment[
InstalledK3dInteropMetadata.ApprovedNativeSha256EnvironmentVariable] =
new string('A', 64);
platform.Environment[
InstalledK3dInteropMetadata.ApprovedSha256EnvironmentVariable] =
new string('B', 64);
platform.Environment[DevelopmentLiveLaunchBootstrap.ModeEnvironmentVariable] =
DevelopmentLiveLaunchBootstrap.RequiredMode;
platform.Environment[PlayoutOptionsLoader.LiveAuthorizationEnvironmentVariable] =
PlayoutOptionsLoader.RequiredLiveAuthorizationValue;
var result = DevelopmentLiveLaunchBootstrap.TryApply(
DevelopmentLiveLaunchBootstrap.ExactLaunchArgument,
isDebugBuild: true,
"authorization.json",
platform);
Assert.Equal(DevelopmentLiveBootstrapStatus.Rejected, result.Status);
Assert.Empty(platform.Environment);
}
public static TheoryData<string> InvalidAuthorizationJson() => new()

View File

@@ -88,6 +88,210 @@ public sealed class PlayoutOptionsLoaderTests
Assert.Empty(options.TestSceneAllowlist);
}
[Fact]
public void Load_RejectedDevelopmentLiveRequestCannotInheritLiveEnvironment()
{
using var environment = ClearedEnvironment()
.Set("MBN_STOCK_PLAYOUT_MODE", "Live")
.Set("MBN_STOCK_PLAYOUT_HOST", "127.0.0.1")
.Set(
PlayoutOptionsLoader.LiveAuthorizationEnvironmentVariable,
PlayoutOptionsLoader.RequiredLiveAuthorizationValue);
using var file = TemporaryJsonFile.Create(DevelopmentLiveBaseJson());
var options = PlayoutOptionsLoader.Load(
file.Path,
operatorSceneDirectory: "C:\\operator-scenes",
operatorBackgroundDirectory: "C:\\operator-backgrounds",
forceSafeDryRun: true);
var validated = ValidatedPlayoutOptions.Create(options);
Assert.Equal(PlayoutMode.DryRun, validated.Mode);
Assert.False(validated.TrustedLiveOutputEnabled);
Assert.Null(validated.SceneDirectory);
Assert.Empty(validated.TestSceneAllowlist);
}
[Fact]
public void LoadDevelopmentLive_IgnoresEnvironmentAndPinsExecutableCuts()
{
using var environment = ClearedEnvironment()
.Set("MBN_STOCK_PLAYOUT_MODE", "Disabled")
.Set("MBN_STOCK_PLAYOUT_HOST", "stale-remote-host.invalid")
.Set("MBN_STOCK_PLAYOUT_PORT", "32001")
.Set("MBN_STOCK_PLAYOUT_OUTPUT_CHANNEL", "99")
.Set("MBN_STOCK_PLAYOUT_SCENE_DIRECTORY", "C:\\stale-scenes")
.Set(
"MBN_STOCK_PLAYOUT_LEGACY_BACKGROUND_DIRECTORY",
"C:\\stale-backgrounds")
.Set("MBN_STOCK_PLAYOUT_RECONNECT_ENABLED", "true")
.Set("MBN_STOCK_PLAYOUT_MAXIMUM_RECONNECT_ATTEMPTS", "9");
using var file = TemporaryJsonFile.Create(DevelopmentLiveBaseJson());
var executableRoot = Path.Combine(
Path.GetTempPath(),
Guid.NewGuid().ToString("N"));
var cuts = Path.Combine(executableRoot, "Cuts");
Directory.CreateDirectory(cuts);
try
{
var options = PlayoutOptionsLoader.LoadDevelopmentLive(
file.Path,
executableRoot);
Assert.True(options.DevelopmentLiveBootstrapApplied);
Assert.Equal(PlayoutMode.Live, options.Mode);
Assert.Equal("127.0.0.1", options.Host);
Assert.Equal(31001, options.Port);
Assert.Equal(7, options.OutputChannel);
Assert.Equal(Path.GetFullPath(cuts), options.SceneDirectory);
Assert.Equal(
Path.GetFullPath(cuts),
options.DevelopmentLiveExecutableSceneDirectory);
Assert.Null(options.LegacyBackgroundDirectory);
Assert.False(options.ReconnectEnabled);
Assert.Equal(0, options.MaximumReconnectAttempts);
var validated = ValidatedPlayoutOptions.Create(options);
Assert.Equal("127.0.0.1", validated.Host);
Assert.Equal(Path.GetFullPath(cuts), validated.SceneDirectory);
Assert.False(validated.ReconnectEnabled);
Assert.Equal(0, validated.MaximumReconnectAttempts);
}
finally
{
Directory.Delete(executableRoot, recursive: true);
}
}
[Fact]
public void LoadDevelopmentLive_RejectsBaseProfileThatCanReconnect()
{
using var environment = ClearedEnvironment();
using var file = TemporaryJsonFile.Create(
DevelopmentLiveBaseJson().Replace(
"\"reconnectEnabled\": false",
"\"reconnectEnabled\": true",
StringComparison.Ordinal));
var exception = Assert.Throws<PlayoutConfigurationException>(
() => PlayoutOptionsLoader.LoadDevelopmentLive(file.Path, "C:\\runtime"));
Assert.Contains(
"local configuration is invalid",
exception.Message,
StringComparison.Ordinal);
}
[Fact]
public void Validate_DevelopmentLiveRequiresLiteralLoopbackAtFinalBoundary()
{
using var environment = ClearedEnvironment();
using var file = TemporaryJsonFile.Create(DevelopmentLiveBaseJson());
var executableRoot = Path.Combine(
Path.GetTempPath(),
Guid.NewGuid().ToString("N"));
Directory.CreateDirectory(Path.Combine(executableRoot, "Cuts"));
try
{
var options = PlayoutOptionsLoader.LoadDevelopmentLive(
file.Path,
executableRoot);
options.Host = "playout.dev.invalid";
var exception = Assert.Throws<PlayoutConfigurationException>(
() => ValidatedPlayoutOptions.Create(options));
Assert.Contains(
nameof(PlayoutOptions.Host),
exception.Message,
StringComparison.Ordinal);
}
finally
{
Directory.Delete(executableRoot, recursive: true);
}
}
[Fact]
public void LoadDevelopmentLive_RejectsNonCanonicalJsonAndAllowlistExpansion()
{
using var environment = ClearedEnvironment();
var canonical = DevelopmentLiveBaseJson();
var closingBrace = canonical.LastIndexOf('}');
Assert.True(closingBrace > 0);
string[] invalidConfigurations =
[
canonical.Replace(
"\"mode\": \"DryRun\"",
"\"Mode\": \"DryRun\"",
StringComparison.Ordinal),
canonical.Replace(
"\"mode\": \"DryRun\"",
"\"mode\": \"DryRun\", \"mode\": \"DryRun\"",
StringComparison.Ordinal),
canonical.Replace(
"\"port\": 31001",
"\"port\": \"31001\"",
StringComparison.Ordinal),
canonical[..closingBrace] + ",\n}",
canonical.Replace("{", "{/* comment */", StringComparison.Ordinal),
canonical[..closingBrace] + ",\n \"unexpected\": true\n}",
canonical.Replace(
"\"N5001\"",
"\"N5001\", \"9999\"",
StringComparison.Ordinal)
];
foreach (var json in invalidConfigurations)
{
using var file = TemporaryJsonFile.Create(json);
var exception = Assert.Throws<PlayoutConfigurationException>(
() => PlayoutOptionsLoader.LoadDevelopmentLive(
file.Path,
"C:\\runtime"));
Assert.Equal(
"Development Live local configuration is invalid.",
exception.Message);
}
}
[Fact]
public void Validate_DevelopmentLiveRejectsSceneRootReplacementAfterLoading()
{
using var environment = ClearedEnvironment();
using var file = TemporaryJsonFile.Create(DevelopmentLiveBaseJson());
var executableRoot = Path.Combine(
Path.GetTempPath(),
Guid.NewGuid().ToString("N"));
var replacementRoot = Path.Combine(
Path.GetTempPath(),
Guid.NewGuid().ToString("N"));
Directory.CreateDirectory(Path.Combine(executableRoot, "Cuts"));
Directory.CreateDirectory(replacementRoot);
try
{
var options = PlayoutOptionsLoader.LoadDevelopmentLive(
file.Path,
executableRoot);
options.SceneDirectory = replacementRoot;
var exception = Assert.Throws<PlayoutConfigurationException>(
() => ValidatedPlayoutOptions.Create(options));
Assert.Contains(
"protected startup contract",
exception.Message,
StringComparison.Ordinal);
}
finally
{
Directory.Delete(executableRoot, recursive: true);
Directory.Delete(replacementRoot, recursive: true);
}
}
[Fact]
public void ApplyExecutableAssetDefaults_UsesCutsBesideExecutable()
{
@@ -335,4 +539,43 @@ public sealed class PlayoutOptionsLoaderTests
return result;
}
private static string DevelopmentLiveBaseJson() =>
"""
{
"mode": "DryRun",
"host": "127.0.0.1",
"port": 31001,
"tcpMode": 1,
"clientPort": 0,
"sceneDirectory": null,
"outputChannel": 7,
"layoutIndex": 10,
"legacySceneFadeDuration": 6,
"legacySceneBackgroundKind": "None",
"legacySceneBackgroundAssetPath": null,
"legacySceneBackgroundVideoLoopCount": 2004,
"legacySceneBackgroundVideoLoopInfinite": true,
"legacyBackgroundDirectory": null,
"testProcessWindowTitlePattern": null,
"testSceneAllowlist": [
"5001", "5006", "5011", "5016", "50160", "5023", "5024", "5025",
"5026", "5029", "5032", "5037", "5068", "5070", "5072", "5074",
"5076", "5077", "5078", "5079", "5080", "5081", "5082", "5083",
"5084", "5085", "5086", "50860", "5087", "5088", "6001", "6067",
"8001", "8002", "8003", "8018", "8032", "8035", "8040", "8046",
"8051", "8056", "8061", "8067", "N5001"
],
"trustedLiveOutputEnabled": true,
"queueCapacity": 64,
"connectTimeoutMilliseconds": 5000,
"operationTimeoutMilliseconds": 5000,
"disconnectTimeoutMilliseconds": 3000,
"processPollIntervalMilliseconds": 1000,
"reconnectDelayMilliseconds": 1000,
"maximumReconnectAttempts": 0,
"reconnectEnabled": false,
"maximumAutomaticRefreshesPerTakeIn": 0
}
""";
}

View File

@@ -258,6 +258,21 @@ public sealed class PlayoutSafetyValidationTests : IDisposable
Assert.Contains(nameof(PlayoutOptions.Host), exception.Message, StringComparison.Ordinal);
}
[Fact]
public void Validate_OrdinaryLiveModeRetainsConfiguredRemoteEndpointBehavior()
{
var options = ValidTestOptions();
options.Mode = PlayoutMode.Live;
options.Host = "playout.dev.invalid";
options.OutputChannel = null;
options.TestProcessWindowTitlePattern = null;
options.TrustedLiveOutputEnabled = true;
var validated = ValidatedPlayoutOptions.Create(options);
Assert.Equal("playout.dev.invalid", validated.Host);
}
[Theory]
[InlineData(".*")]
[InlineData("PGM")]