feat: complete legacy parity and modernize operator UI
This commit is contained in:
@@ -457,6 +457,46 @@ public sealed class LegacyBridgeProtocolTests
|
||||
Assert.IsType<LegacyActivateTradingHaltActionIntent>(haltAction);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ExpertProjection_PreservesNullBuyAmountAndAvailableCompositionAction()
|
||||
{
|
||||
var expert = new LegacyExpertWorkflowSnapshot(
|
||||
1,
|
||||
new LegacyExpertSearchSnapshot(
|
||||
string.Empty,
|
||||
LegacyExpertRequestStatus.Ready,
|
||||
[],
|
||||
false,
|
||||
string.Empty),
|
||||
null,
|
||||
new LegacyExpertPreviewSnapshot(
|
||||
LegacyExpertRequestStatus.Ready,
|
||||
[new LegacyExpertRecommendationView(0, "005930", "Samsung", null)],
|
||||
false,
|
||||
5,
|
||||
1,
|
||||
20,
|
||||
string.Empty),
|
||||
[new LegacyExpertActionView(
|
||||
LegacyExpertWorkflowController.ActionId,
|
||||
"Expert",
|
||||
true)],
|
||||
string.Empty,
|
||||
LegacyOperatorStatusKind.Warning);
|
||||
var snapshot = new LegacyOperatorSnapshot(
|
||||
1, string.Empty, [], null, [], [], null, string.Empty,
|
||||
LegacyOperatorStatusKind.Warning, Expert: expert);
|
||||
|
||||
using var document = JsonDocument.Parse(LegacyBridgeProtocol.SerializeState(snapshot));
|
||||
var projected = document.RootElement.GetProperty("payload").GetProperty("expert");
|
||||
var preview = Assert.Single(
|
||||
projected.GetProperty("preview").GetProperty("items").EnumerateArray());
|
||||
|
||||
Assert.Equal(JsonValueKind.Null, preview.GetProperty("buyAmount").ValueKind);
|
||||
Assert.True(Assert.Single(projected.GetProperty("actions").EnumerateArray())
|
||||
.GetProperty("isAvailable").GetBoolean());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ParseExpertInitialConsonantQuery_PreservesCompatibilityJamo()
|
||||
{
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
using System.Text.Json;
|
||||
using MBN_STOCK_WEBVIEW.LegacyApplication;
|
||||
|
||||
namespace MBN_STOCK_WEBVIEW.LegacyBridge.Tests;
|
||||
@@ -16,6 +17,7 @@ public sealed class LegacyComparisonBridgeTests
|
||||
[InlineData("swap-comparison-targets", "{}", typeof(LegacySwapComparisonTargetsIntent))]
|
||||
[InlineData("clear-comparison-targets", "{}", typeof(LegacyClearComparisonTargetsIntent))]
|
||||
[InlineData("add-comparison-pair", "{}", typeof(LegacyAddComparisonPairIntent))]
|
||||
[InlineData("import-legacy-comparison-pairs", "{}", typeof(LegacyImportComparisonPairsIntent))]
|
||||
[InlineData("select-comparison-pair", "{\"rowId\":\"comparison-pair-00000001\"}", typeof(LegacySelectComparisonPairIntent))]
|
||||
[InlineData("move-comparison-pair", "{\"direction\":\"up\"}", typeof(LegacyMoveComparisonPairIntent))]
|
||||
[InlineData("delete-selected-comparison-pair", "{}", typeof(LegacyDeleteSelectedComparisonPairIntent))]
|
||||
@@ -41,5 +43,99 @@ public sealed class LegacyComparisonBridgeTests
|
||||
"{\"type\":\"move-comparison-pair\",\"payload\":{\"direction\":\"sideways\"}}",
|
||||
out _,
|
||||
out _));
|
||||
Assert.False(LegacyBridgeProtocol.TryParseIntent(
|
||||
"{\"type\":\"import-legacy-comparison-pairs\",\"payload\":{\"path\":\"C:\\\\untrusted.dat\"}}",
|
||||
out _,
|
||||
out _));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void State_projects_truncation_and_isolated_row_diagnostics()
|
||||
{
|
||||
var domestic = new LegacyComparisonSearchSnapshot(
|
||||
string.Empty,
|
||||
LegacyComparisonSearchStatus.Ready,
|
||||
Array.Empty<LegacyComparisonTargetView>(),
|
||||
null,
|
||||
0,
|
||||
string.Empty);
|
||||
var world = new LegacyComparisonSearchSnapshot(
|
||||
string.Empty,
|
||||
LegacyComparisonSearchStatus.Ready,
|
||||
[new LegacyComparisonTargetView(
|
||||
"comparison-world-0001-0001",
|
||||
"ACME, INC",
|
||||
"US · ACME",
|
||||
LegacyComparisonTargetKind.WorldStock)
|
||||
{
|
||||
CanSelect = false,
|
||||
UnavailableReason = "쉼표 이름은 선택할 수 없습니다."
|
||||
}],
|
||||
null,
|
||||
1_474,
|
||||
string.Empty)
|
||||
{
|
||||
IsTruncated = true,
|
||||
IsolatedInvalidRowCount = 16,
|
||||
DeferredUnsafeRowCount = 1
|
||||
};
|
||||
var comparison = new LegacyComparisonWorkflowSnapshot(
|
||||
1,
|
||||
false,
|
||||
domestic,
|
||||
world,
|
||||
Array.Empty<LegacyComparisonTargetView>(),
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
Array.Empty<LegacyComparisonSavedPairView>(),
|
||||
null,
|
||||
Array.Empty<LegacyComparisonActionView>(),
|
||||
string.Empty,
|
||||
LegacyOperatorStatusKind.Neutral)
|
||||
{
|
||||
CanImportLegacyPairs = true,
|
||||
LastImport = new LegacyComparisonImportReceipt(
|
||||
new string('A', 64),
|
||||
8,
|
||||
7,
|
||||
1,
|
||||
8,
|
||||
DateTimeOffset.Parse("2026-07-22T12:00:00+09:00"))
|
||||
};
|
||||
var snapshot = new LegacyOperatorSnapshot(
|
||||
1,
|
||||
string.Empty,
|
||||
Array.Empty<LegacyStockResultRow>(),
|
||||
null,
|
||||
Array.Empty<LegacyCutViewRow>(),
|
||||
Array.Empty<LegacyOperatorPlaylistRow>(),
|
||||
null,
|
||||
string.Empty,
|
||||
LegacyOperatorStatusKind.Neutral,
|
||||
Comparison: comparison);
|
||||
|
||||
using var document = JsonDocument.Parse(LegacyBridgeProtocol.SerializeState(snapshot));
|
||||
var projected = document.RootElement
|
||||
.GetProperty("payload")
|
||||
.GetProperty("comparison")
|
||||
.GetProperty("worldSearch");
|
||||
|
||||
Assert.True(projected.GetProperty("isTruncated").GetBoolean());
|
||||
Assert.Equal(16, projected.GetProperty("isolatedInvalidRowCount").GetInt32());
|
||||
Assert.Equal(1, projected.GetProperty("deferredUnsafeRowCount").GetInt32());
|
||||
Assert.Equal(1_474, projected.GetProperty("totalRowCount").GetInt32());
|
||||
var deferred = projected.GetProperty("results")[0];
|
||||
Assert.False(deferred.GetProperty("canSelect").GetBoolean());
|
||||
var comparisonWire = document.RootElement
|
||||
.GetProperty("payload")
|
||||
.GetProperty("comparison");
|
||||
Assert.True(comparisonWire.GetProperty("canImportLegacyPairs").GetBoolean());
|
||||
Assert.Equal(7, comparisonWire.GetProperty("lastImport")
|
||||
.GetProperty("addedPairCount").GetInt32());
|
||||
Assert.Contains(
|
||||
"쉼표",
|
||||
deferred.GetProperty("unavailableReason").GetString(),
|
||||
StringComparison.Ordinal);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ public sealed class LegacyInteractionMetricsBridgeTests
|
||||
{
|
||||
var json = LegacyBridgeProtocol.SerializeState(
|
||||
CreateSnapshot(),
|
||||
interactionMetrics: new LegacyInteractionMetrics(7, 9, 96, 1.5));
|
||||
interactionMetrics: new LegacyInteractionMetrics(7, 9, 96, 1.5, 725));
|
||||
|
||||
using var document = JsonDocument.Parse(json);
|
||||
var payload = document.RootElement.GetProperty("payload");
|
||||
@@ -22,6 +22,7 @@ public sealed class LegacyInteractionMetricsBridgeTests
|
||||
Assert.Equal("host-dip", metrics.GetProperty("coordinateSpace").GetString());
|
||||
Assert.Equal(96u, metrics.GetProperty("sourceDpi").GetUInt32());
|
||||
Assert.Equal(1.5d, metrics.GetProperty("hostRasterizationScale").GetDouble());
|
||||
Assert.Equal(725, metrics.GetProperty("doubleClickTimeMilliseconds").GetInt32());
|
||||
Assert.False(metrics.TryGetProperty("CutDragWidthDips", out _));
|
||||
Assert.False(metrics.TryGetProperty("CutDragHeightDips", out _));
|
||||
}
|
||||
@@ -54,11 +55,13 @@ public sealed class LegacyInteractionMetricsBridgeTests
|
||||
Assert.Equal(4, unknownDpi.CutDragWidthDips);
|
||||
Assert.Equal(4, unknownDpi.CutDragHeightDips);
|
||||
Assert.Equal(1.5d, unknownDpi.HostRasterizationScale);
|
||||
Assert.Equal(500, unknownDpi.DoubleClickTimeMilliseconds);
|
||||
|
||||
var metrics = LegacyInteractionMetrics.FromPixelsAtDpi(0, -1, 192);
|
||||
Assert.Equal(4, metrics.CutDragWidthDips);
|
||||
Assert.Equal(4, metrics.CutDragHeightDips);
|
||||
Assert.Equal(192u, metrics.SourceDpi);
|
||||
Assert.Equal(500, metrics.DoubleClickTimeMilliseconds);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -66,7 +69,7 @@ public sealed class LegacyInteractionMetricsBridgeTests
|
||||
{
|
||||
var json = LegacyBridgeProtocol.SerializeState(
|
||||
CreateSnapshot(),
|
||||
interactionMetrics: new LegacyInteractionMetrics(0, -1, 0, double.NaN));
|
||||
interactionMetrics: new LegacyInteractionMetrics(0, -1, 0, double.NaN, 5_001));
|
||||
|
||||
using var document = JsonDocument.Parse(json);
|
||||
var metrics = document.RootElement
|
||||
@@ -78,6 +81,7 @@ public sealed class LegacyInteractionMetricsBridgeTests
|
||||
Assert.Equal("host-dip", metrics.GetProperty("coordinateSpace").GetString());
|
||||
Assert.Equal(96u, metrics.GetProperty("sourceDpi").GetUInt32());
|
||||
Assert.Equal(1d, metrics.GetProperty("hostRasterizationScale").GetDouble());
|
||||
Assert.Equal(500, metrics.GetProperty("doubleClickTimeMilliseconds").GetInt32());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -95,6 +99,22 @@ public sealed class LegacyInteractionMetricsBridgeTests
|
||||
Assert.Equal("host-dip", metrics.GetProperty("coordinateSpace").GetString());
|
||||
Assert.Equal(96u, metrics.GetProperty("sourceDpi").GetUInt32());
|
||||
Assert.Equal(1d, metrics.GetProperty("hostRasterizationScale").GetDouble());
|
||||
Assert.Equal(500, metrics.GetProperty("doubleClickTimeMilliseconds").GetInt32());
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(1)]
|
||||
[InlineData(350)]
|
||||
[InlineData(5_000)]
|
||||
public void FromPixelsAtDpi_PreservesValidWindowsDoubleClickTime(int milliseconds)
|
||||
{
|
||||
var metrics = LegacyInteractionMetrics.FromPixelsAtDpi(
|
||||
4,
|
||||
4,
|
||||
96,
|
||||
doubleClickTimeMilliseconds: milliseconds);
|
||||
|
||||
Assert.Equal(milliseconds, metrics.DoubleClickTimeMilliseconds);
|
||||
}
|
||||
|
||||
private static LegacyOperatorSnapshot CreateSnapshot() => new(
|
||||
|
||||
@@ -57,6 +57,29 @@ public sealed class LegacyManualFinancialBridgeTests
|
||||
out _));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Raw_save_preserves_legacy_strings_but_rejects_any_native_row_locator()
|
||||
{
|
||||
const string json =
|
||||
"{\"type\":\"save-manual-financial-raw\",\"payload\":{\"record\":" +
|
||||
"{\"screen\":\"sales\",\"stockName\":\"Alpha\",\"storageValues\":" +
|
||||
"[\"\",\"_\",\"label_value_extra\",\" 12 \",\"\\u0001legacy\",\"\\u0000tail\"]}}}";
|
||||
|
||||
Assert.True(LegacyBridgeProtocol.TryParseIntent(json, out var parsed, out _));
|
||||
var raw = Assert.IsType<LegacySaveManualFinancialRawIntent>(parsed).Record;
|
||||
Assert.Equal(
|
||||
["", "_", "label_value_extra", " 12 ", "\u0001legacy", "\u0000tail"],
|
||||
raw.StorageValues);
|
||||
|
||||
Assert.False(LegacyBridgeProtocol.TryParseIntent(
|
||||
"{\"type\":\"save-manual-financial-raw\",\"payload\":{\"record\":" +
|
||||
"{\"screen\":\"sales\",\"stockName\":\"Alpha\",\"storageValues\":" +
|
||||
"[\"1Q_1\",\"2Q_2\",\"3Q_3\",\"4Q_4\",\"5Q_5\",\"6Q_6\"]," +
|
||||
"\"rowHandle\":\"AAAAAAAAAAAAAAAAAA\"}}}",
|
||||
out _,
|
||||
out _));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CRUD_and_action_intents_accept_only_opaque_ids_or_empty_payloads()
|
||||
{
|
||||
@@ -97,6 +120,26 @@ public sealed class LegacyManualFinancialBridgeTests
|
||||
out _));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Empty_GraphE_stock_search_is_a_valid_quiet_no_op_intent()
|
||||
{
|
||||
Assert.True(LegacyBridgeProtocol.TryParseIntent(
|
||||
"{\"type\":\"search-manual-financial-stocks\",\"payload\":{\"stockName\":\"\"}}",
|
||||
out var parsed,
|
||||
out _));
|
||||
Assert.Equal(
|
||||
string.Empty,
|
||||
Assert.IsType<LegacySearchManualFinancialStocksIntent>(parsed).StockName);
|
||||
|
||||
Assert.True(LegacyBridgeProtocol.TryParseIntent(
|
||||
"{\"type\":\"select-manual-financial-stock\",\"payload\":{\"resultId\":\"\"}}",
|
||||
out var selection,
|
||||
out _));
|
||||
Assert.Equal(
|
||||
string.Empty,
|
||||
Assert.IsType<LegacySelectManualFinancialStockIntent>(selection).ResultId);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GraphE_find_and_name_sort_require_strict_generation_bound_payloads()
|
||||
{
|
||||
@@ -144,8 +187,21 @@ public sealed class LegacyManualFinancialBridgeTests
|
||||
|
||||
Assert.Equal("sales", manual.GetProperty("screen").GetString());
|
||||
Assert.True(manual.GetProperty("generation").GetInt64() > 0);
|
||||
Assert.Equal(0, manual.GetProperty("listRejectedItemCount").GetInt32());
|
||||
Assert.True(manual.GetProperty("rows")[0].GetProperty("isDataReadable").GetBoolean());
|
||||
Assert.True(manual.GetProperty("rows")[0].GetProperty("isPlayoutReady").GetBoolean());
|
||||
Assert.Equal(
|
||||
["Alpha", "1Q_1", "2Q_2", "3Q_3", "4Q_4", "5Q_5", "6Q_6"],
|
||||
manual.GetProperty("rows")[0]
|
||||
.GetProperty("previewValues")
|
||||
.EnumerateArray()
|
||||
.Select(static value => value.GetString()!)
|
||||
.ToArray());
|
||||
Assert.False(manual.GetProperty("rows")[0].TryGetProperty("rowVersion", out _));
|
||||
Assert.False(manual.GetProperty("rows")[0].TryGetProperty("rowHandle", out _));
|
||||
Assert.Equal("ascending", manual.GetProperty("nameSortDirection").GetString());
|
||||
Assert.Equal("Alpha", manual.GetProperty("selectedRecord").GetProperty("stockName").GetString());
|
||||
Assert.Equal(JsonValueKind.Null, manual.GetProperty("selectedRawRecord").ValueKind);
|
||||
Assert.Equal("sales", manual.GetProperty("action").GetProperty("actionId").GetString());
|
||||
Assert.False(manual.TryGetProperty("rowVersion", out _));
|
||||
Assert.False(manual.TryGetProperty("tableName", out _));
|
||||
|
||||
@@ -83,6 +83,16 @@ public sealed class LegacyManualListsBridgeTests
|
||||
Assert.False(Parse("add-manual-vi-result", "{\"code\":\"005930\",\"name\":\"삼성전자\"}"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void VI_search_accepts_the_original_blank_full_list_request()
|
||||
{
|
||||
Assert.True(LegacyBridgeProtocol.TryParseIntent(
|
||||
"{\"type\":\"search-manual-vi\",\"payload\":{\"query\":\"\"}}",
|
||||
out var intent,
|
||||
out _));
|
||||
Assert.Equal(string.Empty, Assert.IsType<LegacySearchManualViIntent>(intent).Query);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void State_projects_display_rows_but_not_native_codes_versions_or_paths()
|
||||
{
|
||||
@@ -115,7 +125,8 @@ public sealed class LegacyManualListsBridgeTests
|
||||
true,
|
||||
true)
|
||||
{
|
||||
SelectedSearchResultId = "manual-vi-result-1"
|
||||
SelectedSearchResultId = "manual-vi-result-1",
|
||||
SearchIsTruncated = true
|
||||
};
|
||||
var snapshot = new LegacyOperatorSnapshot(
|
||||
1,
|
||||
@@ -135,6 +146,7 @@ public sealed class LegacyManualListsBridgeTests
|
||||
Assert.Equal(
|
||||
"manual-vi-result-1",
|
||||
projected.GetProperty("selectedSearchResultId").GetString());
|
||||
Assert.True(projected.GetProperty("searchIsTruncated").GetBoolean());
|
||||
|
||||
Assert.True(projected.GetProperty("isOpen").GetBoolean());
|
||||
Assert.Equal(9, projected.GetProperty("viItems").GetArrayLength());
|
||||
|
||||
@@ -29,6 +29,31 @@ public sealed class LegacyOperatorCatalogBridgeTests
|
||||
out _));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Stock_search_accepts_explicit_blank_show_all_but_rejects_unsafe_or_overlong_input()
|
||||
{
|
||||
var blank = Assert.IsType<LegacySearchOperatorCatalogStocksIntent>(ParseRequired(
|
||||
"search-operator-catalog-stocks",
|
||||
"""{"query":""}"""));
|
||||
Assert.Equal(string.Empty, blank.Query);
|
||||
|
||||
foreach (var query in new[]
|
||||
{
|
||||
"bad\nvalue",
|
||||
"bad\0value",
|
||||
new string('A', LegacyStockSearchService.MaximumQueryLength + 1)
|
||||
})
|
||||
{
|
||||
Assert.False(Parse(
|
||||
JsonSerializer.Serialize(new
|
||||
{
|
||||
type = "search-operator-catalog-stocks",
|
||||
payload = new { query }
|
||||
}),
|
||||
out _));
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Catalog_selection_and_draft_changes_accept_only_opaque_ids()
|
||||
{
|
||||
@@ -96,6 +121,15 @@ public sealed class LegacyOperatorCatalogBridgeTests
|
||||
Assert.IsType<LegacyBeginCreateThemeCatalogIntent>(ParseRequired(
|
||||
"begin-create-theme-catalog",
|
||||
"""{"market":"nxt"}"""));
|
||||
var marketChange = Assert.IsType<LegacyChangeCreateThemeMarketIntent>(ParseRequired(
|
||||
"change-create-theme-market",
|
||||
"""{"market":"krx","editorName":"사용자 초안"}"""));
|
||||
Assert.Equal(ThemeMarket.Krx, marketChange.Market);
|
||||
Assert.Equal("사용자 초안", marketChange.EditorName);
|
||||
Assert.Equal(string.Empty, Assert.IsType<LegacyChangeCreateThemeMarketIntent>(
|
||||
ParseRequired(
|
||||
"change-create-theme-market",
|
||||
"""{"market":"nxt","editorName":""}""")).EditorName);
|
||||
Assert.IsType<LegacyBeginCreateExpertCatalogIntent>(ParseRequired(
|
||||
"begin-create-expert-catalog",
|
||||
"{}"));
|
||||
@@ -141,6 +175,32 @@ public sealed class LegacyOperatorCatalogBridgeTests
|
||||
{"type":"add-operator-catalog-stock","payload":{"buyAmount":1.5}}
|
||||
""",
|
||||
out _));
|
||||
|
||||
foreach (var payload in new[]
|
||||
{
|
||||
"""{"market":"future","editorName":"초안"}""",
|
||||
"""{"market":"nxt","editorName":" 초안"}""",
|
||||
"""{"market":"nxt","editorName":"bad\nvalue"}""",
|
||||
"""{"market":"nxt"}""",
|
||||
"""{"market":"nxt","editorName":"초안","themeCode":"00000001"}"""
|
||||
})
|
||||
{
|
||||
Assert.False(Parse(
|
||||
"{\"type\":\"change-create-theme-market\",\"payload\":" + payload + "}",
|
||||
out _));
|
||||
}
|
||||
|
||||
Assert.False(Parse(
|
||||
JsonSerializer.Serialize(new
|
||||
{
|
||||
type = "change-create-theme-market",
|
||||
payload = new
|
||||
{
|
||||
market = "krx",
|
||||
editorName = new string('A', 129)
|
||||
}
|
||||
}),
|
||||
out _));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
||||
@@ -83,6 +83,9 @@ public sealed class LegacyOverseasBridgeTests
|
||||
Assert.Equal(
|
||||
"overseas-stock-00000001-0001",
|
||||
overseas.GetProperty("stock").GetProperty("selectedId").GetString());
|
||||
Assert.Equal(
|
||||
0,
|
||||
overseas.GetProperty("stock").GetProperty("isolatedInvalidRowCount").GetInt32());
|
||||
Assert.Equal(12, overseas.GetProperty("actions").GetArrayLength());
|
||||
Assert.All(
|
||||
overseas.GetProperty("actions").EnumerateArray(),
|
||||
@@ -143,6 +146,26 @@ public sealed class LegacyOverseasBridgeTests
|
||||
Assert.DoesNotContain("LNS@UNSUPPORTED", document.RootElement.GetRawText(), StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ProjectionReportsRowsIsolatedByTheReadBoundary()
|
||||
{
|
||||
var workflow = new LegacyOverseasSelectionWorkflow(
|
||||
new StaticIndustryService(),
|
||||
new StaticStockService(isolatedInvalidRowCount: 16));
|
||||
await workflow.SearchStocksAsync(string.Empty);
|
||||
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 stock = document.RootElement.GetProperty("payload")
|
||||
.GetProperty("overseas").GetProperty("stock");
|
||||
|
||||
Assert.Equal(16, stock.GetProperty("isolatedInvalidRowCount").GetInt32());
|
||||
Assert.True(stock.GetProperty("isTruncated").GetBoolean());
|
||||
Assert.Single(stock.GetProperty("results").EnumerateArray());
|
||||
}
|
||||
|
||||
private sealed class StaticIndustryService : IOverseasIndustryIndexSearchService
|
||||
{
|
||||
public Task<OverseasIndustryIndexSearchResult> SearchAsync(
|
||||
@@ -157,7 +180,8 @@ public sealed class LegacyOverseasBridgeTests
|
||||
}
|
||||
|
||||
private sealed class StaticStockService(
|
||||
IReadOnlyList<WorldStockSearchItem>? items = null) : IOverseasStockSearchService
|
||||
IReadOnlyList<WorldStockSearchItem>? items = null,
|
||||
int isolatedInvalidRowCount = 0) : IOverseasStockSearchService
|
||||
{
|
||||
public Task<WorldStockSearchResult> SearchAsync(
|
||||
string query,
|
||||
@@ -167,6 +191,7 @@ public sealed class LegacyOverseasBridgeTests
|
||||
query,
|
||||
DateTimeOffset.Parse("2026-07-15T10:00:00+09:00"),
|
||||
items ?? [new WorldStockSearchItem("Apple", "NAS@AAPL", "US")],
|
||||
IsTruncated: false));
|
||||
IsTruncated: isolatedInvalidRowCount > 0,
|
||||
IsolatedInvalidRowCount: isolatedInvalidRowCount));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -50,6 +50,51 @@ public sealed class LegacyRowDoubleClickBridgeTests
|
||||
out _));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(true)]
|
||||
[InlineData(false)]
|
||||
public void Named_playlist_save_carries_only_the_success_dialog_choice(bool showSavedAlert)
|
||||
{
|
||||
var json = JsonSerializer.Serialize(new
|
||||
{
|
||||
type = "save-current-named-playlist-to",
|
||||
payload = new
|
||||
{
|
||||
definitionId = "named-definition-AAAAAAAAAAAB",
|
||||
showSavedAlert
|
||||
}
|
||||
});
|
||||
|
||||
Assert.True(LegacyBridgeProtocol.TryParseIntent(json, out var parsed, out _));
|
||||
var save = Assert.IsType<LegacySaveCurrentNamedPlaylistToIntent>(parsed);
|
||||
Assert.Equal(showSavedAlert, save.ShowSavedAlert);
|
||||
|
||||
Assert.False(LegacyBridgeProtocol.TryParseIntent(
|
||||
"{\"type\":\"save-current-named-playlist-to\",\"payload\":{" +
|
||||
"\"definitionId\":\"named-definition-AAAAAAAAAAAB\"," +
|
||||
"\"showSavedAlert\":\"yes\"}}",
|
||||
out _,
|
||||
out _));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Named_playlist_load_rejects_save_dialog_metadata()
|
||||
{
|
||||
Assert.True(LegacyBridgeProtocol.TryParseIntent(
|
||||
"{\"type\":\"load-named-playlist-by-id\",\"payload\":{" +
|
||||
"\"definitionId\":\"named-definition-AAAAAAAAAAEF\"}}",
|
||||
out var parsed,
|
||||
out _));
|
||||
Assert.IsType<LegacyLoadNamedPlaylistByIdIntent>(parsed);
|
||||
|
||||
Assert.False(LegacyBridgeProtocol.TryParseIntent(
|
||||
"{\"type\":\"load-named-playlist-by-id\",\"payload\":{" +
|
||||
"\"definitionId\":\"named-definition-AAAAAAAAAAEF\"," +
|
||||
"\"showSavedAlert\":false}}",
|
||||
out _,
|
||||
out _));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Projection_exposes_only_closed_command_receipt_and_opaque_target()
|
||||
{
|
||||
|
||||
@@ -0,0 +1,163 @@
|
||||
using System.Globalization;
|
||||
using System.Text.Json;
|
||||
using MBN_STOCK_WEBVIEW.LegacyApplication;
|
||||
using MMoneyCoderSharp.Data;
|
||||
|
||||
namespace MBN_STOCK_WEBVIEW.LegacyBridge.Tests;
|
||||
|
||||
public sealed class LegacyThemeBridgeTests
|
||||
{
|
||||
[Fact]
|
||||
public async Task ProjectionPreservesFiveThousandRowsAndExplicitTruncationWarningState()
|
||||
{
|
||||
var workflow = new LegacyThemeWorkflow(new TruncatedThemeService());
|
||||
var theme = await workflow.SearchAsync(
|
||||
workflow.CreateInitial(),
|
||||
string.Empty,
|
||||
ThemeSession.PreMarket);
|
||||
var snapshot = new LegacyOperatorSnapshot(
|
||||
1,
|
||||
string.Empty,
|
||||
[],
|
||||
null,
|
||||
[],
|
||||
[],
|
||||
null,
|
||||
"테마 검색 결과가 최대 5,000건으로 제한되었습니다.",
|
||||
LegacyOperatorStatusKind.Warning,
|
||||
Theme: theme);
|
||||
|
||||
using var document = JsonDocument.Parse(
|
||||
LegacyBridgeProtocol.SerializeState(snapshot));
|
||||
var payload = document.RootElement.GetProperty("payload");
|
||||
var projected = payload.GetProperty("theme");
|
||||
|
||||
Assert.True(projected.GetProperty("searchIsTruncated").GetBoolean());
|
||||
Assert.Equal(5_000, projected.GetProperty("searchResults").GetArrayLength());
|
||||
Assert.Equal("warning", payload.GetProperty("statusKind").GetString());
|
||||
Assert.Contains("5,000", payload.GetProperty("statusMessage").GetString());
|
||||
Assert.DoesNotContain("themeCode", projected.GetRawText(), StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ProjectionPreservesRawLegacyTitlesExplicitMarketAndEmptyPreviewGuards()
|
||||
{
|
||||
var workflow = new LegacyThemeWorkflow(new CompatibilityThemeService());
|
||||
var search = await workflow.SearchAsync(
|
||||
workflow.CreateInitial(),
|
||||
string.Empty,
|
||||
ThemeSession.PreMarket);
|
||||
var theme = await workflow.SelectAsync(search, "theme-result-005");
|
||||
var snapshot = new LegacyOperatorSnapshot(
|
||||
1,
|
||||
string.Empty,
|
||||
[],
|
||||
null,
|
||||
[],
|
||||
[],
|
||||
null,
|
||||
"Theme compatibility",
|
||||
LegacyOperatorStatusKind.Information,
|
||||
Theme: theme);
|
||||
|
||||
using var document = JsonDocument.Parse(
|
||||
LegacyBridgeProtocol.SerializeState(snapshot));
|
||||
var projected = document.RootElement.GetProperty("payload").GetProperty("theme");
|
||||
var rows = projected.GetProperty("searchResults").EnumerateArray().ToArray();
|
||||
|
||||
Assert.Equal(5, rows.Length);
|
||||
Assert.Equal(" Legacy ", rows[0].GetProperty("displayTitle").GetString());
|
||||
Assert.Equal("Same", rows[1].GetProperty("displayTitle").GetString());
|
||||
Assert.Equal("Same", rows[2].GetProperty("displayTitle").GetString());
|
||||
Assert.Equal("Desk(NXT)", rows[3].GetProperty("displayTitle").GetString());
|
||||
Assert.Equal("krx", rows[3].GetProperty("market").GetString());
|
||||
Assert.Equal(" Empty NXT(NXT)", rows[4].GetProperty("displayTitle").GetString());
|
||||
Assert.Equal("nxt", rows[4].GetProperty("market").GetString());
|
||||
Assert.Equal(0, projected.GetProperty("previewItems").GetArrayLength());
|
||||
Assert.All(projected.GetProperty("actions").EnumerateArray(), action =>
|
||||
{
|
||||
Assert.False(action.GetProperty("isAvailable").GetBoolean());
|
||||
Assert.Equal(
|
||||
"theme-preview-is-empty",
|
||||
action.GetProperty("unavailableReason").GetString());
|
||||
});
|
||||
Assert.DoesNotContain("themeCode", projected.GetRawText(), StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
private sealed class TruncatedThemeService : IThemeSelectionService
|
||||
{
|
||||
public Task<ThemeSearchResult> SearchAsync(
|
||||
string query,
|
||||
ThemeSession nxtSession,
|
||||
int maximumResults = LegacyThemeSelectionService.DefaultMaximumResults,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
Assert.Equal(LegacyThemeWorkflow.MaximumSearchResults, maximumResults);
|
||||
var rows = Enumerable.Range(0, maximumResults)
|
||||
.Select(index => new ThemeSelectorItem(
|
||||
new ThemeSelectionIdentity(
|
||||
ThemeMarket.Krx,
|
||||
ThemeSession.NotApplicable,
|
||||
index.ToString("D8", CultureInfo.InvariantCulture),
|
||||
$"Theme {index}"),
|
||||
DataSourceKind.Oracle))
|
||||
.ToArray();
|
||||
return Task.FromResult(new ThemeSearchResult(
|
||||
query,
|
||||
nxtSession,
|
||||
DateTimeOffset.UtcNow,
|
||||
rows,
|
||||
IsTruncated: true));
|
||||
}
|
||||
|
||||
public Task<ThemePreviewResult> PreviewAsync(
|
||||
ThemeSelectionIdentity identity,
|
||||
int maximumItems = LegacyThemeSelectionService.DefaultMaximumPreviewItems,
|
||||
CancellationToken cancellationToken = default) =>
|
||||
Task.FromResult(new ThemePreviewResult(
|
||||
identity,
|
||||
DateTimeOffset.UtcNow,
|
||||
[],
|
||||
IsTruncated: false));
|
||||
}
|
||||
|
||||
private sealed class CompatibilityThemeService : IThemeSelectionService
|
||||
{
|
||||
public Task<ThemeSearchResult> SearchAsync(
|
||||
string query,
|
||||
ThemeSession nxtSession,
|
||||
int maximumResults = LegacyThemeSelectionService.DefaultMaximumResults,
|
||||
CancellationToken cancellationToken = default) =>
|
||||
Task.FromResult(new ThemeSearchResult(
|
||||
query,
|
||||
nxtSession,
|
||||
DateTimeOffset.UtcNow,
|
||||
[
|
||||
Item(ThemeMarket.Krx, ThemeSession.NotApplicable, "00000001", " Legacy "),
|
||||
Item(ThemeMarket.Krx, ThemeSession.NotApplicable, "00000002", "Same"),
|
||||
Item(ThemeMarket.Krx, ThemeSession.NotApplicable, "00000003", "Same"),
|
||||
Item(ThemeMarket.Krx, ThemeSession.NotApplicable, "00000004", "Desk(NXT)"),
|
||||
Item(ThemeMarket.Nxt, nxtSession, "00000005", " Empty NXT")
|
||||
],
|
||||
IsTruncated: false));
|
||||
|
||||
public Task<ThemePreviewResult> PreviewAsync(
|
||||
ThemeSelectionIdentity identity,
|
||||
int maximumItems = LegacyThemeSelectionService.DefaultMaximumPreviewItems,
|
||||
CancellationToken cancellationToken = default) =>
|
||||
Task.FromResult(new ThemePreviewResult(
|
||||
identity,
|
||||
DateTimeOffset.UtcNow,
|
||||
[],
|
||||
IsTruncated: false));
|
||||
|
||||
private static ThemeSelectorItem Item(
|
||||
ThemeMarket market,
|
||||
ThemeSession session,
|
||||
string code,
|
||||
string title) =>
|
||||
new(
|
||||
new ThemeSelectionIdentity(market, session, code, title),
|
||||
market == ThemeMarket.Krx ? DataSourceKind.Oracle : DataSourceKind.MariaDb);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user