feat: make LegacyParityApp playout live-only

This commit is contained in:
2026-07-28 14:52:59 +09:00
parent 11d3849933
commit ea96a08ad5
23 changed files with 456 additions and 270 deletions

View File

@@ -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" +

View File

@@ -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",

View File

@@ -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);

View File

@@ -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,

View File

@@ -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;

View File

@@ -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,