feat: enable initial-consonant search everywhere

This commit is contained in:
2026-07-27 01:48:32 +09:00
parent 8cad072cf0
commit e3b174eec7
22 changed files with 738 additions and 45 deletions

View File

@@ -54,6 +54,36 @@ public sealed class LegacyExpertWorkflowTests
() => controller.MaterializeSelectedAction("5074-caller-override"));
}
[Fact]
public async Task Search_AcceptsConservativeTruncationAfterInitialFiltering()
{
var service = new DelegateExpertService
{
Search = (query, _, _) => Task.FromResult(
SearchResult(query, Alpha) with { IsTruncated = true })
};
var controller = new LegacyExpertWorkflowController(service);
var searched = await controller.SearchAsync("ㄱㅈㅁ");
Assert.Equal(Alpha.ExpertName, Assert.Single(searched.Search.Results).ExpertName);
Assert.True(searched.Search.IsTruncated);
}
[Fact]
public async Task Search_StillRejectsImpossiblePartialTruncationForOrdinaryText()
{
var service = new DelegateExpertService
{
Search = (query, _, _) => Task.FromResult(
SearchResult(query, Alpha) with { IsTruncated = true })
};
var controller = new LegacyExpertWorkflowController(service);
await Assert.ThrowsAsync<ExpertSelectionDataException>(
() => controller.SearchAsync("Expert"));
}
[Theory]
[InlineData(1, false, 1)]
[InlineData(5, false, 1)]

View File

@@ -366,6 +366,62 @@ public sealed class LegacyThemeWorkflowTests
Assert.Equal("theme-result-5000", snapshot.SearchResults[^1].ResultId);
}
[Fact]
public async Task SearchAsync_AcceptsConservativeTruncationAfterInitialFiltering()
{
var service = new FakeThemeSelectionService
{
SearchHandler = (query, session) => new ThemeSearchResult(
query,
session,
DateTimeOffset.UtcNow,
[
Selector(
ThemeMarket.Krx,
ThemeSession.NotApplicable,
"삼성 테마",
"00000001")
],
IsTruncated: true)
};
var workflow = new LegacyThemeWorkflow(service);
var snapshot = await workflow.SearchAsync(
workflow.CreateInitial(),
"ㅅㅅ",
ThemeSession.PreMarket);
Assert.Equal("삼성 테마", Assert.Single(snapshot.SearchResults).DisplayTitle);
Assert.True(snapshot.SearchIsTruncated);
}
[Fact]
public async Task SearchAsync_StillRejectsImpossiblePartialTruncationForOrdinaryText()
{
var service = new FakeThemeSelectionService
{
SearchHandler = (query, session) => new ThemeSearchResult(
query,
session,
DateTimeOffset.UtcNow,
[
Selector(
ThemeMarket.Krx,
ThemeSession.NotApplicable,
"AI",
"00000001")
],
IsTruncated: true)
};
var workflow = new LegacyThemeWorkflow(service);
await Assert.ThrowsAsync<LegacyThemeWorkflowDataException>(() =>
workflow.SearchAsync(
workflow.CreateInitial(),
"AI",
ThemeSession.PreMarket));
}
[Fact]
public async Task SelectAsync_UsesOpaqueResultIdAndCopiesValidatedPreview()
{

View File

@@ -286,6 +286,78 @@ public sealed class LegacyManualFinancialWorkflowTests
workflow.ToggleNameSort(state, staleGeneration));
}
[Fact]
public async Task Directional_find_matches_Hangul_initial_consonants()
{
var service = new FakeManualFinancialService(
Snapshot(Record(ManualFinancialScreenKind.Sales, "가나다"), 1),
Snapshot(Record(ManualFinancialScreenKind.Sales, "삼성전자"), 2),
Snapshot(Record(ManualFinancialScreenKind.Sales, "현대차"), 3));
var workflow = Workflow(service);
var state = await workflow.SearchAsync(
workflow.CreateInitial(ManualFinancialScreenKind.Sales));
state = workflow.FindByName(
state,
" ㅅㅅㅈ ",
LegacyManualFinancialFindDirection.Down,
state.Revision);
Assert.Equal("ㅅㅅㅈ", state.FindQuery);
Assert.Equal(
"삼성전자",
state.Rows.Single(row => row.ResultId == state.SelectedRowId).StockName);
state = workflow.FindByName(
state,
"ㄱㄴㄷ",
LegacyManualFinancialFindDirection.Up,
state.Revision);
Assert.Equal(
"가나다",
state.Rows.Single(row => row.ResultId == state.SelectedRowId).StockName);
}
[Fact]
public async Task Create_keeps_a_matching_row_inside_an_initial_consonant_filter()
{
const string stockName = "삼성전자";
var service = new FakeManualFinancialService
{
SearchOverride = (screen, query) => new ManualFinancialSearchResult(
screen,
query,
DateTimeOffset.UtcNow,
Array.Empty<ManualFinancialSnapshot>(),
IsTruncated: false)
};
var workflow = new LegacyManualFinancialWorkflow(
service,
new FakeStockSearchService(new StockSearchItem(
StockMarket.Kospi,
DataSourceKind.Oracle,
stockName,
"005930")),
new LegacyManualFinancialWriteSafetyState());
var state = await workflow.SearchAsync(
workflow.CreateInitial(ManualFinancialScreenKind.Sales),
"ㅅㅅㅈ");
state = await workflow.SearchStocksAsync(state, stockName);
state = workflow.SelectStock(
state,
Assert.Single(state.StockCandidates).ResultId);
state = await workflow.ExecuteAsync(
state,
workflow.CreateCreateRequest(
state,
Record(ManualFinancialScreenKind.Sales, stockName)));
Assert.Equal("ㅅㅅㅈ", state.Query);
Assert.Equal(stockName, Assert.Single(state.Rows).StockName);
}
[Theory]
[MemberData(nameof(Screens))]
public async Task Create_save_readback_and_action_id_only_draft_work_for_all_modes(

View File

@@ -34,6 +34,36 @@ public sealed class LegacyTradingHaltWorkflowTests
Assert.Equal("가온전자", current.DisplayName);
}
[Fact]
public async Task Search_AcceptsConservativeTruncationAfterInitialFiltering()
{
var service = new DelegateTradingHaltService
{
Search = (query, _, _) => Task.FromResult(
SearchResult(query, KospiAlpha) with { IsTruncated = true })
};
var controller = new LegacyTradingHaltWorkflowController(service);
var searched = await controller.SearchAsync("ㄱㅈ");
Assert.Equal(KospiAlpha.DisplayName, Assert.Single(searched.Results).DisplayName);
Assert.True(searched.IsTruncated);
}
[Fact]
public async Task Search_StillRejectsImpossiblePartialTruncationForOrdinaryText()
{
var service = new DelegateTradingHaltService
{
Search = (query, _, _) => Task.FromResult(
SearchResult(query, KospiAlpha) with { IsTruncated = true })
};
var controller = new LegacyTradingHaltWorkflowController(service);
await Assert.ThrowsAsync<TradingHaltSelectionDataException>(
() => controller.SearchAsync("Alpha"));
}
[Fact]
public async Task SearchSelect_MaterializesClosedCurrentAndCandleDraftsFromActionIdOnly()
{