feat: restore legacy modal and drag behavior

This commit is contained in:
2026-07-16 01:33:48 +09:00
parent 83398044c6
commit d7ec571343
30 changed files with 3597 additions and 321 deletions

View File

@@ -219,6 +219,153 @@ public sealed class LegacyNamedPlaylistOperatorIntegrationTests
Assert.True(saved.CommandResult?.Succeeded);
}
[Fact]
public async Task Save_to_opaque_id_treats_known_commit_with_failed_readback_as_success()
{
var persistence = new OperatorNamedPlaylistPersistence([])
{
FailNextLoad = true
};
var controller = CreateController(persistence);
var listed = await controller.RefreshNamedPlaylistsAsync();
var definitionId = Assert.Single(
listed.NamedPlaylist!.Definitions).DefinitionId;
var saved = await controller.SaveCurrentNamedPlaylistToAsync(definitionId);
Assert.Equal(1, persistence.ReplaceCalls);
Assert.Equal(
LegacyNamedPlaylistMutationOutcome.CommittedOptimistic,
saved.NamedPlaylist?.LastMutationOutcome);
Assert.Equal(LegacyOperatorStatusKind.Warning, saved.StatusKind);
Assert.Equal("save-current-named-playlist-to", saved.CommandResult?.Command);
Assert.Equal(definitionId, saved.CommandResult?.TargetId);
Assert.True(saved.CommandResult?.Succeeded);
}
[Fact]
public async Task Save_to_opaque_id_does_not_reuse_a_stale_success_after_validation_rejection()
{
var persistence = new OperatorNamedPlaylistPersistence([]);
var controller = CreateController(persistence);
var listed = await controller.RefreshNamedPlaylistsAsync();
var definitionId = Assert.Single(
listed.NamedPlaylist!.Definitions).DefinitionId;
var first = await controller.SaveCurrentNamedPlaylistToAsync(definitionId);
Assert.True(first.CommandResult?.Succeeded);
Assert.Equal(1, persistence.ReplaceCalls);
var action = LegacyFixedActionCatalog.Default
.GetActions(LegacyFixedMarket.Overseas)
.First(candidate => candidate.Available);
for (var index = 0;
index <= LegacyNamedPlaylistWorkflowController.MaximumItems;
index++)
{
controller.ActivateFixedAction(action.Id);
}
var rejected = await controller.SaveCurrentNamedPlaylistToAsync(definitionId);
Assert.Equal(1, persistence.ReplaceCalls);
Assert.Equal(
LegacyNamedPlaylistMutationOutcome.CommittedFresh,
rejected.NamedPlaylist?.LastMutationOutcome);
Assert.Equal(LegacyOperatorStatusKind.Warning, rejected.StatusKind);
Assert.Equal("save-current-named-playlist-to", rejected.CommandResult?.Command);
Assert.Equal(definitionId, rejected.CommandResult?.TargetId);
Assert.False(rejected.CommandResult?.Succeeded);
}
[Theory]
[InlineData("refresh-named-playlists")]
[InlineData("select-named-playlist")]
[InlineData("create-named-playlist")]
[InlineData("delete-selected-named-playlist")]
[InlineData("load-named-playlist-by-id")]
[InlineData("save-current-named-playlist-to")]
public void Native_dispatch_failure_emits_exact_terminal_receipt(
string command)
{
var controller = CreateController(
new OperatorNamedPlaylistPersistence([]));
const string definitionId = "named-definition-opaque";
var failed = controller.ReportNamedPlaylistCommandFailure(
command,
definitionId,
"The native request was rejected before completion.");
Assert.Equal(command, failed.CommandResult?.Command);
Assert.Equal(definitionId, failed.CommandResult?.TargetId);
Assert.False(failed.CommandResult?.Succeeded);
Assert.Equal(LegacyOperatorStatusKind.Error, failed.StatusKind);
}
[Fact]
public void Native_dispatch_failure_rejects_unrelated_command_names()
{
var controller = CreateController(
new OperatorNamedPlaylistPersistence([]));
Assert.Throws<ArgumentOutOfRangeException>(() =>
controller.ReportNamedPlaylistCommandFailure(
"load-selected-named-playlist",
"named-definition-opaque",
"Rejected."));
}
[Theory]
[InlineData("refresh-named-playlists")]
[InlineData("select-named-playlist")]
[InlineData("create-named-playlist")]
[InlineData("delete-selected-named-playlist")]
public void Busy_cycle_completion_echoes_exact_dispatch_id(string command)
{
var controller = CreateController(
new OperatorNamedPlaylistPersistence([]));
var completed = controller.CompleteNamedPlaylistDispatch(
command,
"named-ui-42",
succeeded: true);
Assert.Equal(command, completed.CommandResult?.Command);
Assert.Equal("named-ui-42", completed.CommandResult?.TargetId);
Assert.True(completed.CommandResult?.Succeeded);
}
[Fact]
public async Task Ambiguous_native_save_failure_latches_no_retry_quarantine()
{
var persistence = new OperatorNamedPlaylistPersistence([]);
var controller = CreateController(persistence);
const string definitionId = "named-definition-opaque";
var failed = controller.ReportNamedPlaylistWriteOutcomeUnknown(
"save-current-named-playlist-to",
definitionId,
"The accepted save ended without a definitive database outcome.");
Assert.Equal("save-current-named-playlist-to", failed.CommandResult?.Command);
Assert.Equal(definitionId, failed.CommandResult?.TargetId);
Assert.False(failed.CommandResult?.Succeeded);
Assert.True(failed.NamedPlaylist?.IsWriteQuarantined);
Assert.Equal(
LegacyNamedPlaylistMutationOutcome.OutcomeUnknown,
failed.NamedPlaylist?.LastMutationOutcome);
var listed = await controller.RefreshNamedPlaylistsAsync();
controller.SelectNamedPlaylist(
Assert.Single(listed.NamedPlaylist!.Definitions).DefinitionId);
var blocked = await controller.SaveCurrentNamedPlaylistAsync();
Assert.Equal(0, persistence.ReplaceCalls);
Assert.Equal(
LegacyNamedPlaylistMutationOutcome.Quarantined,
blocked.NamedPlaylist?.LastMutationOutcome);
}
private static LegacyOperatorController CreateController(
INamedPlaylistPersistenceService persistence) =>
new(
@@ -259,6 +406,8 @@ public sealed class LegacyNamedPlaylistOperatorIntegrationTests
public IReadOnlyList<NamedPlaylistStoredItem>? LastReplaceItems { get; private set; }
public bool FailNextLoad { get; set; }
public Task<NamedPlaylistListResult> ListAsync(
int maximumResults = LegacyNamedPlaylistPersistenceService.DefaultMaximumDefinitions,
CancellationToken cancellationToken = default)
@@ -282,6 +431,13 @@ public sealed class LegacyNamedPlaylistOperatorIntegrationTests
CancellationToken cancellationToken = default)
{
++LoadCalls;
if (FailNextLoad)
{
FailNextLoad = false;
return Task.FromException<NamedPlaylistDocument>(
new NamedPlaylistDataException("The committed save could not be read back."));
}
if (!_exists || !string.Equals(programCode, ProgramCode, StringComparison.Ordinal))
{
return Task.FromException<NamedPlaylistDocument>(

View File

@@ -101,8 +101,11 @@ public sealed class LegacyNamedPlaylistWorkflowTests
Assert.Equal(LegacyNamedPlaylistMutationOutcome.CommittedFresh, created.Outcome);
Assert.Equal("00000003", persistence.LastCreatedProgramCode);
Assert.Equal("새 프로그램", created.Snapshot.SelectedTitle);
Assert.Equal(3, created.Snapshot.Definitions.Count);
Assert.Equal(
created.Snapshot.Definitions[0].DefinitionId,
created.Snapshot.SelectedDefinitionId);
var firstTitleAfterCreate = created.Snapshot.Definitions[0].Title;
Assert.Empty(created.Snapshot.Rows);
Assert.DoesNotContain(
typeof(LegacyNamedPlaylistProgramDraft).GetProperties(),
@@ -113,14 +116,45 @@ public sealed class LegacyNamedPlaylistWorkflowTests
Assert.Equal(LegacyNamedPlaylistMutationOutcome.CommittedFresh, deleted.Outcome);
Assert.Equal(1, persistence.DeleteCalls);
Assert.Equal("00000003", persistence.LastDeletedProgramCode);
Assert.Null(deleted.Snapshot.SelectedDefinitionId);
Assert.NotEqual("00000003", persistence.LastDeletedProgramCode);
Assert.Equal(
deleted.Snapshot.Definitions[0].DefinitionId,
deleted.Snapshot.SelectedDefinitionId);
Assert.DoesNotContain(
deleted.Snapshot.Definitions,
definition => string.Equals(definition.Title, "새 프로그램", StringComparison.Ordinal));
definition => string.Equals(
definition.Title,
firstTitleAfterCreate,
StringComparison.Ordinal));
Assert.Contains(
deleted.Snapshot.Definitions,
definition => string.Equals(
definition.Title,
"새 프로그램",
StringComparison.Ordinal));
Assert.Equal(LegacyNamedPlaylistFreshness.Fresh, deleted.Snapshot.ListFreshness);
}
[Fact]
public async Task EveryNonEmptyListRebind_SelectsFirstDefinitionLikeWinFormsPList()
{
var persistence = CreateSeededPersistence();
var controller = new LegacyNamedPlaylistWorkflowController(persistence);
var opened = await controller.RefreshAsync();
Assert.Equal(opened.Definitions[0].DefinitionId, opened.SelectedDefinitionId);
var second = opened.Definitions[1];
controller.SelectDefinition(second.DefinitionId);
var refreshed = await controller.RefreshAsync();
Assert.Equal(refreshed.Definitions[0].DefinitionId, refreshed.SelectedDefinitionId);
Assert.NotEqual(second.Title, refreshed.SelectedTitle);
Assert.True(refreshed.CanLoad);
Assert.True(refreshed.CanDelete);
}
[Fact]
public async Task KnownRollback_PreservesLoadedRowsAndNeverRetries()
{