feat: complete legacy parity and modernize operator UI

This commit is contained in:
2026-07-22 12:34:46 +09:00
parent fc4007d676
commit 939c252d23
149 changed files with 26515 additions and 1736 deletions

View File

@@ -77,6 +77,10 @@ public sealed class LegacyThemeWorkflowTests
property => Assert.False(property.CanWrite));
Assert.Throws<ArgumentOutOfRangeException>(() =>
workflow.CreateInitial(ThemeSession.NotApplicable));
Assert.Equal(5_000, LegacyThemeWorkflow.MaximumSearchResults);
Assert.Equal(
LegacyThemeSelectionService.MaximumResults,
LegacyThemeWorkflow.MaximumSearchResults);
}
[Theory]
@@ -229,8 +233,43 @@ public sealed class LegacyThemeWorkflowTests
query,
session,
Selector(ThemeMarket.Nxt, session, "AI(NXT)", "00000001"));
await Assert.ThrowsAsync<LegacyThemeWorkflowDataException>(() =>
workflow.SearchAsync(snapshot, "AI", ThemeSession.PreMarket));
var storedSuffix = await workflow.SearchAsync(
snapshot,
"AI",
ThemeSession.PreMarket);
Assert.Equal("AI(NXT)", Assert.Single(storedSuffix.SearchResults).DisplayTitle);
service.SearchHandler = (query, session) => SearchResult(
query,
session,
Selector(ThemeMarket.Nxt, session, "AI(NXT)(NXT)", "00000001"));
var repeatedNxtSuffix = await workflow.SearchAsync(
snapshot,
"AI",
ThemeSession.PreMarket);
Assert.Equal(
"AI(NXT)(NXT)",
Assert.Single(repeatedNxtSuffix.SearchResults).Identity.ThemeTitle);
service.SearchHandler = (query, session) => SearchResult(
query,
session,
Selector(ThemeMarket.Krx, ThemeSession.NotApplicable, "AI(NXT)", "00000001"));
var krxStoredSuffix = await workflow.SearchAsync(
snapshot,
"AI",
ThemeSession.PreMarket);
Assert.Equal("AI(NXT)", Assert.Single(krxStoredSuffix.SearchResults).DisplayTitle);
service.SearchHandler = (query, session) => SearchResult(
query,
session,
Selector(ThemeMarket.Krx, ThemeSession.NotApplicable, "AI(NXT)(NXT)", "00000001"));
var repeatedKrxSuffix = await workflow.SearchAsync(
snapshot,
"AI",
ThemeSession.PreMarket);
Assert.Equal(ThemeMarket.Krx, Assert.Single(repeatedKrxSuffix.SearchResults).Market);
service.SearchHandler = (query, session) => SearchResult(
query,
@@ -263,6 +302,70 @@ public sealed class LegacyThemeWorkflowTests
Assert.Contains("search provider order", exception.Message, StringComparison.Ordinal);
}
[Fact]
public async Task SearchAsync_PreservesExactLegacyStoredTitlesWithEdgeSpaces()
{
var service = new FakeThemeSelectionService
{
SearchHandler = (query, session) => SearchResult(
query,
session,
Selector(ThemeMarket.Krx, ThemeSession.NotApplicable, " KRX Theme ", "00000001"),
Selector(ThemeMarket.Nxt, session, " NXT Theme ", "00000002"))
};
var workflow = new LegacyThemeWorkflow(service);
var result = await workflow.SearchAsync(
workflow.CreateInitial(),
string.Empty,
ThemeSession.PreMarket);
Assert.Collection(
result.SearchResults,
row =>
{
Assert.Equal(" KRX Theme ", row.Identity.ThemeTitle);
Assert.Equal(" KRX Theme ", row.DisplayTitle);
Assert.Equal(ThemeMarket.Krx, row.Market);
},
row =>
{
Assert.Equal(" NXT Theme ", row.Identity.ThemeTitle);
Assert.Equal(" NXT Theme (NXT)", row.DisplayTitle);
Assert.Equal(ThemeMarket.Nxt, row.Market);
});
}
[Fact]
public async Task SearchAsync_AcceptsFiniteFiveThousandRowTruncationBoundary()
{
var service = new FakeThemeSelectionService
{
SearchHandler = (query, session) => new ThemeSearchResult(
query,
session,
DateTimeOffset.UtcNow,
Enumerable.Range(0, LegacyThemeWorkflow.MaximumSearchResults)
.Select(index => Selector(
ThemeMarket.Krx,
ThemeSession.NotApplicable,
$"Theme {index}",
index.ToString("D8", System.Globalization.CultureInfo.InvariantCulture)))
.ToArray(),
IsTruncated: true)
};
var workflow = new LegacyThemeWorkflow(service);
var snapshot = await workflow.SearchAsync(
workflow.CreateInitial(),
string.Empty,
ThemeSession.PreMarket);
Assert.Equal(5_000, snapshot.SearchResults.Count);
Assert.True(snapshot.SearchIsTruncated);
Assert.Equal("theme-result-5000", snapshot.SearchResults[^1].ResultId);
}
[Fact]
public async Task SelectAsync_UsesOpaqueResultIdAndCopiesValidatedPreview()
{
@@ -515,6 +618,7 @@ public sealed class LegacyThemeWorkflowTests
action.ValueKind == LegacyThemeValueKind.Expected ? "EXPECTED" : "CURRENT",
draft.Selection.Subtype);
Assert.Equal("00000001", draft.Selection.DataCode);
Assert.Equal("AI", draft.Selection.ExactSubject);
Assert.Equal(action.PageSize, draft.PageSize);
Assert.Equal($"1/{draft.PagePlan.PageCount}", draft.PagePlan.InitialPageText);
Assert.InRange(draft.PagePlan.PageCount, 1, 20);
@@ -554,7 +658,8 @@ public sealed class LegacyThemeWorkflowTests
"Robotics(NXT)",
"AFTER_MARKET",
sortId,
"00000002"),
"00000002",
"Robotics"),
draft.Selection);
Assert.True(workflow.GetActionState(selected, actionId).IsAvailable);
}
@@ -567,6 +672,69 @@ public sealed class LegacyThemeWorkflowTests
Assert.Equal("NXT는 데이터가 없습니다.", exception.Message);
}
[Fact]
public async Task MaterializePlaylistDraft_NxtStoredSuffixIsNotDuplicated()
{
var identity = Identity(
ThemeMarket.Nxt,
ThemeSession.PreMarket,
"Robotics(NXT)",
"00000002");
var workflow = new LegacyThemeWorkflow(
ServiceWithTheme(identity, CreateItems(1, ThemeMarket.Nxt)));
var selected = await SearchAndSelectAsync(workflow, ThemeSession.PreMarket);
var draft = workflow.MaterializePlaylistDraft(selected, "five-row-current");
Assert.Equal("Robotics(NXT)", draft.Title);
Assert.Equal("Robotics(NXT)", draft.Selection.Subject);
Assert.Equal("Robotics(NXT)", draft.Selection.ExactSubject);
}
[Fact]
public async Task MaterializePlaylistDraft_KrxStoredNxtSuffixKeepsExplicitKrxRoute()
{
var identity = Identity(
ThemeMarket.Krx,
ThemeSession.NotApplicable,
"Robotics(NXT)",
"00000002");
var workflow = new LegacyThemeWorkflow(
ServiceWithTheme(identity, CreateItems(1, ThemeMarket.Krx)));
var selected = await SearchAndSelectAsync(workflow);
var draft = workflow.MaterializePlaylistDraft(selected, "five-row-current");
Assert.Equal("Robotics(NXT)", draft.Title);
Assert.Equal("PAGED_THEME", draft.Selection.GroupCode);
Assert.Equal("Robotics(NXT)", draft.Selection.Subject);
Assert.Equal("Robotics(NXT)", draft.Selection.ExactSubject);
}
[Fact]
public async Task SelectAsync_NxtEmptyActivePreviewRemainsSelectedButCannotAddOrPlayout()
{
var identity = Identity(
ThemeMarket.Nxt,
ThemeSession.PreMarket,
" NXT Theme",
"00000004");
var workflow = new LegacyThemeWorkflow(ServiceWithTheme(identity, []));
var selected = await SearchAndSelectAsync(workflow, ThemeSession.PreMarket);
Assert.Equal(ThemeMarket.Nxt, selected.SelectedTheme!.Market);
Assert.Equal(" NXT Theme(NXT)", selected.SelectedTheme.DisplayTitle);
Assert.Empty(selected.PreviewItems);
Assert.All(selected.ActionStates, state =>
{
Assert.False(state.IsAvailable);
Assert.Equal("theme-preview-is-empty", state.UnavailableReason);
});
Assert.Throws<InvalidOperationException>(() =>
workflow.MaterializePlaylistDraft(selected, "five-row-current"));
}
[Fact]
public async Task MaterializePlaylistDraft_NxtReevaluatesTheNativeHourAtActionTime()
{