feat: restore legacy modal and drag behavior
This commit is contained in:
@@ -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>(
|
||||
|
||||
@@ -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()
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user