feat: complete legacy UI and playout parity migration

This commit is contained in:
2026-07-15 13:11:38 +09:00
parent f14800656b
commit 6e0c275fdd
108 changed files with 43738 additions and 560 deletions

View File

@@ -0,0 +1,144 @@
#nullable enable
using MBN_STOCK_WEBVIEW.DbWriteSmoke;
namespace MBN_STOCK_WEBVIEW.DbWriteSmoke.Tests;
public sealed class DevelopmentDatabaseSmokeScenarioTests
{
[Fact]
public async Task SuccessfulScenario_InvokesEachOperationExactlyOnceInOrder()
{
var calls = new List<string>();
var scenario = CreateScenario(calls);
var result = await scenario.RunAsync();
Assert.True(result.Passed);
Assert.True(result.CleanupAttempted);
Assert.True(result.CleanupVerified);
Assert.Equal(
["preflight", "create", "create-readback", "save", "save-readback", "cleanup", "absent"],
calls);
}
[Fact]
public async Task UnknownCreate_DoesNotRetryCleanupOrContinue()
{
var calls = new List<string>();
var scenario = CreateScenario(
calls,
create: _ =>
{
calls.Add("create");
return Task.FromException(new UnknownTestException());
});
var result = await scenario.RunAsync();
Assert.False(result.Passed);
Assert.True(result.OutcomeUnknown);
Assert.Equal(nameof(UnknownTestException), result.FailureClass);
Assert.True(result.ManualCleanupMayBeRequired);
Assert.False(result.CleanupAttempted);
Assert.Equal(["preflight", "create"], calls);
}
[Fact]
public async Task UnknownSave_DoesNotIssueReadbackOrCleanupMutation()
{
var calls = new List<string>();
var scenario = CreateScenario(
calls,
save: _ =>
{
calls.Add("save");
return Task.FromException(new UnknownTestException());
});
var result = await scenario.RunAsync();
Assert.False(result.Passed);
Assert.True(result.OutcomeUnknown);
Assert.True(result.ManualCleanupMayBeRequired);
Assert.False(result.CleanupAttempted);
Assert.Equal(["preflight", "create", "create-readback", "save"], calls);
}
[Fact]
public async Task KnownReadbackFailure_StillCleansKnownCreatedRecordInFinally()
{
var calls = new List<string>();
var scenario = CreateScenario(
calls,
verifyCreate: _ =>
{
calls.Add("create-readback");
return Task.FromException(new InvalidOperationException("bad readback"));
});
var result = await scenario.RunAsync();
Assert.False(result.Passed);
Assert.False(result.OutcomeUnknown);
Assert.Equal(nameof(InvalidOperationException), result.FailureClass);
Assert.Equal(DevelopmentDatabaseSmokeStage.CreateReadback, result.FailureStage);
Assert.True(result.CleanupAttempted);
Assert.True(result.CleanupVerified);
Assert.Equal(
["preflight", "create", "create-readback", "cleanup", "absent"],
calls);
}
[Fact]
public async Task UnknownCleanup_IsNotRetriedAndSkipsAbsenceRead()
{
var calls = new List<string>();
var scenario = CreateScenario(
calls,
cleanup: _ =>
{
calls.Add("cleanup");
return Task.FromException(new UnknownTestException());
});
var result = await scenario.RunAsync();
Assert.False(result.Passed);
Assert.True(result.OutcomeUnknown);
Assert.Equal(DevelopmentDatabaseSmokeStage.Cleanup, result.FailureStage);
Assert.True(result.CleanupAttempted);
Assert.False(result.CleanupVerified);
Assert.Equal(
["preflight", "create", "create-readback", "save", "save-readback", "cleanup"],
calls);
}
private static DevelopmentDatabaseSmokeScenario CreateScenario(
List<string> calls,
Func<CancellationToken, Task>? create = null,
Func<CancellationToken, Task>? verifyCreate = null,
Func<CancellationToken, Task>? save = null,
Func<CancellationToken, Task>? cleanup = null) =>
new(
"fake production workflow",
Record(calls, "preflight"),
create ?? Record(calls, "create"),
verifyCreate ?? Record(calls, "create-readback"),
save ?? Record(calls, "save"),
Record(calls, "save-readback"),
cleanup ?? Record(calls, "cleanup"),
Record(calls, "absent"),
exception => exception is UnknownTestException);
private static Func<CancellationToken, Task> Record(
ICollection<string> calls,
string value) =>
_ =>
{
calls.Add(value);
return Task.CompletedTask;
};
private sealed class UnknownTestException : Exception;
}

View File

@@ -0,0 +1,77 @@
#nullable enable
using System.Data;
using MBN_STOCK_WEBVIEW.DbWriteSmoke;
using MMoneyCoderSharp.Data;
namespace MBN_STOCK_WEBVIEW.DbWriteSmoke.Tests;
public sealed class ExistingChildRowGuardTests
{
[Fact]
public async Task ZeroCount_UsesBoundProductionSourceSpecificPreflights()
{
var executor = new RecordingExecutor("0");
var guard = new ExistingChildRowGuard(executor);
await guard.EnsureNamedPlaylistChildrenAbsentAsync("00000042", default);
await guard.EnsureThemeChildrenAbsentAsync(ThemeMarket.Krx, "00000043", default);
await guard.EnsureThemeChildrenAbsentAsync(ThemeMarket.Nxt, "00000044", default);
await guard.EnsureExpertChildrenAbsentAsync("0045", default);
Assert.Equal(4, executor.Calls.Count);
Assert.Equal(
[DataSourceKind.Oracle, DataSourceKind.Oracle, DataSourceKind.MariaDb, DataSourceKind.Oracle],
executor.Calls.Select(call => call.Source));
Assert.All(executor.Calls, call =>
{
var parameter = Assert.Single(call.Spec.Parameters);
Assert.Equal("parent_code", parameter.Name);
Assert.Equal(DbType.String, parameter.DbType);
Assert.DoesNotContain((string)parameter.Value!, call.Spec.Sql, StringComparison.Ordinal);
});
}
[Fact]
public async Task ExistingOrInvalidChildCount_FailsClosedBeforeMutation()
{
var guard = new ExistingChildRowGuard(new RecordingExecutor("1"));
await Assert.ThrowsAsync<InvalidOperationException>(() =>
guard.EnsureNamedPlaylistChildrenAbsentAsync("00000042", default));
}
private sealed class RecordingExecutor(string count) : IDataQueryExecutor
{
public List<QueryCall> Calls { get; } = [];
public DataTable Execute(DataSourceKind source, string tableName, string query) =>
throw new NotSupportedException();
public Task<DataTable> ExecuteAsync(
DataSourceKind source,
string tableName,
string query,
CancellationToken cancellationToken = default) =>
throw new NotSupportedException();
public Task<DataTable> ExecuteAsync(
DataSourceKind source,
string tableName,
DataQuerySpec query,
CancellationToken cancellationToken = default)
{
cancellationToken.ThrowIfCancellationRequested();
Calls.Add(new QueryCall(source, tableName, query));
var table = new DataTable();
table.Columns.Add("ROW_COUNT", typeof(string));
table.Rows.Add(count);
return Task.FromResult(table);
}
}
private sealed record QueryCall(
DataSourceKind Source,
string QueryName,
DataQuerySpec Spec);
}

View File

@@ -0,0 +1 @@
global using Xunit;

View File

@@ -0,0 +1,26 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<IsPackable>false</IsPackable>
<IsTestProject>true</IsTestProject>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
<AssemblyName>MBN_STOCK_WEBVIEW.DbWriteSmoke.Tests</AssemblyName>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.14.1" />
<PackageReference Include="xunit" Version="2.9.3" />
<PackageReference Include="xunit.runner.visualstudio" Version="3.1.5">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\tools\MBN_STOCK_WEBVIEW.DbWriteSmoke\MBN_STOCK_WEBVIEW.DbWriteSmoke.csproj" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,115 @@
#nullable enable
using MBN_STOCK_WEBVIEW.DbWriteSmoke;
namespace MBN_STOCK_WEBVIEW.DbWriteSmoke.Tests;
public sealed class SmokeCommandLineTests
{
[Fact]
public async Task DefaultInvocation_DoesNotCreatePlanOrTouchDatabase()
{
var planCalls = 0;
using var output = new StringWriter();
var exitCode = await DevelopmentDatabaseWriteSmokeApp.RunAsync(
[],
_ =>
{
++planCalls;
throw new InvalidOperationException("must not load configuration");
},
output);
Assert.Equal(DevelopmentDatabaseWriteSmokeApp.NotAcknowledgedExitCode, exitCode);
Assert.Equal(0, planCalls);
Assert.Contains("no database call", output.ToString(), StringComparison.OrdinalIgnoreCase);
}
[Fact]
public async Task ExactAcknowledgement_RunsPlanAndRedactsFailureDetails()
{
var secret = "SECRET_CREDENTIAL_OR_VALUE";
var planCalls = 0;
var scenario = Scenario(
create: _ => Task.FromException(new UnknownTestException(secret)));
using var output = new StringWriter();
var exitCode = await DevelopmentDatabaseWriteSmokeApp.RunAsync(
[SmokeCommandLine.WriteAcknowledgement],
_ =>
{
++planCalls;
return [scenario];
},
output);
Assert.Equal(DevelopmentDatabaseWriteSmokeApp.OutcomeUnknownExitCode, exitCode);
Assert.Equal(1, planCalls);
Assert.DoesNotContain(secret, output.ToString(), StringComparison.Ordinal);
Assert.Contains("QUARANTINED", output.ToString(), StringComparison.Ordinal);
}
[Theory]
[InlineData("--i-understand-this-writes-development-db")]
[InlineData("--I-UNDERSTAND-THIS-WRITES-DEVELOPMENT-DATABASE")]
[InlineData("--i-understand-this-writes-development-database --extra")]
public async Task NearMissAcknowledgement_IsRejectedWithoutPlan(string commandLine)
{
var planCalls = 0;
using var output = new StringWriter();
var exitCode = await DevelopmentDatabaseWriteSmokeApp.RunAsync(
commandLine.Split(' ', StringSplitOptions.RemoveEmptyEntries),
_ =>
{
++planCalls;
return [];
},
output);
Assert.Equal(DevelopmentDatabaseWriteSmokeApp.NotAcknowledgedExitCode, exitCode);
Assert.Equal(0, planCalls);
}
[Fact]
public async Task OutcomeUnknown_StopsAllLaterScenarios()
{
var laterCalls = 0;
var unknown = Scenario(
create: _ => Task.FromException(new UnknownTestException("hidden")));
var later = Scenario(
preflight: _ =>
{
++laterCalls;
return Task.CompletedTask;
});
using var output = new StringWriter();
var exitCode = await DevelopmentDatabaseWriteSmokeApp.RunAsync(
[SmokeCommandLine.WriteAcknowledgement],
_ => [unknown, later],
output);
Assert.Equal(DevelopmentDatabaseWriteSmokeApp.OutcomeUnknownExitCode, exitCode);
Assert.Equal(0, laterCalls);
}
private static DevelopmentDatabaseSmokeScenario Scenario(
Func<CancellationToken, Task>? preflight = null,
Func<CancellationToken, Task>? create = null) =>
new(
"test scenario",
preflight ?? Completed,
create ?? Completed,
Completed,
save: null,
verifySave: null,
Completed,
Completed,
exception => exception is UnknownTestException);
private static Task Completed(CancellationToken _) => Task.CompletedTask;
private sealed class UnknownTestException(string message) : Exception(message);
}

View File

@@ -0,0 +1,33 @@
#nullable enable
using MBN_STOCK_WEBVIEW.DbWriteSmoke;
namespace MBN_STOCK_WEBVIEW.DbWriteSmoke.Tests;
public sealed class SmokeRunIdentityTests
{
[Fact]
public void Create_ProducesCollisionResistantSafeBoundedPrefix()
{
var identity = SmokeRunIdentity.Create(
DateTimeOffset.Parse("2026-07-15T10:11:12+09:00"),
Guid.Parse("01234567-89ab-cdef-0123-456789abcdef"));
var value = identity.For("G_PROFIT");
Assert.Equal(
"MBW2607150123456789O",
value);
Assert.Equal(20, value.Length);
Assert.Matches("^[A-Z0-9]+$", value);
}
[Fact]
public void Create_RejectsEmptyNonceAndUnsafeSuffix()
{
Assert.Throws<ArgumentException>(() =>
SmokeRunIdentity.Create(DateTimeOffset.UtcNow, Guid.Empty));
var identity = SmokeRunIdentity.Create(DateTimeOffset.UtcNow, Guid.NewGuid());
Assert.Throws<ArgumentException>(() => identity.For("bad-value"));
}
}