fix: allow safe fresh-clone DryRun builds

This commit is contained in:
2026-07-24 14:03:29 +09:00
parent b050f2f06c
commit 68a41194c9
15 changed files with 420 additions and 40 deletions

View File

@@ -204,6 +204,12 @@ test("appearance controls and accessibility preferences retain clear feedback",
test("workspace header keeps one concise title and the live native connection state", () => {
assert.match(markup,
/class="brand"[\s\S]*?class="brand-wordmark-plate"[\s\S]*?<img class="brand-wordmark" src="Brand\/logo\.png" width="176" height="43"[\s\S]*?alt="매일경제TV" draggable="false">[\s\S]*?class="brand-divider" aria-hidden="true"[\s\S]*?class="brand-product">V-STOCK<\/span>/);
assert.match(markup,
/class="brand-wordmark-fallback">매일경제TV<\/span>/);
assert.match(styles,
/\.brand-wordmark-plate\.wordmark-fallback-active \.brand-wordmark-fallback\s*\{[^}]*visibility:\s*visible/);
assert.match(styles,
/\.brand-wordmark-plate\.wordmark-fallback-active \.brand-wordmark\s*\{[^}]*display:\s*none/);
assert.doesNotMatch(markup, /class="brand-symbol"|class="brand-lockup"|>VRi</);
assert.match(markup,
/class="workspace-heading"[\s\S]*?class="workspace-heading-icon"[\s\S]*?<div class="workspace-heading-copy">[\s\S]*?<h1 id="workspace-title">[^<]+<\/h1>/);

View File

@@ -510,6 +510,15 @@ test("workspace navigation stores only the approved local presentation preferenc
assert.doesNotMatch(navigation, /postMessage|sessionStorage|send\s*\(/);
});
test("missing external wordmark uses the source-only text fallback", () => {
assert.match(navigation,
/brandWordmark\.addEventListener\("error", showBrandWordmarkFallback, \{ once: true \}\)/);
assert.match(navigation,
/brandWordmark\.complete && brandWordmark\.naturalWidth === 0/);
assert.match(navigation,
/classList\.add\("wordmark-fallback-active"\)/);
});
test("splitter clamps pointer and keyboard resizing and Escape restores the gesture", () => {
for (const declaration of [
/const scheduleMinimumPercent = 34/,

View File

@@ -88,6 +88,13 @@ public sealed class LegacyDevelopmentLiveStartupContractTests
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.Contains("args.Arguments", app, StringComparison.Ordinal);
Assert.Contains("Environment.GetCommandLineArgs()", app, StringComparison.Ordinal);
Assert.Contains("ResolveLaunchArguments(", app, StringComparison.Ordinal);

View File

@@ -268,6 +268,154 @@ public sealed class LegacyRuntimeAssetDeploymentContractTests
Assert.Single(rejectionTarget.Elements("Error"));
}
[Fact]
public void FreshCloneUsesIsolatedSourceOnlyOutputWithoutWeakeningFullBuilds()
{
var project = XDocument.Load(ProjectPath);
var sourceRootWasSpecified = Assert.Single(
project.Descendants("LegacyRuntimeSourceRootWasSpecified"));
Assert.Equal(
"'$(LegacyRuntimeSourceRoot)' != ''",
sourceRootWasSpecified.Attribute("Condition")?.Value);
var modes = project.Descendants("LegacyRuntimeAssetsMode").ToArray();
Assert.Equal(2, modes.Length);
var requiredMode = Assert.Single(
modes,
mode => string.Equals(mode.Value, "Required", StringComparison.Ordinal));
var requiredCondition = requiredMode.Attribute("Condition")?.Value;
Assert.Contains(
"$(LegacyRuntimeSourceRootWasSpecified)",
requiredCondition,
StringComparison.Ordinal);
Assert.Contains(
"'$(Configuration)' == 'Release'",
requiredCondition,
StringComparison.Ordinal);
Assert.Contains(
"$(GenerateAppxPackageOnBuild)",
requiredCondition,
StringComparison.Ordinal);
Assert.Contains("$(PublishAppxPackage)", requiredCondition, StringComparison.Ordinal);
var autoMode = Assert.Single(
modes,
mode => string.Equals(mode.Value, "Auto", StringComparison.Ordinal));
Assert.Equal(
"'$(LegacyRuntimeAssetsMode)' == ''",
autoMode.Attribute("Condition")?.Value);
var assetsEnabled = Assert.Single(
project.Descendants("LegacyRuntimeAssetsEnabled"),
element => (element.Attribute("Condition")?.Value ?? string.Empty)
.Contains("Exists('$(LegacyRuntimeSourceRoot)')", StringComparison.Ordinal));
Assert.Equal("true", assetsEnabled.Value);
var sourceOnlyOutput = Assert.Single(project.Descendants("BaseOutputPath"));
Assert.Equal(@"bin\SourceOnly\", sourceOnlyOutput.Value);
Assert.Contains(
"$(LegacyRuntimeAssetsEnabled)",
sourceOnlyOutput.Attribute("Condition")?.Value,
StringComparison.Ordinal);
var sourceOnlyIntermediate = Assert.Single(
project.Descendants("IntermediateOutputPath"));
Assert.Equal(
@"obj\SourceOnly\$(Platform)\$(Configuration)\",
sourceOnlyIntermediate.Value);
Assert.Contains(
"$(LegacyRuntimeAssetsEnabled)",
sourceOnlyIntermediate.Attribute("Condition")?.Value,
StringComparison.Ordinal);
var sourceOnlyConstants = Assert.Single(project.Descendants("DefineConstants"));
Assert.Contains(
"SOURCE_ONLY_RUNTIME",
sourceOnlyConstants.Value,
StringComparison.Ordinal);
Assert.Contains(
"$(LegacyRuntimeAssetsEnabled)",
sourceOnlyConstants.Attribute("Condition")?.Value,
StringComparison.Ordinal);
var validationTarget = Assert.Single(
project.Descendants("Target"),
element => string.Equals(
element.Attribute("Name")?.Value,
"ValidateLegacyRuntimeAssets",
StringComparison.Ordinal));
var sourceOnlyMessage = Assert.Single(validationTarget.Elements("Message"));
Assert.Contains(
"$(LegacyRuntimeAssetsMode)",
sourceOnlyMessage.Attribute("Condition")?.Value,
StringComparison.Ordinal);
Assert.Contains(
"source-only DryRun",
sourceOnlyMessage.Attribute("Text")?.Value,
StringComparison.Ordinal);
var runtimeValidationErrors = validationTarget.Elements("Error")
.Where(error => (error.Attribute("Condition")?.Value ?? string.Empty)
.Contains("Exists(", StringComparison.Ordinal))
.ToArray();
Assert.NotEmpty(runtimeValidationErrors);
Assert.All(runtimeValidationErrors, error =>
Assert.Contains(
"$(LegacyRuntimeAssetsValidationEnabled)",
error.Attribute("Condition")?.Value,
StringComparison.Ordinal));
string[] externalContentIncludes =
[
@"$(LegacyCutsSourceRoot)\**\*",
"@(LegacyPackagedResAsset)",
"@(LegacyFinancialPreviewAsset)",
"@(LegacyBrandAsset)"
];
Assert.All(externalContentIncludes, include =>
{
var content = Assert.Single(
project.Descendants("Content"),
element => string.Equals(
element.Attribute("Include")?.Value,
include,
StringComparison.Ordinal));
Assert.Contains(
"$(LegacyRuntimeAssetsEnabled)",
content.Attribute("Condition")?.Value,
StringComparison.Ordinal);
});
var databaseCopy = Assert.Single(
project.Descendants("Target"),
element => string.Equals(
element.Attribute("Name")?.Value,
"CopyLegacyDatabaseIniToLocalOutput",
StringComparison.Ordinal));
Assert.Contains(
"$(LegacyRuntimeAssetsEnabled)",
databaseCopy.Attribute("Condition")?.Value,
StringComparison.Ordinal);
var mainWindow = File.ReadAllText(Path.Combine(
RepositoryRoot,
"src",
"MBN_STOCK_WEBVIEW.LegacyParityApp",
"MainWindow.xaml.cs"));
Assert.Contains("#if SOURCE_ONLY_RUNTIME", mainWindow, StringComparison.Ordinal);
Assert.Contains(
"Source-only builds do not initialize a database runtime.",
mainWindow,
StringComparison.Ordinal);
var playout = File.ReadAllText(Path.Combine(
RepositoryRoot,
"src",
"MBN_STOCK_WEBVIEW.LegacyParityApp",
"MainWindow.Playout.cs"));
Assert.Contains("forceSafeDryRun: IsSourceOnlyBuild", playout,
StringComparison.Ordinal);
}
[Fact]
public void LegacyRuntimeSourceRootIsOverrideableAndValidatedBeforeBuild()
{

View File

@@ -62,6 +62,32 @@ public sealed class PlayoutOptionsLoaderTests
Assert.Null(options.MaximumAutomaticRefreshesPerTakeIn);
}
[Fact]
public void Load_SourceOnlyIgnoresLocalProfileOperatorRootsAndEnvironment()
{
using var environment = ClearedEnvironment()
.Set("MBN_STOCK_PLAYOUT_MODE", "Live")
.Set("MBN_STOCK_PLAYOUT_HOST", "configured-live-host")
.Set("MBN_STOCK_PLAYOUT_SCENE_DIRECTORY", "C:\\configured-live-scenes")
.Set(
PlayoutOptionsLoader.LiveAuthorizationEnvironmentVariable,
PlayoutOptionsLoader.RequiredLiveAuthorizationValue);
using var file = TemporaryJsonFile.Create("{ intentionally-invalid-json");
var options = PlayoutOptionsLoader.Load(
file.Path,
operatorSceneDirectory: "C:\\operator-scenes",
operatorBackgroundDirectory: "C:\\operator-backgrounds",
forceSafeDryRun: true);
Assert.Equal(PlayoutMode.DryRun, options.Mode);
Assert.Equal("127.0.0.1", options.Host);
Assert.Null(options.SceneDirectory);
Assert.Null(options.LegacyBackgroundDirectory);
Assert.False(options.TrustedLiveOutputEnabled);
Assert.Empty(options.TestSceneAllowlist);
}
[Fact]
public void ApplyExecutableAssetDefaults_UsesCutsBesideExecutable()
{