feat: advance legacy UI behavior parity
This commit is contained in:
@@ -295,7 +295,7 @@ public sealed class LegacyBridgeProtocolTests
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ParseIndustryIntents_AcceptsOnlyIndexAndOpaqueActionId()
|
||||
public void ParseIndustryIntents_AcceptsOnlyClosedSelectionTextAndOpaqueActionId()
|
||||
{
|
||||
Assert.True(LegacyBridgeProtocol.TryParseIntent(
|
||||
"{\"type\":\"select-industry\",\"payload\":{\"index\":2}}",
|
||||
@@ -325,22 +325,53 @@ public sealed class LegacyBridgeProtocolTests
|
||||
out _));
|
||||
Assert.IsType<LegacySwapIndustriesIntent>(swap);
|
||||
|
||||
Assert.True(LegacyBridgeProtocol.TryParseIntent(
|
||||
"{\"type\":\"set-industry-comparison-text\",\"payload\":{\"slot\":\"first\",\"value\":\"직접 입력_%\"}}",
|
||||
out var edit,
|
||||
out _));
|
||||
var editIntent = Assert.IsType<LegacySetIndustryComparisonTextIntent>(edit);
|
||||
Assert.Equal(LegacyIndustryComparisonSlot.First, editIntent.Slot);
|
||||
Assert.Equal("직접 입력_%", editIntent.Value);
|
||||
|
||||
Assert.True(LegacyBridgeProtocol.TryParseIntent(
|
||||
"{\"type\":\"set-industry-comparison-text\",\"payload\":{\"slot\":\"second\",\"value\":\"\"}}",
|
||||
out var clear,
|
||||
out _));
|
||||
Assert.Equal(string.Empty,
|
||||
Assert.IsType<LegacySetIndustryComparisonTextIntent>(clear).Value);
|
||||
|
||||
Assert.False(LegacyBridgeProtocol.TryParseIntent(
|
||||
"{\"type\":\"activate-industry-action\",\"payload\":{\"actionId\":\"../5001\"}}",
|
||||
out _,
|
||||
out _));
|
||||
Assert.False(LegacyBridgeProtocol.TryParseIntent(
|
||||
"{\"type\":\"set-industry-comparison-text\",\"payload\":{\"slot\":\"third\",\"value\":\"업종\"}}",
|
||||
out _,
|
||||
out _));
|
||||
Assert.False(LegacyBridgeProtocol.TryParseIntent(
|
||||
"{\"type\":\"set-industry-comparison-text\",\"payload\":{\"slot\":\"first\",\"value\":\"업종\\n이름\"}}",
|
||||
out _,
|
||||
out _));
|
||||
Assert.False(LegacyBridgeProtocol.TryParseIntent(
|
||||
"{\"type\":\"set-industry-comparison-text\",\"payload\":{\"slot\":\"first\",\"value\":\"업종\",\"actionId\":\"two-column\"}}",
|
||||
out _,
|
||||
out _));
|
||||
|
||||
var oversized = new string('가',
|
||||
LegacyIndustrySelectionWorkflow.MaximumComparisonTextLength + 1);
|
||||
var oversizedJson = "{\"type\":\"set-industry-comparison-text\",\"payload\":{\"slot\":\"first\",\"value\":" +
|
||||
System.Text.Json.JsonSerializer.Serialize(oversized) + "}}";
|
||||
Assert.False(LegacyBridgeProtocol.TryParseIntent(oversizedJson, out _, out _));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ParseThemeIntents_UsesClosedSessionSortAndOpaqueIds()
|
||||
public void ParseThemeIntents_UsesQueryOnlySearchSortAndOpaqueIds()
|
||||
{
|
||||
Assert.True(LegacyBridgeProtocol.TryParseIntent(
|
||||
"{\"type\":\"search-themes\",\"payload\":{\"query\":\"AI\",\"nxtSession\":\"preMarket\"}}",
|
||||
"{\"type\":\"search-themes\",\"payload\":{\"query\":\"AI\"}}",
|
||||
out var search,
|
||||
out _));
|
||||
Assert.Equal(
|
||||
ThemeSession.PreMarket,
|
||||
Assert.IsType<LegacySearchThemesIntent>(search).NxtSession);
|
||||
Assert.Equal("AI", Assert.IsType<LegacySearchThemesIntent>(search).Query);
|
||||
|
||||
Assert.True(LegacyBridgeProtocol.TryParseIntent(
|
||||
"{\"type\":\"select-theme\",\"payload\":{\"resultId\":\"theme-result-001\"}}",
|
||||
@@ -365,7 +396,7 @@ public sealed class LegacyBridgeProtocolTests
|
||||
Assert.IsType<LegacyActivateThemeActionIntent>(action).ActionId);
|
||||
|
||||
Assert.False(LegacyBridgeProtocol.TryParseIntent(
|
||||
"{\"type\":\"search-themes\",\"payload\":{\"query\":\"AI\",\"nxtSession\":\"auto\"}}",
|
||||
"{\"type\":\"search-themes\",\"payload\":{\"query\":\"AI\",\"nxtSession\":\"preMarket\"}}",
|
||||
out _,
|
||||
out _));
|
||||
}
|
||||
@@ -531,6 +562,41 @@ public sealed class LegacyBridgeProtocolTests
|
||||
Assert.Equal(expected, LegacyBridgeProtocol.IsTrustedSource(source));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SerializedIndustryStateProjectsAuthoritativeEditedTextNotTypedIdentityName()
|
||||
{
|
||||
var identity = new IndustrySelection(IndustryMarket.Kospi, "DB 업종명", "013");
|
||||
var industry = new LegacyIndustrySelectionSnapshot(
|
||||
3,
|
||||
IndustryMarket.Kospi,
|
||||
[identity],
|
||||
LegacyIndustryActionCatalog.GetActions(IndustryMarket.Kospi),
|
||||
0,
|
||||
identity,
|
||||
identity,
|
||||
null,
|
||||
"직접 입력 첫째",
|
||||
"직접 입력 둘째");
|
||||
var snapshot = new LegacyOperatorSnapshot(
|
||||
1,
|
||||
string.Empty,
|
||||
[],
|
||||
null,
|
||||
[],
|
||||
[],
|
||||
null,
|
||||
string.Empty,
|
||||
LegacyOperatorStatusKind.Neutral,
|
||||
Industry: industry);
|
||||
|
||||
using var document = JsonDocument.Parse(LegacyBridgeProtocol.SerializeState(snapshot));
|
||||
var projected = document.RootElement.GetProperty("payload").GetProperty("industry");
|
||||
|
||||
Assert.Equal("직접 입력 첫째", projected.GetProperty("firstName").GetString());
|
||||
Assert.Equal("직접 입력 둘째", projected.GetProperty("secondName").GetString());
|
||||
Assert.NotEqual("DB 업종명", projected.GetProperty("firstName").GetString());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SerializedState_DoesNotExposeHiddenDbOrSceneAuthority()
|
||||
{
|
||||
|
||||
@@ -48,6 +48,15 @@ public sealed class LegacyOperatorCatalogBridgeTests
|
||||
Assert.IsType<LegacyRemoveOperatorCatalogDraftRowIntent>(ParseRequired(
|
||||
"remove-operator-catalog-draft-row",
|
||||
"""{"rowId":"catalog-draft-0123456789abcdef"}"""));
|
||||
Assert.IsType<LegacySelectOperatorCatalogDraftRowIntent>(ParseRequired(
|
||||
"select-operator-catalog-draft-row",
|
||||
"""{"rowId":"catalog-draft-0123456789abcdef"}"""));
|
||||
Assert.IsType<LegacyRemoveActiveOperatorCatalogDraftRowIntent>(ParseRequired(
|
||||
"remove-active-operator-catalog-draft-row",
|
||||
"{}"));
|
||||
Assert.IsType<LegacyClearOperatorCatalogDraftRowsIntent>(ParseRequired(
|
||||
"clear-operator-catalog-draft-rows",
|
||||
"{}"));
|
||||
|
||||
Assert.False(Parse(
|
||||
"""
|
||||
@@ -64,10 +73,25 @@ public sealed class LegacyOperatorCatalogBridgeTests
|
||||
{"type":"reorder-operator-catalog-draft-row","payload":{"sourceRowId":"catalog-draft-0123456789abcdef","targetRowId":"catalog-draft-fedcba9876543210","position":"after","playIndex":1}}
|
||||
""",
|
||||
out _));
|
||||
Assert.False(Parse(
|
||||
"""
|
||||
{"type":"select-operator-catalog-draft-row","payload":{"rowId":"catalog-draft-0123456789abcdef","stockCode":"005930"}}
|
||||
""",
|
||||
out _));
|
||||
Assert.False(Parse(
|
||||
"""
|
||||
{"type":"remove-active-operator-catalog-draft-row","payload":{"rowId":"catalog-draft-0123456789abcdef"}}
|
||||
""",
|
||||
out _));
|
||||
Assert.False(Parse(
|
||||
"""
|
||||
{"type":"clear-operator-catalog-draft-rows","payload":{"themeCode":"00000001"}}
|
||||
""",
|
||||
out _));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Create_save_delete_and_add_are_closed_and_typed()
|
||||
public void Create_save_and_add_are_closed_and_typed_while_child_delete_is_removed()
|
||||
{
|
||||
Assert.IsType<LegacyBeginCreateThemeCatalogIntent>(ParseRequired(
|
||||
"begin-create-theme-catalog",
|
||||
@@ -88,9 +112,14 @@ public sealed class LegacyOperatorCatalogBridgeTests
|
||||
Assert.Equal("김전문", Assert.IsType<LegacySaveOperatorCatalogIntent>(ParseRequired(
|
||||
"save-operator-catalog",
|
||||
"""{"name":"김전문"}""")).Name);
|
||||
Assert.IsType<LegacyDeleteOperatorCatalogIntent>(ParseRequired(
|
||||
"delete-operator-catalog",
|
||||
"{}"));
|
||||
Assert.Equal(string.Empty, Assert.IsType<LegacySaveOperatorCatalogIntent>(ParseRequired(
|
||||
"save-operator-catalog",
|
||||
"""{"name":""}""")).Name);
|
||||
Assert.False(Parse(
|
||||
"""
|
||||
{"type":"delete-operator-catalog","payload":{}}
|
||||
""",
|
||||
out _));
|
||||
|
||||
Assert.False(Parse(
|
||||
"""
|
||||
@@ -114,6 +143,77 @@ public sealed class LegacyOperatorCatalogBridgeTests
|
||||
out _));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Confirmation_commands_accept_only_an_empty_payload_and_never_accept_trusted_identity()
|
||||
{
|
||||
Assert.IsType<LegacyConfirmNativeDialogIntent>(ParseRequired(
|
||||
"confirm-native-dialog",
|
||||
"{}"));
|
||||
Assert.IsType<LegacyCancelNativeDialogIntent>(ParseRequired(
|
||||
"cancel-native-dialog",
|
||||
"{}"));
|
||||
Assert.False(Parse(
|
||||
"""{"type":"confirm-operator-catalog-confirmation","payload":{}}""",
|
||||
out _));
|
||||
Assert.False(Parse(
|
||||
"""{"type":"cancel-operator-catalog-confirmation","payload":{}}""",
|
||||
out _));
|
||||
|
||||
foreach (var type in new[]
|
||||
{
|
||||
"confirm-native-dialog",
|
||||
"cancel-native-dialog"
|
||||
})
|
||||
{
|
||||
foreach (var payload in new[]
|
||||
{
|
||||
"""{"themeCode":"00000001"}""",
|
||||
"""{"expertCode":"0001"}""",
|
||||
"""{"rowId":"catalog-draft-0123456789abcdef"}""",
|
||||
"""{"comparisonPairId":"comparison-pair-00000001"}""",
|
||||
"""{"expectedIdentity":{"code":"00000001"}}"""
|
||||
})
|
||||
{
|
||||
Assert.False(Parse(
|
||||
"{\"type\":\"" + type + "\",\"payload\":" + payload + "}",
|
||||
out _));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Confirmation_dialog_projection_exposes_caption_and_kind_without_trusted_identity()
|
||||
{
|
||||
var snapshot = new LegacyOperatorSnapshot(
|
||||
7,
|
||||
string.Empty,
|
||||
[],
|
||||
null,
|
||||
[],
|
||||
[],
|
||||
new LegacyDialogState(
|
||||
"선택한 테마를 삭제하겠습니까?",
|
||||
"MmoneyCoder",
|
||||
IsConfirmation: true),
|
||||
string.Empty,
|
||||
LegacyOperatorStatusKind.Neutral);
|
||||
|
||||
using var document = JsonDocument.Parse(LegacyBridgeProtocol.SerializeState(snapshot));
|
||||
var dialog = document.RootElement.GetProperty("payload").GetProperty("dialog");
|
||||
|
||||
Assert.Equal("선택한 테마를 삭제하겠습니까?", dialog.GetProperty("message").GetString());
|
||||
Assert.Equal("MmoneyCoder", dialog.GetProperty("caption").GetString());
|
||||
Assert.True(dialog.GetProperty("isConfirmation").GetBoolean());
|
||||
Assert.Equal(3, dialog.EnumerateObject().Count());
|
||||
foreach (var forbidden in new[]
|
||||
{
|
||||
"themeCode", "expertCode", "stockCode", "rowId", "expectedIdentity"
|
||||
})
|
||||
{
|
||||
Assert.False(dialog.TryGetProperty(forbidden, out _));
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Projection_exposes_display_data_and_opaque_ids_without_DB_identity_authority()
|
||||
{
|
||||
@@ -147,6 +247,7 @@ public sealed class LegacyOperatorCatalogBridgeTests
|
||||
null,
|
||||
0)
|
||||
],
|
||||
"catalog-draft-0123456789abcdef",
|
||||
"삼성",
|
||||
[
|
||||
new LegacyOperatorCatalogStockResultView(
|
||||
@@ -186,6 +287,7 @@ public sealed class LegacyOperatorCatalogBridgeTests
|
||||
Assert.Contains("themeNameAvailability", json, StringComparison.Ordinal);
|
||||
Assert.Contains("checkedThemeName", json, StringComparison.Ordinal);
|
||||
Assert.Contains("canCheckThemeName", json, StringComparison.Ordinal);
|
||||
Assert.Contains("activeDraftRowId", json, StringComparison.Ordinal);
|
||||
Assert.Contains("duplicate", json, StringComparison.OrdinalIgnoreCase);
|
||||
Assert.DoesNotContain("themeCode", json, StringComparison.Ordinal);
|
||||
Assert.DoesNotContain("expertCode", json, StringComparison.Ordinal);
|
||||
|
||||
@@ -90,10 +90,59 @@ public sealed class LegacyOverseasBridgeTests
|
||||
Assert.DoesNotContain("builderKey", json, StringComparison.OrdinalIgnoreCase);
|
||||
Assert.DoesNotContain("sceneAlias", json, StringComparison.OrdinalIgnoreCase);
|
||||
Assert.DoesNotContain("dataCode", json, StringComparison.OrdinalIgnoreCase);
|
||||
Assert.DoesNotContain("nationCode", json, StringComparison.OrdinalIgnoreCase);
|
||||
Assert.DoesNotContain("NAS@AAPL", json, StringComparison.Ordinal);
|
||||
Assert.DoesNotContain("NAS@SOX", json, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ProjectionKeepsNonUsTwRowOpaqueAndReportsItAvailable()
|
||||
{
|
||||
var workflow = new LegacyOverseasSelectionWorkflow(
|
||||
new StaticIndustryService(),
|
||||
new StaticStockService([new WorldStockSearchItem("London Stock", "LNS@LON", "GB")]));
|
||||
var searched = await workflow.SearchStocksAsync("London");
|
||||
workflow.SelectStock(Assert.Single(searched.Stock.Results).SelectionId);
|
||||
var snapshot = new LegacyOperatorSnapshot(
|
||||
7, string.Empty, [], null, [], [], null, string.Empty,
|
||||
LegacyOperatorStatusKind.Neutral, Overseas: workflow.Current);
|
||||
|
||||
using var document = JsonDocument.Parse(LegacyBridgeProtocol.SerializeState(snapshot));
|
||||
var overseas = document.RootElement.GetProperty("payload").GetProperty("overseas");
|
||||
var result = Assert.Single(overseas.GetProperty("stock").GetProperty("results").EnumerateArray());
|
||||
Assert.Equal("London Stock", result.GetProperty("displayName").GetString());
|
||||
Assert.False(result.TryGetProperty("nationCode", out _));
|
||||
var action = overseas.GetProperty("actions").EnumerateArray().Single(value =>
|
||||
value.GetProperty("actionId").GetString() == "stock-current");
|
||||
Assert.True(action.GetProperty("isAvailable").GetBoolean());
|
||||
Assert.Equal(JsonValueKind.Null, action.GetProperty("reason").ValueKind);
|
||||
Assert.DoesNotContain("LNS@LON", document.RootElement.GetRawText(), StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ProjectionKeepsThreeLetterNationOpaqueAndFailsClosed()
|
||||
{
|
||||
var workflow = new LegacyOverseasSelectionWorkflow(
|
||||
new StaticIndustryService(),
|
||||
new StaticStockService([new WorldStockSearchItem("Unsupported", "LNS@UNSUPPORTED", "GBR")]));
|
||||
var searched = await workflow.SearchStocksAsync("unsupported");
|
||||
workflow.SelectStock(Assert.Single(searched.Stock.Results).SelectionId);
|
||||
var snapshot = new LegacyOperatorSnapshot(
|
||||
7, string.Empty, [], null, [], [], null, string.Empty,
|
||||
LegacyOperatorStatusKind.Neutral, Overseas: workflow.Current);
|
||||
|
||||
using var document = JsonDocument.Parse(LegacyBridgeProtocol.SerializeState(snapshot));
|
||||
var overseas = document.RootElement.GetProperty("payload").GetProperty("overseas");
|
||||
var action = overseas.GetProperty("actions").EnumerateArray().Single(value =>
|
||||
value.GetProperty("actionId").GetString() == "stock-current");
|
||||
|
||||
Assert.False(action.GetProperty("isAvailable").GetBoolean());
|
||||
Assert.Equal("선택한 해외종목의 국가 코드는 두 글자여야 합니다.",
|
||||
action.GetProperty("reason").GetString());
|
||||
Assert.DoesNotContain("GBR", document.RootElement.GetRawText(), StringComparison.Ordinal);
|
||||
Assert.DoesNotContain("LNS@UNSUPPORTED", document.RootElement.GetRawText(), StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
private sealed class StaticIndustryService : IOverseasIndustryIndexSearchService
|
||||
{
|
||||
public Task<OverseasIndustryIndexSearchResult> SearchAsync(
|
||||
@@ -107,16 +156,17 @@ public sealed class LegacyOverseasBridgeTests
|
||||
IsTruncated: false));
|
||||
}
|
||||
|
||||
private sealed class StaticStockService : IWorldStockSearchService
|
||||
private sealed class StaticStockService(
|
||||
IReadOnlyList<WorldStockSearchItem>? items = null) : IOverseasStockSearchService
|
||||
{
|
||||
public Task<WorldStockSearchResult> SearchAsync(
|
||||
string query,
|
||||
int maximumResults = LegacyWorldStockSearchService.DefaultMaximumResults,
|
||||
int maximumResults = LegacyOverseasStockSearchService.DefaultMaximumResults,
|
||||
CancellationToken cancellationToken = default) =>
|
||||
Task.FromResult(new WorldStockSearchResult(
|
||||
query,
|
||||
DateTimeOffset.Parse("2026-07-15T10:00:00+09:00"),
|
||||
[new WorldStockSearchItem("Apple", "NAS@AAPL", "US")],
|
||||
items ?? [new WorldStockSearchItem("Apple", "NAS@AAPL", "US")],
|
||||
IsTruncated: false));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -142,7 +142,12 @@ public sealed class LegacyPlayoutBridgeTests
|
||||
CanChangeBackground: false,
|
||||
Message: "준비 완료")
|
||||
{
|
||||
IsTakeOutCompletionPending = true
|
||||
IsTakeOutCompletionPending = true,
|
||||
RefreshNextAtUtc = new DateTimeOffset(
|
||||
2026, 7, 17, 2, 3, 4, TimeSpan.Zero),
|
||||
RefreshLastSuccessAtUtc = new DateTimeOffset(
|
||||
2026, 7, 17, 1, 2, 3, TimeSpan.Zero),
|
||||
RefreshFaultCode = "DATABASE_UNAVAILABLE"
|
||||
};
|
||||
|
||||
using var document = JsonDocument.Parse(
|
||||
@@ -158,6 +163,15 @@ public sealed class LegacyPlayoutBridgeTests
|
||||
Assert.Equal("기본.vrv", projected.GetProperty("backgroundFileName").GetString());
|
||||
Assert.Equal("entry-1", projected.GetProperty("currentEntryId").GetString());
|
||||
Assert.True(projected.GetProperty("isTakeOutCompletionPending").GetBoolean());
|
||||
Assert.Equal(
|
||||
new DateTimeOffset(2026, 7, 17, 2, 3, 4, TimeSpan.Zero),
|
||||
projected.GetProperty("refreshNextAtUtc").GetDateTimeOffset());
|
||||
Assert.Equal(
|
||||
new DateTimeOffset(2026, 7, 17, 1, 2, 3, TimeSpan.Zero),
|
||||
projected.GetProperty("refreshLastSuccessAtUtc").GetDateTimeOffset());
|
||||
Assert.Equal(
|
||||
"DATABASE_UNAVAILABLE",
|
||||
projected.GetProperty("refreshFaultCode").GetString());
|
||||
Assert.False(projected.TryGetProperty("builderKey", out _));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user