From 93cadcb689bcdf98c37dc2176f758833214424f0 Mon Sep 17 00:00:00 2001 From: stepan Date: Sun, 13 Sep 2026 03:00:01 +0300 Subject: [PATCH] =?UTF-8?q?=D0=9F=D0=B5=D1=80=D0=B5=D0=B2=D0=B5=D1=81?= =?UTF-8?q?=D1=82=D0=B8=20FakeKanjStore=20=D0=BD=D0=B0=20NSubstitute?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Contracts/CardComposerTests.cs | 13 +- .../Contracts/CardsServiceTests.cs | 81 ++- .../Contracts/IntegrationsDiTests.cs | 3 +- .../Contracts/PipelineCardWriterTests.cs | 15 +- .../Contracts/PipelineWorkerGrpcAiTests.cs | 13 +- .../Infrastructure/CardMoverTests.cs | 13 +- .../Kanban/AiClassifyContextBuilderTests.cs | 37 +- .../Kanban/CardsServiceRemindersTests.cs | 27 +- .../Modules/Kanban/MlReviewServiceTests.cs | 29 +- .../Modules/Kanban/StorageTickServiceTests.cs | 31 +- .../Support/AdminTickOrchestratorTests.cs | 30 +- .../Support/CardReclassifierTests.cs | 11 +- .../Support/CardsServiceFilesTests.cs | 31 +- .../Support/CardsServiceSelectedTests.cs | 57 +- .../Support/ContainersServiceTests.cs | 25 +- .../Support/ConversionRecomputerTests.cs | 35 +- .../Support/GrpcAiClassifierTests.cs | 25 +- .../Support/LocalColumnSuggesterTests.cs | 29 +- .../Support/PipelineWorkerSchedulerTests.cs | 11 +- .../Support/PipelineWorkerServiceTests.cs | 11 +- .../Support/StorageTickSchedulerTests.cs | 65 +- .../TestKanjStore.cs} | 558 +++++++++--------- 22 files changed, 541 insertions(+), 609 deletions(-) rename src/core/tests/Deal.Tests.Unit/{Modules/Cards/FakeKanjStore.cs => Support/TestKanjStore.cs} (54%) diff --git a/src/core/tests/Deal.Tests.Unit/Contracts/CardComposerTests.cs b/src/core/tests/Deal.Tests.Unit/Contracts/CardComposerTests.cs index b377e17..006b11c 100644 --- a/src/core/tests/Deal.Tests.Unit/Contracts/CardComposerTests.cs +++ b/src/core/tests/Deal.Tests.Unit/Contracts/CardComposerTests.cs @@ -6,7 +6,6 @@ using Deal.Modules.Pipeline.Application.Models; using Deal.Modules.Pipeline.Application.Services; using Deal.Modules.Settings.Application.Models; using Deal.Modules.Settings.Application.Services; -using Deal.Tests.Unit.Modules.Cards; using Deal.Tests.Unit.Modules.Settings; using Deal.Tests.Unit.Support; @@ -109,7 +108,7 @@ public sealed class CardComposerTests [Fact] public async Task BuildAsync_BoardWithoutRules_PlacesCardIntoBoardWithEmptyMatchHits() { - (CardComposer composer, FakeKanjStore store, _) = Create(); + (CardComposer composer, TestKanjStore store, _) = Create(); store.SeedBoard(new ContainerDto { Id = "b_py" }); CardSnapshot snapshot = await composer.BuildAsync( @@ -125,7 +124,7 @@ public sealed class CardComposerTests [Fact] public async Task BuildAsync_BoardWithNonMatchingRules_FallsBackToInbox() { - (CardComposer composer, FakeKanjStore store, _) = Create(); + (CardComposer composer, TestKanjStore store, _) = Create(); store.SeedBoard(BoardWithRules("b_wpf", new[] { "wpf" })); CardSnapshot snapshot = await composer.BuildAsync( @@ -141,7 +140,7 @@ public sealed class CardComposerTests [Fact] public async Task BuildAsync_BoardWithMatchingRules_PlacesCardAndComputesMatchHits() { - (CardComposer composer, FakeKanjStore store, _) = Create(); + (CardComposer composer, TestKanjStore store, _) = Create(); store.SeedBoard(BoardWithRules("b_py", new[] { "python" })); CardSnapshot snapshot = await composer.BuildAsync( @@ -232,11 +231,11 @@ public sealed class CardComposerTests // Создаёт контекст теста: пустые хранилища (дефолты: конверсия включена, курсы — мок). // Возвращает: Кортеж (композитор, канбан-хранилище, KV-настройки). - private static (CardComposer Composer, FakeKanjStore Store, TestSettingsStore Settings) Create() + private static (CardComposer Composer, TestKanjStore Store, TestSettingsStore Settings) Create() { - var store = new FakeKanjStore(); + var store = new TestKanjStore(); var settings = new TestSettingsStore(); - return (new CardComposer(store, settings.Store), store, settings); + return (new CardComposer(store.Store, settings.Store), store, settings); } // Разбор карточки со значениями по умолчанию (сценарий теста перекрывает нужные поля). diff --git a/src/core/tests/Deal.Tests.Unit/Contracts/CardsServiceTests.cs b/src/core/tests/Deal.Tests.Unit/Contracts/CardsServiceTests.cs index 607c5d4..717e77e 100644 --- a/src/core/tests/Deal.Tests.Unit/Contracts/CardsServiceTests.cs +++ b/src/core/tests/Deal.Tests.Unit/Contracts/CardsServiceTests.cs @@ -3,7 +3,6 @@ using Deal.Tests.Unit.Support; using Deal.Modules.Cards.Application.Sources; using Deal.Modules.Kanban.Application.Models; using Deal.Modules.Kanban.Application.Services; -using Deal.Tests.Unit.Modules.Cards; using Deal.Tests.Unit.Modules.Settings; namespace Deal.Tests.Unit.Contracts; @@ -17,7 +16,7 @@ public sealed class CardsServiceTests [Fact] public async Task ListCards_NoCol_ReturnsAllOrderedByReceivedAtDesc() { - (CardsService service, FakeKanjStore store, _, _) = Create(); + (CardsService service, TestKanjStore store, _, _) = Create(); store.SeedCard(Card("l_1", "inbox", receivedAtMs: 100)); store.SeedCard(Card("l_2", "b_x", receivedAtMs: 300)); @@ -30,7 +29,7 @@ public sealed class CardsServiceTests [Fact] public async Task ListCards_ByColumn_ReturnsOnlyColumnCards() { - (CardsService service, FakeKanjStore store, _, _) = Create(); + (CardsService service, TestKanjStore store, _, _) = Create(); store.SeedCard(Card("l_1", "inbox")); store.SeedCard(Card("l_2", KanbanColumns.Trash)); @@ -43,7 +42,7 @@ public sealed class CardsServiceTests [Fact] public async Task GetCard_Existing_ReturnsCard() { - (CardsService service, FakeKanjStore store, _, _) = Create(); + (CardsService service, TestKanjStore store, _, _) = Create(); store.SeedCard(Card("l_1", "inbox", title: "Middle Python")); CardDto? card = await service.GetCardAsync("l_1", CancellationToken.None); @@ -66,7 +65,7 @@ public sealed class CardsServiceTests [Fact] public async Task Move_ToBoardWithRules_UpdatesColumnAndComputesMatchHits() { - (CardsService service, FakeKanjStore store, _, _) = Create(); + (CardsService service, TestKanjStore store, _, _) = Create(); store.SeedBoard(Board("b_py", Rules(stack: new[] { "python" }))); store.SeedCard(Card("l_1", "inbox", sourceMsg: "Нужен Middle Python-разработчик на бота, 1600$.")); @@ -84,7 +83,7 @@ public sealed class CardsServiceTests [Fact] public async Task Move_ToBoard_WritesJournalAndPush() { - (CardsService service, FakeKanjStore store, _, TestMlClient ml) = Create(); + (CardsService service, TestKanjStore store, _, TestMlClient ml) = Create(); store.SeedBoard(Board("b_py", Rules(stack: new[] { "python" }))); store.SeedCard(Card("l_1", "inbox", sourceMsg: "Нужен Python-разработчик")); @@ -105,7 +104,7 @@ public sealed class CardsServiceTests [Fact] public async Task Move_ToInbox_WritesJournalWithoutPush() { - (CardsService service, FakeKanjStore store, _, TestMlClient ml) = Create(); + (CardsService service, TestKanjStore store, _, TestMlClient ml) = Create(); store.SeedBoard(Board("b_py", Rules(stack: new[] { "python" }))); store.SeedCard(Card("l_1", "b_py", sourceMsg: "Нужен Python-разработчик", prevCol: "inbox")); @@ -124,7 +123,7 @@ public sealed class CardsServiceTests [Fact] public async Task Move_ToUnknownBoard_Returns400Text() { - (CardsService service, FakeKanjStore store, _, TestMlClient ml) = Create(); + (CardsService service, TestKanjStore store, _, TestMlClient ml) = Create(); store.SeedCard(Card("l_1", "inbox", sourceMsg: "Нужен Python-разработчик")); CardResultDto result = await service.MoveDashboardCardAsync("l_1", "b_ghost", CancellationToken.None); @@ -148,7 +147,7 @@ public sealed class CardsServiceTests [Fact] public async Task Move_CardMissing_ReturnsNullLead() { - (CardsService service, FakeKanjStore store, _, TestMlClient ml) = Create(); + (CardsService service, TestKanjStore store, _, TestMlClient ml) = Create(); store.SeedBoard(Board("b_py")); CardResultDto result = await service.MoveDashboardCardAsync("l_ghost", "b_py", CancellationToken.None); @@ -162,7 +161,7 @@ public sealed class CardsServiceTests [Fact] public async Task Move_SameColumn_NoOpWithoutJournalAndPush() { - (CardsService service, FakeKanjStore store, _, TestMlClient ml) = Create(); + (CardsService service, TestKanjStore store, _, TestMlClient ml) = Create(); store.SeedBoard(Board("b_py", Rules(stack: new[] { "python" }))); store.SeedCard(Card("l_1", "b_py", sourceMsg: "Нужен Python-разработчик", isNew: true)); @@ -179,7 +178,7 @@ public sealed class CardsServiceTests [Fact] public async Task Move_ToBoardWithoutRules_MatchHitsEmptyButPushSent() { - (CardsService service, FakeKanjStore store, _, TestMlClient ml) = Create(); + (CardsService service, TestKanjStore store, _, TestMlClient ml) = Create(); store.SeedBoard(Board("b_free")); // правил нет store.SeedCard(Card("l_1", "inbox", sourceMsg: "Любой текст")); @@ -195,7 +194,7 @@ public sealed class CardsServiceTests [Fact] public async Task Move_FromBoardToBoard_PrevColIsOldBoardAndPushNewLabel() { - (CardsService service, FakeKanjStore store, _, TestMlClient ml) = Create(); + (CardsService service, TestKanjStore store, _, TestMlClient ml) = Create(); store.SeedBoard(Board("b_java", Rules(stack: new[] { "java" }))); store.SeedBoard(Board("b_py", Rules(stack: new[] { "python" }))); store.SeedCard(Card("l_1", "b_java", sourceMsg: "Нужен Java-разработчик", prevCol: "inbox")); @@ -214,7 +213,7 @@ public sealed class CardsServiceTests [Fact] public async Task Move_EmptySourceText_UsesTitleForMatchHitsAndPush() { - (CardsService service, FakeKanjStore store, _, TestMlClient ml) = Create(); + (CardsService service, TestKanjStore store, _, TestMlClient ml) = Create(); store.SeedBoard(Board("b_py", Rules(stack: new[] { "python" }))); store.SeedCard(Card("l_1", "inbox", title: "Middle Python-разработчик", sourceMsg: " ")); @@ -228,7 +227,7 @@ public sealed class CardsServiceTests [Fact] public async Task Move_EmptySourceAndTitle_JournalWithoutPush() { - (CardsService service, FakeKanjStore store, _, TestMlClient ml) = Create(); + (CardsService service, TestKanjStore store, _, TestMlClient ml) = Create(); store.SeedBoard(Board("b_py")); store.SeedCard(Card("l_1", "inbox")); @@ -241,7 +240,7 @@ public sealed class CardsServiceTests [Fact] public async Task Move_ToBoardWithGradeAndBudget_HitsIncludeGradeWordAndBudget() { - (CardsService service, FakeKanjStore store, _, _) = Create(); + (CardsService service, TestKanjStore store, _, _) = Create(); store.SeedBoard(Board("b_mid", Rules( grade: new[] { "middle" }, budget: new BudgetRangeDto(1000, 3000, "USD")))); @@ -263,7 +262,7 @@ public sealed class CardsServiceTests [Fact] public async Task Move_FromArchive_Returns400AndWritesNothing() { - (CardsService service, FakeKanjStore store, _, TestMlClient ml) = Create(); + (CardsService service, TestKanjStore store, _, TestMlClient ml) = Create(); store.SeedBoard(Board("b_py")); store.SeedCard(Card("l_1", KanbanColumns.Archive, sourceMsg: "Старая вакансия", prevCol: "b_py")); @@ -279,7 +278,7 @@ public sealed class CardsServiceTests [Fact] public async Task Move_FromTrash_Returns400AndWritesNothing() { - (CardsService service, FakeKanjStore store, _, TestMlClient ml) = Create(); + (CardsService service, TestKanjStore store, _, TestMlClient ml) = Create(); store.SeedBoard(Board("b_py")); store.SeedCard(Card("l_1", KanbanColumns.Trash, sourceMsg: "Спам-текст", prevCol: "b_py")); @@ -296,7 +295,7 @@ public sealed class CardsServiceTests [Fact] public async Task Trash_FromInbox_MovesToTrashWithJournalAndSpamPush() { - (CardsService service, FakeKanjStore store, _, TestMlClient ml) = Create(); + (CardsService service, TestKanjStore store, _, TestMlClient ml) = Create(); store.SeedCard(Card("l_1", "inbox", sourceMsg: "Предлагаю услуги продвижения", isNew: true)); CardDto? result = await service.TrashCardAsync("l_1", CancellationToken.None); @@ -318,7 +317,7 @@ public sealed class CardsServiceTests [Fact] public async Task Trash_AlreadyInTrash_NoOpWithoutJournalAndPush() { - (CardsService service, FakeKanjStore store, _, TestMlClient ml) = Create(); + (CardsService service, TestKanjStore store, _, TestMlClient ml) = Create(); store.SeedCard(Card("l_1", KanbanColumns.Trash, sourceMsg: "Текст")); CardDto? result = await service.TrashCardAsync("l_1", CancellationToken.None); @@ -331,7 +330,7 @@ public sealed class CardsServiceTests [Fact] public async Task Trash_FromArchive_WritesJournalWithoutPush() { - (CardsService service, FakeKanjStore store, _, TestMlClient ml) = Create(); + (CardsService service, TestKanjStore store, _, TestMlClient ml) = Create(); store.SeedCard(Card("l_1", KanbanColumns.Archive, sourceMsg: "Старая карточка", prevCol: "b_py")); CardDto? result = await service.TrashCardAsync("l_1", CancellationToken.None); @@ -358,7 +357,7 @@ public sealed class CardsServiceTests [Fact] public async Task Restore_FromTrash_ToPrevColBoard_UnlearnsSpam() { - (CardsService service, FakeKanjStore store, _, TestMlClient ml) = Create(); + (CardsService service, TestKanjStore store, _, TestMlClient ml) = Create(); store.SeedBoard(Board("b_py", Rules(stack: new[] { "python" }))); store.SeedCard(Card("l_1", KanbanColumns.Trash, sourceMsg: "Нужен Python-разработчик", prevCol: "b_py")); @@ -383,7 +382,7 @@ public sealed class CardsServiceTests [Fact] public async Task Restore_FromArchive_NoPush() { - (CardsService service, FakeKanjStore store, _, TestMlClient ml) = Create(); + (CardsService service, TestKanjStore store, _, TestMlClient ml) = Create(); store.SeedBoard(Board("b_py")); store.SeedCard(Card("l_1", KanbanColumns.Archive, sourceMsg: "Старая карточка", prevCol: "b_py")); @@ -399,7 +398,7 @@ public sealed class CardsServiceTests [Fact] public async Task Restore_FromTrash_PrevColDeletedBoard_FallsBackToInbox() { - (CardsService service, FakeKanjStore store, _, TestMlClient ml) = Create(); + (CardsService service, TestKanjStore store, _, TestMlClient ml) = Create(); store.SeedCard(Card("l_1", KanbanColumns.Trash, sourceMsg: "Текст", prevCol: "b_gone")); string? back = await service.RestoreCardAsync("l_1", CancellationToken.None); @@ -414,7 +413,7 @@ public sealed class CardsServiceTests [Fact] public async Task Restore_FromTrash_PrevColInbox_ReturnsInbox() { - (CardsService service, FakeKanjStore store, _, _) = Create(); + (CardsService service, TestKanjStore store, _, _) = Create(); store.SeedCard(Card("l_1", KanbanColumns.Trash, sourceMsg: "Текст", prevCol: "inbox")); string? back = await service.RestoreCardAsync("l_1", CancellationToken.None); @@ -439,7 +438,7 @@ public sealed class CardsServiceTests [Fact] public async Task DeleteForever_RemovesCardAndComments_KeepsJournal() { - (CardsService service, FakeKanjStore store, _, _) = Create(); + (CardsService service, TestKanjStore store, _, _) = Create(); store.SeedCard(Card("l_1", KanbanColumns.Trash, sourceMsg: "Нужен Python-разработчик", comments: [new CardCommentDto("cm_1", "Вы", "Перезвонить", "5 мин")])); @@ -454,7 +453,7 @@ public sealed class CardsServiceTests [Fact] public async Task DeleteForever_JournalRowsSurviveCardDeletion() { - (CardsService service, FakeKanjStore store, _, _) = Create(); + (CardsService service, TestKanjStore store, _, _) = Create(); store.SeedBoard(Board("b_py", Rules(stack: new[] { "python" }))); store.SeedCard(Card("l_1", "inbox", sourceMsg: "Нужен Python-разработчик")); @@ -480,7 +479,7 @@ public sealed class CardsServiceTests [Fact] public async Task ClearCol_Trash_ReturnsClearedCount() { - (CardsService service, FakeKanjStore store, _, _) = Create(); + (CardsService service, TestKanjStore store, _, _) = Create(); store.SeedCard(Card("l_1", KanbanColumns.Trash)); store.SeedCard(Card("l_2", KanbanColumns.Trash)); store.SeedCard(Card("l_3", KanbanColumns.Archive)); @@ -507,7 +506,7 @@ public sealed class CardsServiceTests [Fact] public async Task ClearCol_Board_Returns400Text() { - (CardsService service, FakeKanjStore store, _, _) = Create(); + (CardsService service, TestKanjStore store, _, _) = Create(); store.SeedBoard(Board("b_py")); store.SeedCard(Card("l_1", "b_py")); @@ -531,7 +530,7 @@ public sealed class CardsServiceTests [Fact] public async Task MarkSeen_ById_OnlyThatCard() { - (CardsService service, FakeKanjStore store, _, _) = Create(); + (CardsService service, TestKanjStore store, _, _) = Create(); store.SeedCard(Card("l_1", "inbox", isNew: true)); store.SeedCard(Card("l_2", "inbox", isNew: true)); @@ -544,7 +543,7 @@ public sealed class CardsServiceTests [Fact] public async Task MarkSeen_ByCol_OnlyColumnCards() { - (CardsService service, FakeKanjStore store, _, _) = Create(); + (CardsService service, TestKanjStore store, _, _) = Create(); store.SeedCard(Card("l_1", "inbox", isNew: true)); store.SeedCard(Card("l_2", "inbox", isNew: true)); store.SeedCard(Card("l_3", "b_py", isNew: true)); @@ -558,7 +557,7 @@ public sealed class CardsServiceTests [Fact] public async Task MarkSeen_All_WithoutParameters() { - (CardsService service, FakeKanjStore store, _, _) = Create(); + (CardsService service, TestKanjStore store, _, _) = Create(); store.SeedCard(Card("l_1", "inbox", isNew: true)); store.SeedCard(Card("l_2", KanbanColumns.Trash, isNew: true)); @@ -571,7 +570,7 @@ public sealed class CardsServiceTests [Fact] public async Task AddComment_EmptyOrWhitespaceText_Returns400Text() { - (CardsService service, FakeKanjStore store, _, _) = Create(); + (CardsService service, TestKanjStore store, _, _) = Create(); store.SeedCard(Card("l_1", "inbox")); AddCommentResultDto empty = await service.AddCommentAsync("l_1", " ", CancellationToken.None); @@ -585,7 +584,7 @@ public sealed class CardsServiceTests [Fact] public async Task AddComment_Valid_AppendsTrimmedCommentWithJournal() { - (CardsService service, FakeKanjStore store, _, _) = Create(); + (CardsService service, TestKanjStore store, _, _) = Create(); store.SeedCard(Card("l_1", "inbox")); AddCommentResultDto result = await service.AddCommentAsync("l_1", " Перезвонить завтра ", CancellationToken.None); @@ -618,7 +617,7 @@ public sealed class CardsServiceTests [Fact] public async Task Counts_ColumnsNewAndMlStats() { - (CardsService service, FakeKanjStore store, _, TestMlClient ml) = Create(); + (CardsService service, TestKanjStore store, _, TestMlClient ml) = Create(); store.SeedCard(Card("l_1", "inbox", isNew: true)); store.SeedCard(Card("l_2", "inbox")); store.SeedCard(Card("l_3", "b_py")); @@ -652,7 +651,7 @@ public sealed class CardsServiceTests [Fact] public async Task Search_QueryShorterThanTwoChars_ReturnsEmptyAndDoesNotCallStore() { - (CardsService service, FakeKanjStore store, _, _) = Create(); + (CardsService service, TestKanjStore store, _, _) = Create(); store.SeedCard(Card("l_1", "inbox", title: "Python-разработчик")); IReadOnlyList one = await service.SearchCardsAsync("p", CancellationToken.None); @@ -666,7 +665,7 @@ public sealed class CardsServiceTests [Fact] public async Task Search_DelegatesToStoreWithQueryAndLimit() { - (CardsService service, FakeKanjStore store, _, _) = Create(); + (CardsService service, TestKanjStore store, _, _) = Create(); store.SeedCard(Card("l_1", "inbox", title: "Middle Python-разработчик", receivedAtMs: 300)); IReadOnlyList result = await service.SearchCardsAsync(" pYtHoN ", CancellationToken.None); @@ -681,7 +680,7 @@ public sealed class CardsServiceTests [Fact] public async Task Search_FindsByTitleSummaryContactSourceText_OrderedByReceivedAtDesc() { - (CardsService service, FakeKanjStore store, _, _) = Create(); + (CardsService service, TestKanjStore store, _, _) = Create(); store.SeedCard(Card("l_title", "inbox", title: "Middle Python-разработчик", receivedAtMs: 300)); store.SeedCard(Card("l_summary", "b_py", summary: "проект на python", receivedAtMs: 200)); store.SeedCard(Card("l_contact", "inbox", contact: "@python_dev", receivedAtMs: 100)); @@ -696,7 +695,7 @@ public sealed class CardsServiceTests [Fact] public async Task Search_LimitIsTwelve() { - (CardsService service, FakeKanjStore store, _, _) = Create(); + (CardsService service, TestKanjStore store, _, _) = Create(); for (int i = 0; i < 15; i++) { store.SeedCard(Card($"l_{i:00}", "inbox", title: $"python {i}", receivedAtMs: i)); @@ -711,12 +710,12 @@ public sealed class CardsServiceTests // ─── Хелперы ────────────────────────────────────────────────────────── - private static (CardsService Service, FakeKanjStore Store, TestSettingsStore Settings, TestMlClient Ml) Create() + private static (CardsService Service, TestKanjStore Store, TestSettingsStore Settings, TestMlClient Ml) Create() { - var store = new FakeKanjStore(); + var store = new TestKanjStore(); var settings = new TestSettingsStore(); var ml = new TestMlClient(); - return (new CardsService(store, settings.Store, ml.Client, new TestFileStorage().Storage), store, settings, ml); + return (new CardsService(store.Store, settings.Store, ml.Client, new TestFileStorage().Storage), store, settings, ml); } // Доска с правилами (ContainerRulesDto) либо без них. diff --git a/src/core/tests/Deal.Tests.Unit/Contracts/IntegrationsDiTests.cs b/src/core/tests/Deal.Tests.Unit/Contracts/IntegrationsDiTests.cs index e2957c6..e30468d 100644 --- a/src/core/tests/Deal.Tests.Unit/Contracts/IntegrationsDiTests.cs +++ b/src/core/tests/Deal.Tests.Unit/Contracts/IntegrationsDiTests.cs @@ -13,7 +13,6 @@ using Deal.Modules.Tenants.Application.Abstractions; using Deal.Modules.Tenants.Application.Services; using Deal.SharedKernel.Tenants.Abstractions; using Deal.SharedKernel.Tenants.Models; -using Deal.Tests.Unit.Modules.Cards; using Deal.Tests.Unit.Modules.Kanban; using Deal.Tests.Unit.Modules.Settings; using Deal.Tests.Unit.Modules.Tenants; @@ -146,7 +145,7 @@ public sealed class IntegrationsDiTests services.AddSingleton(tenantContext); services.AddScoped(_ => new TestSettingsStore().Store); services.AddScoped(_ => TestCiphers.New()); - services.AddScoped(_ => new FakeKanjStore()); + services.AddScoped(_ => new TestKanjStore().Store); services.AddScoped(_ => new TestMlLearningStore().Store); services.AddScoped(_ => new TestTenantLimitStore().Store); services.AddScoped(_ => new TokenUsageEventService(new TestTokenUsageEventStore().Store)); diff --git a/src/core/tests/Deal.Tests.Unit/Contracts/PipelineCardWriterTests.cs b/src/core/tests/Deal.Tests.Unit/Contracts/PipelineCardWriterTests.cs index f6e5c5b..cc4d3a5 100644 --- a/src/core/tests/Deal.Tests.Unit/Contracts/PipelineCardWriterTests.cs +++ b/src/core/tests/Deal.Tests.Unit/Contracts/PipelineCardWriterTests.cs @@ -2,7 +2,6 @@ using Deal.Contracts.Integrations.Models; using Deal.Modules.Kanban.Application.Models; using Deal.Modules.Pipeline.Application.Models; using Deal.Modules.Pipeline.Application.Services; -using Deal.Tests.Unit.Modules.Cards; using Deal.Tests.Unit.Modules.Kanban; using Deal.Tests.Unit.Modules.Settings; using Deal.Tests.Unit.Support; @@ -18,7 +17,7 @@ public sealed class PipelineCardWriterTests [Fact] public async Task CreateCard_CreatesInboxCardAndLinksDedupClaim() { - (PipelineCardWriter writer, FakeKanjStore store, TestPipelineStore pipelineStore) = Create(); + (PipelineCardWriter writer, TestKanjStore store, TestPipelineStore pipelineStore) = Create(); const string hash = "abcdef0123456789abcdef0123456789abcdef01"; await pipelineStore.Store.ClaimAsync(hash, CancellationToken.None); // заявка воркера (LeadId=null, Ruling 8) AiParsedCardDto parsed = Parsed( @@ -44,7 +43,7 @@ public sealed class PipelineCardWriterTests [Fact] public async Task CreateCard_AssignedBoardAccepted_WritesCardIntoBoardColumn() { - (PipelineCardWriter writer, FakeKanjStore store, TestPipelineStore pipelineStore) = Create(); + (PipelineCardWriter writer, TestKanjStore store, TestPipelineStore pipelineStore) = Create(); store.SeedBoard(new ContainerDto { Id = "b_py" }); const string hash = "abcdef0123456789abcdef0123456789abcdef01"; await pipelineStore.Store.ClaimAsync(hash, CancellationToken.None); @@ -66,7 +65,7 @@ public sealed class PipelineCardWriterTests [Fact] public async Task CreateCard_AddCardFails_ThrowsAndDoesNotLinkDedup() { - (PipelineCardWriter writer, FakeKanjStore store, TestPipelineStore pipelineStore) = Create(); + (PipelineCardWriter writer, TestKanjStore store, TestPipelineStore pipelineStore) = Create(); store.FailAddCard = true; // сбой адаптера записи (например, недоступна БД тенанта) const string hash = "abcdef0123456789abcdef0123456789abcdef01"; await pipelineStore.Store.ClaimAsync(hash, CancellationToken.None); @@ -84,12 +83,12 @@ public sealed class PipelineCardWriterTests // Создаёт контекст теста: подставки канбана, пайплайна и композитор поверх них. // Возвращает: Кортеж (обёртка, канбан-хранилище, хранилище пайплайна). - private static (PipelineCardWriter Writer, FakeKanjStore Store, TestPipelineStore PipelineStore) Create() + private static (PipelineCardWriter Writer, TestKanjStore Store, TestPipelineStore PipelineStore) Create() { - var store = new FakeKanjStore(); + var store = new TestKanjStore(); var pipelineStore = new TestPipelineStore(); - var composer = new CardComposer(store, new TestSettingsStore().Store); - return (new PipelineCardWriter(store, pipelineStore.Store, composer), store, pipelineStore); + var composer = new CardComposer(store.Store, new TestSettingsStore().Store); + return (new PipelineCardWriter(store.Store, pipelineStore.Store, composer), store, pipelineStore); } // Разбор карточки со значениями по умолчанию (сценарий теста перекрывает нужные поля). diff --git a/src/core/tests/Deal.Tests.Unit/Contracts/PipelineWorkerGrpcAiTests.cs b/src/core/tests/Deal.Tests.Unit/Contracts/PipelineWorkerGrpcAiTests.cs index e5fa47c..4cea455 100644 --- a/src/core/tests/Deal.Tests.Unit/Contracts/PipelineWorkerGrpcAiTests.cs +++ b/src/core/tests/Deal.Tests.Unit/Contracts/PipelineWorkerGrpcAiTests.cs @@ -15,7 +15,6 @@ using Deal.Modules.Tenants.Application.Models; using Deal.Modules.Tenants.Application.Services; using Deal.SharedKernel.Tenants.Models; using Deal.Tests.Unit.Grpc; -using Deal.Tests.Unit.Modules.Cards; using Deal.Tests.Unit.Modules.Kanban; using Deal.Tests.Unit.Modules.Settings; using Deal.Tests.Unit.Modules.Tenants; @@ -155,7 +154,7 @@ public sealed class PipelineWorkerGrpcAiTests private sealed record Context( PipelineWorkerService Worker, TestPipelineStore PipelineStore, - FakeKanjStore KanjStore, + TestKanjStore KanjStore, TestSettingsStore Settings, TestTenantLimitStore Limits); @@ -166,7 +165,7 @@ public sealed class PipelineWorkerGrpcAiTests { var settings = new TestSettingsStore(); var pipelineStore = new TestPipelineStore(); - var kanjStore = new FakeKanjStore(); + var kanjStore = new TestKanjStore(); var mlClient = new TestMlClient { Predict = NotReadyPrediction() }; var rules = new IncomingRules(settings.Store); var fieldsParser = new LocalFieldsParser(settings.Store); @@ -180,7 +179,7 @@ public sealed class PipelineWorkerGrpcAiTests tenantContext, connection, new AiProviderConfigBuilder(settings.Store, TestCiphers.New()), - new AiClassifyContextBuilder(settings.Store, kanjStore), + new AiClassifyContextBuilder(settings.Store, kanjStore.Store), new TokenUsageRecorder(settings.Store, limits.Store, tenantContext, new TokenUsageEventService(new TestTokenUsageEventStore().Store)), NullLogger.Instance); IAiClassifier aiClassifier = budgeted @@ -193,10 +192,10 @@ public sealed class PipelineWorkerGrpcAiTests : grpcClassifier; var processing = new PipelineProcessingService(pipelineStore.Store, mlClient.Client, new PipelineIngestService(pipelineStore.Store)); - var composer = new CardComposer(kanjStore, settings.Store); - var writer = new PipelineCardWriter(kanjStore, pipelineStore.Store, composer); + var composer = new CardComposer(kanjStore.Store, settings.Store); + var writer = new PipelineCardWriter(kanjStore.Store, pipelineStore.Store, composer); var worker = new PipelineWorkerService( - pipelineStore.Store, settings.Store, rules, kanjStore, mlClient.Client, aiClassifier, processing, writer, fieldsParser); + pipelineStore.Store, settings.Store, rules, kanjStore.Store, mlClient.Client, aiClassifier, processing, writer, fieldsParser); return new Context(worker, pipelineStore, kanjStore, settings, limits); } diff --git a/src/core/tests/Deal.Tests.Unit/Infrastructure/CardMoverTests.cs b/src/core/tests/Deal.Tests.Unit/Infrastructure/CardMoverTests.cs index fd9ff3a..8bcd7bd 100644 --- a/src/core/tests/Deal.Tests.Unit/Infrastructure/CardMoverTests.cs +++ b/src/core/tests/Deal.Tests.Unit/Infrastructure/CardMoverTests.cs @@ -6,7 +6,6 @@ using Deal.Modules.Kanban.Application.Models; using Deal.Tests.Unit.Support; using Deal.Modules.Kanban.Application.Services; using Deal.Tests.Unit.Contracts; -using Deal.Tests.Unit.Modules.Cards; using Deal.Tests.Unit.Modules.Settings; namespace Deal.Tests.Unit.Infrastructure; @@ -21,7 +20,7 @@ public sealed class CardMoverTests [Fact] public async Task Move_ToStage_RoutesToStageMoveAndResetsReminder() { - (CardMover mover, FakeKanjStore store) = Create(); + (CardMover mover, TestKanjStore store) = Create(); store.SeedCard(new CardDto { Id = "c_1", @@ -44,7 +43,7 @@ public sealed class CardMoverTests [Fact] public async Task Move_ToBoard_RoutesToDashboard() { - (CardMover mover, FakeKanjStore store) = Create(); + (CardMover mover, TestKanjStore store) = Create(); store.SeedBoard(new ContainerDto { Id = "b_py", Name = "Python" }); store.SeedCard(new CardDto { Id = "c_1", Col = KanbanColumns.Inbox, Content = new SourceContent { Text = "Нужен Python" } }); @@ -59,7 +58,7 @@ public sealed class CardMoverTests [Fact] public async Task Move_UnknownTarget_ReturnsDashboardInvalidTargetError() { - (CardMover mover, FakeKanjStore store) = Create(); + (CardMover mover, TestKanjStore store) = Create(); store.SeedCard(new CardDto { Id = "c_1", Col = KanbanColumns.Inbox, Content = new SourceContent { Text = "Текст" } }); CardMoveResultDto result = await mover.MoveAsync("c_1", "b_ghost", UserMove, CancellationToken.None); @@ -79,10 +78,10 @@ public sealed class CardMoverTests Assert.False(result.Exists); } - private static (CardMover Mover, FakeKanjStore Store) Create() + private static (CardMover Mover, TestKanjStore Store) Create() { - var store = new FakeKanjStore(); - var cardsService = new CardsService(store, new TestSettingsStore().Store, new TestMlClient().Client, new TestFileStorage().Storage); + var store = new TestKanjStore(); + var cardsService = new CardsService(store.Store, new TestSettingsStore().Store, new TestMlClient().Client, new TestFileStorage().Storage); return (new CardMover(cardsService), store); } } diff --git a/src/core/tests/Deal.Tests.Unit/Modules/Kanban/AiClassifyContextBuilderTests.cs b/src/core/tests/Deal.Tests.Unit/Modules/Kanban/AiClassifyContextBuilderTests.cs index af5bb54..829deca 100644 --- a/src/core/tests/Deal.Tests.Unit/Modules/Kanban/AiClassifyContextBuilderTests.cs +++ b/src/core/tests/Deal.Tests.Unit/Modules/Kanban/AiClassifyContextBuilderTests.cs @@ -4,7 +4,6 @@ using Deal.Modules.Pipeline.Application.Services; using Deal.Modules.Settings.Application.Models; using Deal.Tests.Unit.Support; using Deal.Modules.Settings.Application.Services; -using Deal.Tests.Unit.Modules.Cards; using Deal.Tests.Unit.Modules.Settings; namespace Deal.Tests.Unit.Modules.Kanban; @@ -25,11 +24,11 @@ public sealed class AiClassifyContextBuilderTests [Fact] public async Task BuildFilterPrompt_FillsDomainAndKeywords() { - TestSettingsStore settings = Context(out FakeKanjStore kanj); + TestSettingsStore settings = Context(out TestKanjStore kanj); settings.Preload(SettingsKeys.AiFilterPrompt, Json("Страж. Сфера: {domain}. Маркеры: {keywords}.")); settings.Preload(SettingsKeys.DomainDescription, Json(Domain)); settings.Preload(SettingsKeys.DomainKeywords, Json(Keywords)); - var builder = new AiClassifyContextBuilder(settings.Store, kanj); + var builder = new AiClassifyContextBuilder(settings.Store, kanj.Store); string prompt = await builder.BuildFilterPromptAsync(CancellationToken.None); @@ -39,9 +38,9 @@ public sealed class AiClassifyContextBuilderTests [Fact] public async Task BuildFilterPrompt_NoDomain_UsesFallbackTextAndHint() { - TestSettingsStore settings = Context(out FakeKanjStore kanj); + TestSettingsStore settings = Context(out TestKanjStore kanj); settings.Preload(SettingsKeys.AiFilterPrompt, Json("[{domain}] {keywords}")); - var builder = new AiClassifyContextBuilder(settings.Store, kanj); + var builder = new AiClassifyContextBuilder(settings.Store, kanj.Store); string prompt = await builder.BuildFilterPromptAsync(CancellationToken.None); @@ -53,12 +52,12 @@ public sealed class AiClassifyContextBuilderTests [Fact] public async Task BuildClassifySystemPrompt_AppendsCardPromptWhenSet() { - TestSettingsStore settings = Context(out FakeKanjStore kanj); + TestSettingsStore settings = Context(out TestKanjStore kanj); settings.Preload(SettingsKeys.AiPrompt, Json("Классифицируй {domain}.")); settings.Preload(SettingsKeys.CardPrompt, Json("Верни блок «О заявке» {keywords}.")); settings.Preload(SettingsKeys.DomainDescription, Json(Domain)); settings.Preload(SettingsKeys.DomainKeywords, Json(Keywords)); - var builder = new AiClassifyContextBuilder(settings.Store, kanj); + var builder = new AiClassifyContextBuilder(settings.Store, kanj.Store); string prompt = await builder.BuildClassifySystemPromptAsync(CancellationToken.None); @@ -70,8 +69,8 @@ public sealed class AiClassifyContextBuilderTests [Fact] public async Task BuildClassifyUserContext_EmptyBoards_ShowsNoBoardsPhrase() { - TestSettingsStore settings = Context(out FakeKanjStore kanj); - var builder = new AiClassifyContextBuilder(settings.Store, kanj); + TestSettingsStore settings = Context(out TestKanjStore kanj); + var builder = new AiClassifyContextBuilder(settings.Store, kanj.Store); string context = await builder.BuildClassifyUserContextAsync("Нужен разработчик", CancellationToken.None); @@ -83,7 +82,7 @@ public sealed class AiClassifyContextBuilderTests [Fact] public async Task BuildClassifyUserContext_ListsBoardsWithKeywordsAndRules() { - TestSettingsStore settings = Context(out FakeKanjStore kanj); + TestSettingsStore settings = Context(out TestKanjStore kanj); kanj.SeedBoard(new ContainerDto { Id = "b_py", @@ -114,7 +113,7 @@ public sealed class AiClassifyContextBuilderTests Order = 1, }); kanj.SeedBoard(new ContainerDto { Id = "b_sug", Name = "Предложение", Suggested = true, Order = 2 }); - var builder = new AiClassifyContextBuilder(settings.Store, kanj); + var builder = new AiClassifyContextBuilder(settings.Store, kanj.Store); string context = await builder.BuildClassifyUserContextAsync("Ищу Python-разработчика", CancellationToken.None); @@ -129,10 +128,10 @@ public sealed class AiClassifyContextBuilderTests [Fact] public async Task BuildClassifyUserContext_IncludesMarkupExamplesNewestFirst() { - TestSettingsStore settings = Context(out FakeKanjStore kanj); + TestSettingsStore settings = Context(out TestKanjStore kanj); SeedCardWithMove(kanj, "l_1", "b_py", "Пример: middle python (старый)"); SeedCardWithMove(kanj, "l_2", "b_py", "Пример: senior go (свежий)"); - var builder = new AiClassifyContextBuilder(settings.Store, kanj); + var builder = new AiClassifyContextBuilder(settings.Store, kanj.Store); string context = await builder.BuildClassifyUserContextAsync("Ищу разработчика", CancellationToken.None); @@ -145,8 +144,8 @@ public sealed class AiClassifyContextBuilderTests [Fact] public async Task BuildClassifyUserContext_LongMessage_TruncatesTo5000CodePoints() { - TestSettingsStore settings = Context(out FakeKanjStore kanj); - var builder = new AiClassifyContextBuilder(settings.Store, kanj); + TestSettingsStore settings = Context(out TestKanjStore kanj); + var builder = new AiClassifyContextBuilder(settings.Store, kanj.Store); string text = new string('д', 6000); string context = await builder.BuildClassifyUserContextAsync(text, CancellationToken.None); @@ -159,9 +158,9 @@ public sealed class AiClassifyContextBuilderTests // Создаёт контекст: пустое KV + пустой канбан-фейк. // kanj: Канбан-фейк (доски/журнал примеров). // Возвращает: KV-хранилище тенанта. - private static TestSettingsStore Context(out FakeKanjStore kanj) + private static TestSettingsStore Context(out TestKanjStore kanj) { - kanj = new FakeKanjStore(); + kanj = new TestKanjStore(); return new TestSettingsStore(); } @@ -171,12 +170,12 @@ public sealed class AiClassifyContextBuilderTests // board: Колонка-назначение (доска). // text: Исходный текст карточки (source_msg). private static void SeedCardWithMove( - FakeKanjStore kanj, + TestKanjStore kanj, string cardId, string board, string text) { kanj.SeedCard(new CardDto { Id = cardId, Col = board, Content = new SourceContent { Text = text } }); - kanj.AddMoveAsync(new CardMoveDto("lm_" + cardId, cardId, "move", "inbox", board), CancellationToken.None).GetAwaiter().GetResult(); + kanj.Store.AddMoveAsync(new CardMoveDto("lm_" + cardId, cardId, "move", "inbox", board), CancellationToken.None).GetAwaiter().GetResult(); } } diff --git a/src/core/tests/Deal.Tests.Unit/Modules/Kanban/CardsServiceRemindersTests.cs b/src/core/tests/Deal.Tests.Unit/Modules/Kanban/CardsServiceRemindersTests.cs index 2573d25..ea7bd08 100644 --- a/src/core/tests/Deal.Tests.Unit/Modules/Kanban/CardsServiceRemindersTests.cs +++ b/src/core/tests/Deal.Tests.Unit/Modules/Kanban/CardsServiceRemindersTests.cs @@ -3,7 +3,6 @@ using Deal.Tests.Unit.Support; using Deal.Modules.Kanban.Application.Services; using Deal.Modules.Settings.Application.Models; using Deal.Tests.Unit.Contracts; -using Deal.Tests.Unit.Modules.Cards; using Deal.Tests.Unit.Modules.Settings; namespace Deal.Tests.Unit.Modules.Kanban; @@ -19,7 +18,7 @@ public sealed class CardsServiceRemindersTests [Fact] public async Task Set_RemindersDisabled_Returns400TextAndWritesNothing() { - (CardsService service, FakeKanjStore store, _) = Create(remindersEnabled: false); + (CardsService service, TestKanjStore store, _) = Create(remindersEnabled: false); store.SeedCard(Card("c_1", stage: "hold", title: "Отложенный бот")); CardResultDto result = await service.SetReminderAsync("c_1", NowMs() + DayMs, CancellationToken.None); @@ -32,7 +31,7 @@ public sealed class CardsServiceRemindersTests [Fact] public async Task Set_Enabled_SetsReminderAndReturnsCard() { - (CardsService service, FakeKanjStore store, _) = Create(); + (CardsService service, TestKanjStore store, _) = Create(); store.SeedCard(Card("c_1", stage: "hold", title: "Отложенный бот", updatedAtMs: 1)); long atMs = NowMs() + DayMs; @@ -61,7 +60,7 @@ public sealed class CardsServiceRemindersTests [Fact] public async Task Set_StageNotHold_Allowed() { - (CardsService service, FakeKanjStore store, _) = Create(); + (CardsService service, TestKanjStore store, _) = Create(); store.SeedCard(Card("c_1", stage: "work", title: "В работе")); long atMs = NowMs() + DayMs; @@ -76,7 +75,7 @@ public sealed class CardsServiceRemindersTests [Fact] public async Task Clear_WithReminder_ClearsItAndReturnsTrue() { - (CardsService service, FakeKanjStore store, _) = Create(); + (CardsService service, TestKanjStore store, _) = Create(); store.SeedCard(Card("c_1", stage: "hold") with { Reminder = new CardReminderDto(NowMs() - 1) }); bool cleared = await service.ClearReminderAsync("c_1", CancellationToken.None); @@ -98,7 +97,7 @@ public sealed class CardsServiceRemindersTests [Fact] public async Task Clear_RemindersDisabled_StillClears() { - (CardsService service, FakeKanjStore store, _) = Create(remindersEnabled: false); + (CardsService service, TestKanjStore store, _) = Create(remindersEnabled: false); store.SeedCard(Card("c_1", stage: "hold") with { Reminder = new CardReminderDto(NowMs() + DayMs) }); bool cleared = await service.ClearReminderAsync("c_1", CancellationToken.None); @@ -111,7 +110,7 @@ public sealed class CardsServiceRemindersTests [Fact] public async Task Snooze_MovesReminderToNowPlus24h() { - (CardsService service, FakeKanjStore store, _) = Create(); + (CardsService service, TestKanjStore store, _) = Create(); store.SeedCard(Card("c_1", stage: "hold") with { Reminder = new CardReminderDto(NowMs() - 1) }); long beforeMs = NowMs(); @@ -136,7 +135,7 @@ public sealed class CardsServiceRemindersTests [Fact] public async Task Snooze_RemindersDisabled_StillSnoozes() { - (CardsService service, FakeKanjStore store, _) = Create(remindersEnabled: false); + (CardsService service, TestKanjStore store, _) = Create(remindersEnabled: false); store.SeedCard(Card("c_1", stage: "hold") with { Reminder = new CardReminderDto(NowMs() - 1) }); bool snoozed = await service.SnoozeReminderAsync("c_1", CancellationToken.None); @@ -149,7 +148,7 @@ public sealed class CardsServiceRemindersTests [Fact] public async Task CheckDue_Disabled_ClearsExpiredAndReturnsEmpty() { - (CardsService service, FakeKanjStore store, _) = Create(remindersEnabled: false); + (CardsService service, TestKanjStore store, _) = Create(remindersEnabled: false); long pastMs = NowMs() - 1; store.SeedCard(Card("c_hold_past", stage: "hold") with { Reminder = new CardReminderDto(pastMs) }); store.SeedCard(Card("c_work_past", stage: "work") with { Reminder = new CardReminderDto(pastMs) }); @@ -167,7 +166,7 @@ public sealed class CardsServiceRemindersTests [Fact] public async Task CheckDue_Enabled_ReturnsDueAndMarksFired() { - (CardsService service, FakeKanjStore store, _) = Create(); + (CardsService service, TestKanjStore store, _) = Create(); store.SeedCard(Card("c_d1", stage: "hold", title: "Ранний") with { Reminder = new CardReminderDto(NowMs() - 2 * DayMs) }); store.SeedCard(Card("c_d2", stage: "hold", title: "Поздний") with { Reminder = new CardReminderDto(NowMs() - 1) }); store.SeedCard(Card("c_future", stage: "hold", title: "Будущий") with { Reminder = new CardReminderDto(NowMs() + DayMs) }); @@ -181,21 +180,21 @@ public sealed class CardsServiceRemindersTests due.Select(item => (item.Id, item.Title))); Assert.All(due, item => Assert.Equal("hold", item.ContainerId)); // «Выстрелившие» помечены fired: повторная выборка due пуста (признак держит строка БД). - Assert.Empty(await store.ListDueRemindersAsync(DateTimeOffset.UtcNow, CancellationToken.None)); + Assert.Empty(await store.Store.ListDueRemindersAsync(DateTimeOffset.UtcNow, CancellationToken.None)); } // ─── Хелперы ────────────────────────────────────────────────────────── - private static (CardsService Service, FakeKanjStore Store, TestSettingsStore Settings) Create(bool remindersEnabled = true) + private static (CardsService Service, TestKanjStore Store, TestSettingsStore Settings) Create(bool remindersEnabled = true) { - var store = new FakeKanjStore(); + var store = new TestKanjStore(); var settings = new TestSettingsStore(); if (!remindersEnabled) { settings.Preload(SettingsKeys.RemindersEnabled, "false"); } - return (new CardsService(store, settings.Store, new TestMlClient().Client, new TestFileStorage().Storage), store, settings); + return (new CardsService(store.Store, settings.Store, new TestMlClient().Client, new TestFileStorage().Storage), store, settings); } // Текущее время в epoch-мс (UTC) — для посева напоминаний в прошлом/будущем. diff --git a/src/core/tests/Deal.Tests.Unit/Modules/Kanban/MlReviewServiceTests.cs b/src/core/tests/Deal.Tests.Unit/Modules/Kanban/MlReviewServiceTests.cs index 08e21f7..9a2e827 100644 --- a/src/core/tests/Deal.Tests.Unit/Modules/Kanban/MlReviewServiceTests.cs +++ b/src/core/tests/Deal.Tests.Unit/Modules/Kanban/MlReviewServiceTests.cs @@ -5,7 +5,6 @@ using Deal.Modules.Kanban.Application.Services; using Deal.Modules.Pipeline.Application.Models; using Deal.Modules.Pipeline.Application.Services; using Deal.Tests.Unit.Contracts; -using Deal.Tests.Unit.Modules.Cards; using Deal.Tests.Unit.Modules.Settings; using Deal.Tests.Unit.Support; @@ -20,14 +19,14 @@ public sealed class MlReviewServiceTests private static MlReviewService Create( TestPipelineStore pipeline, - FakeKanjStore kanj, + TestKanjStore kanj, TestMlClient ml, out CardsService cards) { var settings = new TestSettingsStore(); - cards = new CardsService(kanj, settings.Store, ml.Client, new TestFileStorage().Storage); + cards = new CardsService(kanj.Store, settings.Store, ml.Client, new TestFileStorage().Storage); var processing = new PipelineProcessingService(pipeline.Store, ml.Client, new PipelineIngestService(pipeline.Store)); - return new MlReviewService(pipeline.Store, kanj, cards, processing, ml.Client); + return new MlReviewService(pipeline.Store, kanj.Store, cards, processing, ml.Client); } private static QueueItemDto QueueRow( @@ -76,7 +75,7 @@ public sealed class MlReviewServiceTests var pipeline = new TestPipelineStore(); pipeline.SeedQueue(QueueRow(101, "из очереди")); pipeline.SeedRejected(RejectedRow(102, "из отсева")); - var kanj = new FakeKanjStore(); + var kanj = new TestKanjStore(); kanj.SeedCard(Card(103, "из карточки")); MlReviewService service = Create(pipeline, kanj, new TestMlClient(), out _); @@ -97,7 +96,7 @@ public sealed class MlReviewServiceTests var pipeline = new TestPipelineStore(); pipeline.SeedQueue(QueueRow(101, "нужный", Dialog)); pipeline.SeedQueue(QueueRow(201, "другой", "d_2")); - MlReviewService service = Create(pipeline, new FakeKanjStore(), new TestMlClient(), out _); + MlReviewService service = Create(pipeline, new TestKanjStore(), new TestMlClient(), out _); IReadOnlyList items = await service.CandidatesAsync(Dialog, 10, CancellationToken.None); @@ -111,7 +110,7 @@ public sealed class MlReviewServiceTests var pipeline = new TestPipelineStore(); pipeline.SeedQueue(QueueRow(101, "a", "d_1")); pipeline.SeedQueue(QueueRow(201, "b", "d_2")); - MlReviewService service = Create(pipeline, new FakeKanjStore(), new TestMlClient(), out _); + MlReviewService service = Create(pipeline, new TestKanjStore(), new TestMlClient(), out _); IReadOnlyList items = await service.CandidatesAsync(null, 10, CancellationToken.None); @@ -127,7 +126,7 @@ public sealed class MlReviewServiceTests pipeline.SeedQueue(QueueRow(100 + i, $"текст {i}")); } - MlReviewService service = Create(pipeline, new FakeKanjStore(), new TestMlClient(), out _); + MlReviewService service = Create(pipeline, new TestKanjStore(), new TestMlClient(), out _); IReadOnlyList items = await service.CandidatesAsync(Dialog, 2, CancellationToken.None); @@ -140,7 +139,7 @@ public sealed class MlReviewServiceTests var pipeline = new TestPipelineStore(); pipeline.SeedQueue(QueueRow(101, "текст")); var ml = new TestMlClient(); - MlReviewService service = Create(pipeline, new FakeKanjStore(), ml, out _); + MlReviewService service = Create(pipeline, new TestKanjStore(), ml, out _); MlApplyResult? result = await service.ApplyAsync(Dialog, 101, MlReviewService.ActionSkip, CancellationToken.None); @@ -154,7 +153,7 @@ public sealed class MlReviewServiceTests [Fact] public async Task Apply_Spam_WithCard_TrashesAndLearns() { - var kanj = new FakeKanjStore(); + var kanj = new TestKanjStore(); kanj.SeedCard(Card(101, "спамный текст")); var ml = new TestMlClient(); MlReviewService service = Create(new TestPipelineStore(), kanj, ml, out _); @@ -175,7 +174,7 @@ public sealed class MlReviewServiceTests var pipeline = new TestPipelineStore(); pipeline.SeedQueue(QueueRow(101, "рекламный текст")); var ml = new TestMlClient(); - MlReviewService service = Create(pipeline, new FakeKanjStore(), ml, out _); + MlReviewService service = Create(pipeline, new TestKanjStore(), ml, out _); MlApplyResult? result = await service.ApplyAsync(Dialog, 101, MlReviewService.ActionSpam, CancellationToken.None); @@ -192,7 +191,7 @@ public sealed class MlReviewServiceTests [Fact] public async Task Apply_Board_MovesCardAndLearns() { - var kanj = new FakeKanjStore(); + var kanj = new TestKanjStore(); kanj.SeedBoard(new ContainerDto { Id = "b_py", @@ -216,7 +215,7 @@ public sealed class MlReviewServiceTests [Fact] public async Task Apply_UnknownBoard_ReturnsError() { - var kanj = new FakeKanjStore(); + var kanj = new TestKanjStore(); kanj.SeedCard(Card(101, "текст")); MlReviewService service = Create(new TestPipelineStore(), kanj, new TestMlClient(), out _); @@ -232,7 +231,7 @@ public sealed class MlReviewServiceTests { var pipeline = new TestPipelineStore(); pipeline.SeedQueue(QueueRow(101, "текст")); - MlReviewService service = Create(pipeline, new FakeKanjStore(), new TestMlClient(), out _); + MlReviewService service = Create(pipeline, new TestKanjStore(), new TestMlClient(), out _); MlApplyResult? result = await service.ApplyAsync(Dialog, 101, "что-то", CancellationToken.None); @@ -244,7 +243,7 @@ public sealed class MlReviewServiceTests [Fact] public async Task Apply_MessageNotFound_ReturnsNull() { - MlReviewService service = Create(new TestPipelineStore(), new FakeKanjStore(), new TestMlClient(), out _); + MlReviewService service = Create(new TestPipelineStore(), new TestKanjStore(), new TestMlClient(), out _); MlApplyResult? result = await service.ApplyAsync(Dialog, 999, MlReviewService.ActionSpam, CancellationToken.None); diff --git a/src/core/tests/Deal.Tests.Unit/Modules/Kanban/StorageTickServiceTests.cs b/src/core/tests/Deal.Tests.Unit/Modules/Kanban/StorageTickServiceTests.cs index fb311ae..69fd13d 100644 --- a/src/core/tests/Deal.Tests.Unit/Modules/Kanban/StorageTickServiceTests.cs +++ b/src/core/tests/Deal.Tests.Unit/Modules/Kanban/StorageTickServiceTests.cs @@ -2,7 +2,6 @@ using Deal.Modules.Kanban.Application.Models; using Deal.Modules.Kanban.Application.Services; using Deal.Modules.Settings.Application.Models; using Deal.Tests.Unit.Support; -using Deal.Tests.Unit.Modules.Cards; using Deal.Tests.Unit.Modules.Settings; namespace Deal.Tests.Unit.Modules.Kanban; @@ -16,7 +15,7 @@ public sealed class StorageTickServiceTests [Fact] public async Task Tick_DefaultSettings_ArchivesExpiredBoardAndInboxCards() { - (StorageTickService service, FakeKanjStore store, _) = Create(); + (StorageTickService service, TestKanjStore store, _) = Create(); store.SeedBoard(Board("b_1")); store.SeedCard(Card("l_board_old", "b_1", ReceivedAtMsAgo(TimeSpan.FromDays(16)))); store.SeedCard(Card("l_board_young", "b_1", ReceivedAtMsAgo(TimeSpan.FromDays(13)))); // < 14 дн. — не кандидат @@ -48,7 +47,7 @@ public sealed class StorageTickServiceTests [Fact] public async Task Tick_AutoArchiveSettingFalse_KeepsExpiredCardsInPlace() { - (StorageTickService service, FakeKanjStore store, TestSettingsStore settings) = Create(); + (StorageTickService service, TestKanjStore store, TestSettingsStore settings) = Create(); settings.Preload(SettingsKeys.AutoArchive, "false"); store.SeedBoard(Board("b_1")); store.SeedCard(Card("l_old", "b_1", ReceivedAtMsAgo(TimeSpan.FromDays(30)))); @@ -63,7 +62,7 @@ public sealed class StorageTickServiceTests [Fact] public async Task Tick_ArchiveAfterDays30_ArchivesOnlyCardsOlderThanConfiguredMonth() { - (StorageTickService service, FakeKanjStore store, TestSettingsStore settings) = Create(); + (StorageTickService service, TestKanjStore store, TestSettingsStore settings) = Create(); settings.Preload(SettingsKeys.ArchiveAfterDays, "30"); store.SeedBoard(Board("b_1")); store.SeedCard(Card("l_older_month", "b_1", ReceivedAtMsAgo(TimeSpan.FromDays(32)))); @@ -79,7 +78,7 @@ public sealed class StorageTickServiceTests [Fact] public async Task Tick_ArchiveAfterDays1_ArchivesTwoDayOldCard() { - (StorageTickService service, FakeKanjStore store, TestSettingsStore settings) = Create(); + (StorageTickService service, TestKanjStore store, TestSettingsStore settings) = Create(); settings.Preload(SettingsKeys.ArchiveAfterDays, "1"); store.SeedCard(Card("l_inbox_old", KanbanColumns.Inbox, ReceivedAtMsAgo(TimeSpan.FromDays(2)))); @@ -92,7 +91,7 @@ public sealed class StorageTickServiceTests [Fact] public async Task Tick_SecondRun_DoesNotReArchiveNorPurgeJustArchivedCards() { - (StorageTickService service, FakeKanjStore store, _) = Create(); + (StorageTickService service, TestKanjStore store, _) = Create(); store.SeedBoard(Board("b_1")); store.SeedCard(Card("l_old", "b_1", ReceivedAtMsAgo(TimeSpan.FromDays(20)))); @@ -110,7 +109,7 @@ public sealed class StorageTickServiceTests [Fact] public async Task Tick_PurgesOnlyArchiveCardsWithArchivedAtOlderThanArchiveClearDays() { - (StorageTickService service, FakeKanjStore store, _) = Create(); + (StorageTickService service, TestKanjStore store, _) = Create(); DateTimeOffset now = DateTimeOffset.UtcNow; store.SeedCard(Card("l_expired", KanbanColumns.Archive, ReceivedAtMsAgo(TimeSpan.FromDays(30)))); store.SetArchivedAt("l_expired", now.AddDays(-100)); // старше 90 дн. @@ -130,7 +129,7 @@ public sealed class StorageTickServiceTests [Fact] public async Task Tick_ArchiveClearDaysFromSettings_ControlsArchivePurgeBoundary() { - (StorageTickService service, FakeKanjStore store, TestSettingsStore settings) = Create(); + (StorageTickService service, TestKanjStore store, TestSettingsStore settings) = Create(); settings.Preload(SettingsKeys.ArchiveClearDays, "30"); DateTimeOffset now = DateTimeOffset.UtcNow; store.SeedCard(Card("l_expired", KanbanColumns.Archive, ReceivedAtMsAgo(TimeSpan.FromDays(10)))); @@ -149,7 +148,7 @@ public sealed class StorageTickServiceTests [Fact] public async Task Tick_PurgesOnlyTrashCardsWithReceivedAtOlderThanTrashClearDays() { - (StorageTickService service, FakeKanjStore store, _) = Create(); + (StorageTickService service, TestKanjStore store, _) = Create(); store.SeedCard(Card("l_old", KanbanColumns.Trash, ReceivedAtMsAgo(TimeSpan.FromDays(10)))); // старше 7 дн. store.SeedCard(Card("l_fresh", KanbanColumns.Trash, ReceivedAtMsAgo(TimeSpan.FromHours(6)))); @@ -166,7 +165,7 @@ public sealed class StorageTickServiceTests [Fact] public async Task Tick_TrashClearDaysFromSettings_ControlsTrashPurgeBoundary() { - (StorageTickService service, FakeKanjStore store, TestSettingsStore settings) = Create(); + (StorageTickService service, TestKanjStore store, TestSettingsStore settings) = Create(); settings.Preload(SettingsKeys.TrashClearDays, "3"); store.SeedCard(Card("l_old", KanbanColumns.Trash, ReceivedAtMsAgo(TimeSpan.FromDays(5)))); // старше 3 дн. store.SeedCard(Card("l_fresh", KanbanColumns.Trash, ReceivedAtMsAgo(TimeSpan.FromDays(1)))); @@ -195,11 +194,11 @@ public sealed class StorageTickServiceTests // ─── Хелперы ──────────────────────────────────────────────────────────── - private static (StorageTickService Service, FakeKanjStore Store, TestSettingsStore Settings) Create() + private static (StorageTickService Service, TestKanjStore Store, TestSettingsStore Settings) Create() { - var store = new FakeKanjStore(); + var store = new TestKanjStore(); var settings = new TestSettingsStore(); - return (new StorageTickService(store, settings.Store), store, settings); + return (new StorageTickService(store.Store, settings.Store), store, settings); } // Доска-колонка минимально (id/имя достаточно для списка кандидатов фейка). @@ -216,13 +215,13 @@ public sealed class StorageTickServiceTests private static long ReceivedAtMsAgo(TimeSpan age) => DateTimeOffset.UtcNow.Subtract(age).ToUnixTimeMilliseconds(); // Колонка карточки в фейке после тика. - private static string ColOf(FakeKanjStore store, string cardId) => + private static string ColOf(TestKanjStore store, string cardId) => store.CardDtos.Single(card => card.Id == cardId).Col; // Флаг is_new карточки после тика. - private static bool IsNewOf(FakeKanjStore store, string cardId) => + private static bool IsNewOf(TestKanjStore store, string cardId) => store.CardDtos.Single(card => card.Id == cardId).IsNew; - private static IReadOnlyList MatchHitsOf(FakeKanjStore store, string cardId) => + private static IReadOnlyList MatchHitsOf(TestKanjStore store, string cardId) => store.CardDtos.Single(card => card.Id == cardId).MatchHits; } diff --git a/src/core/tests/Deal.Tests.Unit/Support/AdminTickOrchestratorTests.cs b/src/core/tests/Deal.Tests.Unit/Support/AdminTickOrchestratorTests.cs index 663c69d..3409bed 100644 --- a/src/core/tests/Deal.Tests.Unit/Support/AdminTickOrchestratorTests.cs +++ b/src/core/tests/Deal.Tests.Unit/Support/AdminTickOrchestratorTests.cs @@ -11,7 +11,6 @@ using Deal.Modules.Pipeline.Application.Parse; using Deal.Modules.Pipeline.Application.Services; using Deal.Modules.Settings.Application.Models; using Deal.Tests.Unit.Contracts; -using Deal.Tests.Unit.Modules.Cards; using Deal.Tests.Unit.Modules.Kanban; using NSubstitute; using Deal.Tests.Unit.Modules.Settings; @@ -31,7 +30,7 @@ public sealed class AdminTickOrchestratorTests private sealed record Context( AdminTickOrchestrator Orchestrator, TestPipelineStore PipelineStore, - FakeKanjStore KanjStore, + TestKanjStore KanjStore, TestSettingsStore Settings, TestAiClassifier AiClassifier, PipelinePumpGate PumpGate, @@ -160,7 +159,7 @@ public sealed class AdminTickOrchestratorTests Assert.Equal("hold", payload.RootElement.GetProperty("containerId").GetString()); // «Выстрелившее» помечено fired: повторная выборка due пуста (признак держит строка БД). - Assert.Empty(await ctx.KanjStore.ListDueRemindersAsync(DateTimeOffset.UtcNow, CancellationToken.None)); + Assert.Empty(await ctx.KanjStore.Store.ListDueRemindersAsync(DateTimeOffset.UtcNow, CancellationToken.None)); } [Fact] @@ -211,24 +210,24 @@ public sealed class AdminTickOrchestratorTests .Returns>(_ => throw new InvalidOperationException("Тестовый сбой чтения очереди (ListAsync).")); } - var kanjStore = new FakeKanjStore(); + var kanjStore = new TestKanjStore(); var mlClient = new TestMlClient(); var aiClassifier = new TestAiClassifier(); var rules = new IncomingRules(settings.Store); var fieldsParser = new LocalFieldsParser(settings.Store); var ingest = new PipelineIngestService(store.Store); var processing = new PipelineProcessingService(store.Store, mlClient.Client, ingest); - var composer = new CardComposer(kanjStore, settings.Store); - var writer = new PipelineCardWriter(kanjStore, store.Store, composer); + var composer = new CardComposer(kanjStore.Store, settings.Store); + var writer = new PipelineCardWriter(kanjStore.Store, store.Store, composer); var worker = new PipelineWorkerService( - store.Store, settings.Store, rules, kanjStore, mlClient.Client, aiClassifier.Classifier, processing, writer, fieldsParser); + store.Store, settings.Store, rules, kanjStore.Store, mlClient.Client, aiClassifier.Classifier, processing, writer, fieldsParser); var broker = new SseBroker(); - var tickService = new StorageTickService(kanjStore, settings.Store); + var tickService = new StorageTickService(kanjStore.Store, settings.Store); var toastPublisher = new StorageToastPublisher(broker); var pumpGate = new PipelinePumpGate(); - FakeKanjStore reminderStore = withThrowingReminderCheck ? new ThrowingDueKanjStore() : kanjStore; - var cardsService = new CardsService(reminderStore, settings.Store, mlClient.Client, new TestFileStorage().Storage); + TestKanjStore reminderStore = withThrowingReminderCheck ? new TestKanjStore(throwOnDueReminders: true) : kanjStore; + var cardsService = new CardsService(reminderStore.Store, settings.Store, mlClient.Client, new TestFileStorage().Storage); var orchestrator = new AdminTickOrchestrator( tickService, processing, worker, cardsService, toastPublisher, broker, pumpGate, NullLogger.Instance); @@ -237,16 +236,7 @@ public sealed class AdminTickOrchestratorTests broker.Subscribe(TenantA)); } - // Хранилище карточек со сбоем выборки due-напоминаний: ListDueRemindersAsync бросает (сценарий - // «БД/схема недоступны» на проверке напоминаний — тик продолжается, reminders ответа пуст). - private sealed class ThrowingDueKanjStore : FakeKanjStore - { - /// - public override Task> ListDueRemindersAsync(DateTimeOffset now, CancellationToken ct) - { - throw new InvalidOperationException("Тестовый сбой выборки due-напоминаний (ListDueRemindersAsync)."); - } - } + // Сбой выборки due-напоминаний задаётся флагом throwOnDueReminders в TestKanjStore. // Текущее время в epoch-мс (UTC) — для посева напоминаний в прошлом/будущем. private static long NowMs() => DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(); diff --git a/src/core/tests/Deal.Tests.Unit/Support/CardReclassifierTests.cs b/src/core/tests/Deal.Tests.Unit/Support/CardReclassifierTests.cs index 61868ff..d771f6b 100644 --- a/src/core/tests/Deal.Tests.Unit/Support/CardReclassifierTests.cs +++ b/src/core/tests/Deal.Tests.Unit/Support/CardReclassifierTests.cs @@ -9,7 +9,6 @@ using Deal.Modules.Pipeline.Application.Parse; using Deal.Modules.Pipeline.Application.Services; using Deal.Modules.Settings.Application.Models; using Deal.Tests.Unit.Contracts; -using Deal.Tests.Unit.Modules.Cards; using Deal.Tests.Unit.Modules.Settings; namespace Deal.Tests.Unit.Support; @@ -22,7 +21,7 @@ public sealed class CardReclassifierTests // Контекст теста: сервис поверх in-memory фейков. private sealed record Context( CardReclassifier Reclassifier, - FakeKanjStore Store, + TestKanjStore Store, TestSettingsStore Settings, TestMlClient MlClient, TestAiClassifier AiClassifier, @@ -33,15 +32,15 @@ public sealed class CardReclassifierTests private static Context CreateContext() { var settings = new TestSettingsStore(); - var store = new FakeKanjStore(); + var store = new TestKanjStore(); var mlClient = new TestMlClient(); var aiClassifier = new TestAiClassifier(); var fieldsParser = new LocalFieldsParser(settings.Store); - var composer = new CardComposer(store, settings.Store); - var cardsService = new CardsService(store, settings.Store, mlClient.Client, new TestFileStorage().Storage); + var composer = new CardComposer(store.Store, settings.Store); + var cardsService = new CardsService(store.Store, settings.Store, mlClient.Client, new TestFileStorage().Storage); var gate = new ReclassifyGate(); var reclassifier = new CardReclassifier( - store, settings.Store, aiClassifier.Classifier, fieldsParser, composer, cardsService, mlClient.Client, gate); + store.Store, settings.Store, aiClassifier.Classifier, fieldsParser, composer, cardsService, mlClient.Client, gate); return new Context(reclassifier, store, settings, mlClient, aiClassifier, gate); } diff --git a/src/core/tests/Deal.Tests.Unit/Support/CardsServiceFilesTests.cs b/src/core/tests/Deal.Tests.Unit/Support/CardsServiceFilesTests.cs index 3c0f10e..cecfa82 100644 --- a/src/core/tests/Deal.Tests.Unit/Support/CardsServiceFilesTests.cs +++ b/src/core/tests/Deal.Tests.Unit/Support/CardsServiceFilesTests.cs @@ -2,7 +2,6 @@ using System.Text; using Deal.Modules.Kanban.Application.Models; using Deal.Modules.Kanban.Application.Services; using Deal.Tests.Unit.Contracts; -using Deal.Tests.Unit.Modules.Cards; using Deal.Tests.Unit.Modules.Settings; namespace Deal.Tests.Unit.Support; @@ -16,7 +15,7 @@ public sealed class CardsServiceFilesTests [Fact] public async Task Add_ImageMime_DetectsKindByMimeWritesObjectAndMeta() { - (CardsService service, FakeKanjStore store, TestFileStorage storage) = Create(); + (CardsService service, TestKanjStore store, TestFileStorage storage) = Create(); store.SeedCard(Card("c_1", updatedAtMs: 1)); byte[] content = Encoding.UTF8.GetBytes("данные-картинки"); using MemoryStream stream = new(content); @@ -42,7 +41,7 @@ public sealed class CardsServiceFilesTests [Fact] public async Task Add_PdfExtensionWithoutMime_DetectsKindByExtension() { - (CardsService service, FakeKanjStore store, _) = Create(); + (CardsService service, TestKanjStore store, _) = Create(); store.SeedCard(Card("c_1")); CardFileDto? entry = await service.AddFileAsync( @@ -57,7 +56,7 @@ public sealed class CardsServiceFilesTests [Fact] public async Task Add_TwoFiles_AppendsPreservingOrderAndBothObjects() { - (CardsService service, FakeKanjStore store, TestFileStorage storage) = Create(); + (CardsService service, TestKanjStore store, TestFileStorage storage) = Create(); store.SeedCard(Card("c_1")); CardFileDto? first = await service.AddFileAsync( @@ -76,7 +75,7 @@ public sealed class CardsServiceFilesTests [Fact] public async Task Add_CardMissing_ReturnsNullAndDoesNotWriteObject() { - (CardsService service, FakeKanjStore store, TestFileStorage storage) = Create(); + (CardsService service, TestKanjStore store, TestFileStorage storage) = Create(); CardFileDto? entry = await service.AddFileAsync( "c_missing", "photo.png", "image/png", new MemoryStream("x"u8.ToArray()), 1, CancellationToken.None); @@ -89,7 +88,7 @@ public sealed class CardsServiceFilesTests [Fact] public async Task Add_EmptyOrWhitespaceFileName_DefaultsToPrototypeFile() { - (CardsService service, FakeKanjStore store, _) = Create(); + (CardsService service, TestKanjStore store, _) = Create(); store.SeedCard(Card("c_1")); CardFileDto? entry = await service.AddFileAsync( @@ -105,7 +104,7 @@ public sealed class CardsServiceFilesTests [Fact] public async Task Add_FileNameWithPathAndQuoteChars_SanitizesObjectKeyButKeepsMetaName() { - (CardsService service, FakeKanjStore store, TestFileStorage storage) = Create(); + (CardsService service, TestKanjStore store, TestFileStorage storage) = Create(); store.SeedCard(Card("c_1")); string rawName = "..\\файл\"отчёта v2.pdf"; @@ -123,7 +122,7 @@ public sealed class CardsServiceFilesTests [Fact] public async Task Add_StreamPositionNotZero_StoresWholeContent() { - (CardsService service, FakeKanjStore store, TestFileStorage storage) = Create(); + (CardsService service, TestKanjStore store, TestFileStorage storage) = Create(); store.SeedCard(Card("c_1")); byte[] content = Encoding.UTF8.GetBytes("полное-содержимое-файла"); using MemoryStream stream = new(content) { Position = 5 }; // эндпоинт мог прочитать поток раньше @@ -141,7 +140,7 @@ public sealed class CardsServiceFilesTests [Fact] public async Task GetEntry_ExistingFile_ReturnsEntryMeta() { - (CardsService service, FakeKanjStore store, _) = Create(); + (CardsService service, TestKanjStore store, _) = Create(); var file = new CardFileDto("pf_1", "tz.pdf", 100, "document", "Документ", "projects/c_1/1710000000000_tz.pdf"); store.SeedCard(Card("c_1") with { Files = new[] { file } }); @@ -164,7 +163,7 @@ public sealed class CardsServiceFilesTests [Fact] public async Task GetEntry_FileNotInMetadata_ReturnsNull() { - (CardsService service, FakeKanjStore store, _) = Create(); + (CardsService service, TestKanjStore store, _) = Create(); store.SeedCard(Card("c_1") with { Files = new[] { new CardFileDto("pf_1", "tz.pdf", 100, "document", "Документ", "k") } }); @@ -177,7 +176,7 @@ public sealed class CardsServiceFilesTests [Fact] public async Task Remove_Existing_DeletesObjectRemovesMetaAndReturnsCard() { - (CardsService service, FakeKanjStore store, TestFileStorage storage) = Create(); + (CardsService service, TestKanjStore store, TestFileStorage storage) = Create(); var first = new CardFileDto("pf_1", "tz.pdf", 100, "document", "Документ", "projects/c_1/1710000000001_tz.pdf"); var second = new CardFileDto("pf_2", "photo.png", 200, "image", "Изображение", "projects/c_1/1710000000002_photo.png"); store.SeedCard(Card("c_1", updatedAtMs: 1) with { Files = new[] { first, second } }); @@ -199,7 +198,7 @@ public sealed class CardsServiceFilesTests [Fact] public async Task Remove_UnknownFileId_ReturnsCardUnchangedWithoutStorageDelete() { - (CardsService service, FakeKanjStore store, TestFileStorage storage) = Create(); + (CardsService service, TestKanjStore store, TestFileStorage storage) = Create(); var file = new CardFileDto("pf_1", "tz.pdf", 100, "document", "Документ", "projects/c_1/1710000000001_tz.pdf"); store.SeedCard(Card("c_1") with { Files = new[] { file } }); @@ -214,7 +213,7 @@ public sealed class CardsServiceFilesTests [Fact] public async Task Remove_EntryWithEmptyObjectKey_SkipsStorageDelete() { - (CardsService service, FakeKanjStore store, TestFileStorage storage) = Create(); + (CardsService service, TestKanjStore store, TestFileStorage storage) = Create(); store.SeedCard(Card("c_1") with { Files = new[] { new CardFileDto("pf_mock", "meta-only.pdf", 10, "document", "Документ", ObjectKey: string.Empty) } }); @@ -238,11 +237,11 @@ public sealed class CardsServiceFilesTests // ─── Хелперы ────────────────────────────────────────────────────────── - private static (CardsService Service, FakeKanjStore Store, TestFileStorage Storage) Create() + private static (CardsService Service, TestKanjStore Store, TestFileStorage Storage) Create() { - var store = new FakeKanjStore(); + var store = new TestKanjStore(); var storage = new TestFileStorage(); - return (new CardsService(store, new TestSettingsStore().Store, new TestMlClient().Client, storage.Storage), store, storage); + return (new CardsService(store.Store, new TestSettingsStore().Store, new TestMlClient().Client, storage.Storage), store, storage); } // Карточка с полями по умолчанию (planned, CreatedAtMs=1, UpdatedAtMs=1; Files пуст). diff --git a/src/core/tests/Deal.Tests.Unit/Support/CardsServiceSelectedTests.cs b/src/core/tests/Deal.Tests.Unit/Support/CardsServiceSelectedTests.cs index 3081645..e2317c2 100644 --- a/src/core/tests/Deal.Tests.Unit/Support/CardsServiceSelectedTests.cs +++ b/src/core/tests/Deal.Tests.Unit/Support/CardsServiceSelectedTests.cs @@ -2,7 +2,6 @@ using System.Text.Json; using Deal.Modules.Kanban.Application.Models; using Deal.Modules.Kanban.Application.Services; using Deal.Tests.Unit.Contracts; -using Deal.Tests.Unit.Modules.Cards; using Deal.Tests.Unit.Modules.Settings; namespace Deal.Tests.Unit.Support; @@ -16,7 +15,7 @@ public sealed class CardsServiceSelectedTests [Fact] public async Task List_NoStage_ReturnsCardsOrderedByUpdatedAtDesc() { - (CardsService service, FakeKanjStore store, _, _) = Create(); + (CardsService service, TestKanjStore store, _, _) = Create(); store.SeedCard(Card("c_1", updatedAtMs: 100)); store.SeedCard(Card("c_2", updatedAtMs: 300)); store.SeedCard(Card("c_3", stage: "work", updatedAtMs: 200)); @@ -29,7 +28,7 @@ public sealed class CardsServiceSelectedTests [Fact] public async Task List_ByStage_ReturnsOnlyStageCards() { - (CardsService service, FakeKanjStore store, _, _) = Create(); + (CardsService service, TestKanjStore store, _, _) = Create(); store.SeedCard(Card("c_1")); store.SeedCard(Card("c_2", stage: "work")); @@ -53,7 +52,7 @@ public sealed class CardsServiceSelectedTests [Fact] public async Task CreateLocal_WithStage_TrimsTitleAndWritesCreatedLocalHistory() { - (CardsService service, FakeKanjStore store, _, _) = Create(); + (CardsService service, TestKanjStore store, _, _) = Create(); CardDto card = await service.CreateLocalCardAsync( new CardLocalCreateDto(Title: " Задача на бота ", ContainerId: "work"), @@ -102,7 +101,7 @@ public sealed class CardsServiceSelectedTests [Fact] public async Task TakeCard_CardMissing_ReturnsNullAndCreatesNothing() { - (CardsService service, FakeKanjStore store, _, _) = Create(); + (CardsService service, TestKanjStore store, _, _) = Create(); CardDto? card = await service.TakeCardAsync("c_missing", CancellationToken.None); @@ -113,7 +112,7 @@ public sealed class CardsServiceSelectedTests [Fact] public async Task TakeCard_MovesCardToPlannedKeepingFields() { - (CardsService service, FakeKanjStore store, _, _) = Create(); + (CardsService service, TestKanjStore store, _, _) = Create(); store.SeedCard(Card( "c_1", stage: "inbox", @@ -150,7 +149,7 @@ public sealed class CardsServiceSelectedTests [Fact] public async Task TakeCard_AlreadyInStage_ReturnsCardUnchanged() { - (CardsService service, FakeKanjStore store, _, _) = Create(); + (CardsService service, TestKanjStore store, _, _) = Create(); store.SeedCard(Card("c_1", stage: "work", title: "Уже взята")); CardDto? card = await service.TakeCardAsync("c_1", CancellationToken.None); @@ -167,7 +166,7 @@ public sealed class CardsServiceSelectedTests [Fact] public async Task Patch_PresentKeys_UpdateFieldsAndBumpUpdatedAt() { - (CardsService service, FakeKanjStore store, _, _) = Create(); + (CardsService service, TestKanjStore store, _, _) = Create(); store.SeedCard(Card( "c_1", title: "Старый заголовок", @@ -198,7 +197,7 @@ public sealed class CardsServiceSelectedTests [Fact] public async Task Patch_BudgetNull_ClearsBudget() { - (CardsService service, FakeKanjStore store, _, _) = Create(); + (CardsService service, TestKanjStore store, _, _) = Create(); store.SeedCard(Card("c_1", title: "С бюджетом", updatedAtMs: 1) with { Budget = new CardBudgetDto(1000, 2000, "USD") }); @@ -213,7 +212,7 @@ public sealed class CardsServiceSelectedTests [Fact] public async Task Patch_StackNull_ClearsStack() { - (CardsService service, FakeKanjStore store, _, _) = Create(); + (CardsService service, TestKanjStore store, _, _) = Create(); store.SeedCard(Card("c_1", title: "С стеком", updatedAtMs: 1) with { Stack = new[] { "Python" } }); @@ -226,7 +225,7 @@ public sealed class CardsServiceSelectedTests [Fact] public async Task Patch_UnknownKeys_AreIgnored() { - (CardsService service, FakeKanjStore store, _, _) = Create(); + (CardsService service, TestKanjStore store, _, _) = Create(); store.SeedCard(Card("c_1", title: "Старый")); CardDto? card = await service.PatchCardAsync( @@ -242,7 +241,7 @@ public sealed class CardsServiceSelectedTests [Fact] public async Task Patch_NullTextKey_IsIgnored() { - (CardsService service, FakeKanjStore store, _, _) = Create(); + (CardsService service, TestKanjStore store, _, _) = Create(); store.SeedCard(Card("c_1", title: "Старый", summary: "Описание", updatedAtMs: 1)); CardDto? card = await service.PatchCardAsync( @@ -272,7 +271,7 @@ public sealed class CardsServiceSelectedTests [Fact] public async Task Move_TwoMoves_AppendHistoryResetReminderAndBumpUpdatedAt() { - (CardsService service, FakeKanjStore store, _, _) = Create(); + (CardsService service, TestKanjStore store, _, _) = Create(); long seededAt = 5_000; CardDto before = Card("c_1", stage: "hold", title: "Отложенная", updatedAtMs: seededAt) with @@ -310,7 +309,7 @@ public sealed class CardsServiceSelectedTests [Fact] public async Task Move_UnknownStage_Returns400ErrorAndLeavesCardUntouched() { - (CardsService service, FakeKanjStore store, _, _) = Create(); + (CardsService service, TestKanjStore store, _, _) = Create(); store.SeedCard(Card("c_1", stage: "planned")); CardResultDto result = await service.MoveStageCardAsync("c_1", "stuck", CancellationToken.None); @@ -337,7 +336,7 @@ public sealed class CardsServiceSelectedTests [Fact] public async Task ClearRejected_RemovesOnlyRejectedAndReturnsCount() { - (CardsService service, FakeKanjStore store, _, _) = Create(); + (CardsService service, TestKanjStore store, _, _) = Create(); store.SeedCard(Card("c_1", stage: "rejected")); store.SeedCard(Card("c_2", stage: "rejected")); store.SeedCard(Card("c_3", stage: "finished")); @@ -363,7 +362,7 @@ public sealed class CardsServiceSelectedTests [Fact] public async Task AddComment_Valid_AppendsTrimmedCommentWithWireForm() { - (CardsService service, FakeKanjStore store, _, _) = Create(); + (CardsService service, TestKanjStore store, _, _) = Create(); store.SeedCard(Card("c_1", title: "Карточка", updatedAtMs: 1)); AddCommentResultDto result = await service.AddCommentAsync("c_1", " Перезвонить завтра ", CancellationToken.None); @@ -381,7 +380,7 @@ public sealed class CardsServiceSelectedTests [Fact] public async Task AddComment_TwoComments_AppendsPreservingOrder() { - (CardsService service, FakeKanjStore store, _, _) = Create(); + (CardsService service, TestKanjStore store, _, _) = Create(); store.SeedCard(Card("c_1", title: "Карточка") with { Comments = new[] { new CardCommentDto("cm_1", "Вы", "Первый", "5 мин") } }); @@ -396,7 +395,7 @@ public sealed class CardsServiceSelectedTests [Fact] public async Task AddComment_EmptyOrWhitespaceText_Returns400DetailAndWritesNothing() { - (CardsService service, FakeKanjStore store, _, _) = Create(); + (CardsService service, TestKanjStore store, _, _) = Create(); store.SeedCard(Card("c_1", title: "Карточка")); AddCommentResultDto empty = await service.AddCommentAsync("c_1", string.Empty, CancellationToken.None); @@ -411,7 +410,7 @@ public sealed class CardsServiceSelectedTests [Fact] public async Task AddComment_CardMissing_ReturnsNullComments() { - (CardsService service, FakeKanjStore store, _, _) = Create(); + (CardsService service, TestKanjStore store, _, _) = Create(); AddCommentResultDto result = await service.AddCommentAsync("c_missing", "Текст", CancellationToken.None); @@ -424,7 +423,7 @@ public sealed class CardsServiceSelectedTests [Fact] public async Task AddLink_NoScheme_PrefixesHttpsAndDefaultsNameToUrl() { - (CardsService service, FakeKanjStore store, _, _) = Create(); + (CardsService service, TestKanjStore store, _, _) = Create(); store.SeedCard(Card("c_1", title: "Карточка", updatedAtMs: 1) with { Links = new[] { new CardLinkDto("pl_1", "Сайт", "https://example.com") } }); @@ -444,7 +443,7 @@ public sealed class CardsServiceSelectedTests [Fact] public async Task AddLink_HasHttpScheme_KeepsSchemeAndUsesTrimmedName() { - (CardsService service, FakeKanjStore store, _, _) = Create(); + (CardsService service, TestKanjStore store, _, _) = Create(); store.SeedCard(Card("c_1", title: "Карточка")); CardResultDto result = await service.AddLinkAsync("c_1", " Сайт ", "http://site.ru/abc", CancellationToken.None); @@ -458,7 +457,7 @@ public sealed class CardsServiceSelectedTests [Fact] public async Task AddLink_EmptyUrl_Returns400DetailAndWritesNothing() { - (CardsService service, FakeKanjStore store, _, _) = Create(); + (CardsService service, TestKanjStore store, _, _) = Create(); store.SeedCard(Card("c_1", title: "Карточка")); CardResultDto result = await service.AddLinkAsync("c_1", "Имя", " ", CancellationToken.None); @@ -471,7 +470,7 @@ public sealed class CardsServiceSelectedTests [Fact] public async Task AddLink_CardMissing_ReturnsNullCardBeforeUrlValidation() { - (CardsService service, FakeKanjStore store, _, _) = Create(); + (CardsService service, TestKanjStore store, _, _) = Create(); CardResultDto result = await service.AddLinkAsync("c_missing", string.Empty, "example.com", CancellationToken.None); @@ -483,7 +482,7 @@ public sealed class CardsServiceSelectedTests [Fact] public async Task RemoveLink_ById_RemovesOnlyTargetAndReturnsCard() { - (CardsService service, FakeKanjStore store, _, _) = Create(); + (CardsService service, TestKanjStore store, _, _) = Create(); store.SeedCard(Card("c_1", title: "Карточка", updatedAtMs: 1) with { @@ -507,7 +506,7 @@ public sealed class CardsServiceSelectedTests [Fact] public async Task RemoveLink_UnknownLinkId_LeavesLinksUnchangedWithoutError() { - (CardsService service, FakeKanjStore store, _, _) = Create(); + (CardsService service, TestKanjStore store, _, _) = Create(); store.SeedCard(Card("c_1", title: "Карточка") with { Links = new[] { new CardLinkDto("pl_1", "Сайт", "https://a.b") } }); @@ -520,7 +519,7 @@ public sealed class CardsServiceSelectedTests [Fact] public async Task RemoveLink_CardMissing_ReturnsNullCard() { - (CardsService service, FakeKanjStore store, _, _) = Create(); + (CardsService service, TestKanjStore store, _, _) = Create(); CardResultDto result = await service.RemoveLinkAsync("c_missing", "pl_1", CancellationToken.None); @@ -531,12 +530,12 @@ public sealed class CardsServiceSelectedTests // ─── Хелперы ────────────────────────────────────────────────────────── - private static (CardsService Service, FakeKanjStore Store, TestSettingsStore Settings, TestMlClient Ml) Create() + private static (CardsService Service, TestKanjStore Store, TestSettingsStore Settings, TestMlClient Ml) Create() { - var store = new FakeKanjStore(); + var store = new TestKanjStore(); var settings = new TestSettingsStore(); var ml = new TestMlClient(); - return (new CardsService(store, settings.Store, ml.Client, new TestFileStorage().Storage), store, settings, ml); + return (new CardsService(store.Store, settings.Store, ml.Client, new TestFileStorage().Storage), store, settings, ml); } // Тело PATCH из пар «ключ → значение» (presence = наличие пары; null — явный JSON-null). diff --git a/src/core/tests/Deal.Tests.Unit/Support/ContainersServiceTests.cs b/src/core/tests/Deal.Tests.Unit/Support/ContainersServiceTests.cs index 34877c9..a329ea2 100644 --- a/src/core/tests/Deal.Tests.Unit/Support/ContainersServiceTests.cs +++ b/src/core/tests/Deal.Tests.Unit/Support/ContainersServiceTests.cs @@ -1,7 +1,6 @@ using System.Text.Json; using Deal.Modules.Kanban.Application.Models; using Deal.Modules.Kanban.Application.Services; -using Deal.Tests.Unit.Modules.Cards; using Deal.Tests.Unit.Modules.Settings; namespace Deal.Tests.Unit.Support; @@ -29,7 +28,7 @@ public sealed class ContainersServiceTests [Fact] public async Task Create_ExistingContainers_OrderIsMaxPlusOne() { - (ContainersService service, FakeKanjStore store, _) = Create(); + (ContainersService service, TestKanjStore store, _) = Create(); store.SeedBoard(Container("b_2", 2)); store.SeedBoard(Container("b_5", 5)); @@ -43,7 +42,7 @@ public sealed class ContainersServiceTests [Fact] public async Task Create_OrderEight_CyclesPaletteToFirst() { - (ContainersService service, FakeKanjStore store, _) = Create(); + (ContainersService service, TestKanjStore store, _) = Create(); store.SeedBoard(Container("b_7", 7)); ContainerDto container = await service.CreateAsync(new ContainerCreateDto("Middle"), CancellationToken.None); @@ -157,7 +156,7 @@ public sealed class ContainersServiceTests [Fact] public async Task List_FillsCountsFromCards() { - (ContainersService service, FakeKanjStore store, _) = Create(); + (ContainersService service, TestKanjStore store, _) = Create(); ContainerDto container = await service.CreateAsync(new ContainerCreateDto("Middle"), CancellationToken.None); store.SeedCard(new CardDto { Id = "c_1", Col = container.Id, IsNew = true }); store.SeedCard(new CardDto { Id = "c_2", Col = container.Id, IsNew = true }); @@ -283,16 +282,16 @@ public sealed class ContainersServiceTests [Fact] public async Task Reorder_AssignsOrdersInGivenOrder() { - (ContainersService service, FakeKanjStore store, _) = Create(); + (ContainersService service, TestKanjStore store, _) = Create(); store.SeedBoard(Container("b_1", 0)); store.SeedBoard(Container("b_2", 1)); store.SeedBoard(Container("b_3", 2)); await service.ReorderAsync(ContainerSpaces.Dashboard, ["b_3", "b_1", "b_2"], CancellationToken.None); - Assert.Equal(0, (await store.GetContainerAsync("b_3", CancellationToken.None))!.Order); - Assert.Equal(1, (await store.GetContainerAsync("b_1", CancellationToken.None))!.Order); - Assert.Equal(2, (await store.GetContainerAsync("b_2", CancellationToken.None))!.Order); + Assert.Equal(0, (await store.Store.GetContainerAsync("b_3", CancellationToken.None))!.Order); + Assert.Equal(1, (await store.Store.GetContainerAsync("b_1", CancellationToken.None))!.Order); + Assert.Equal(2, (await store.Store.GetContainerAsync("b_2", CancellationToken.None))!.Order); } // ─── Удаление контейнера ────────────────────────────────────────────── @@ -300,7 +299,7 @@ public sealed class ContainersServiceTests [Fact] public async Task Delete_MovesCardsToInbox_ReturnsMovedCount() { - (ContainersService service, FakeKanjStore store, _) = Create(); + (ContainersService service, TestKanjStore store, _) = Create(); ContainerDto container = await service.CreateAsync(new ContainerCreateDto("Middle"), CancellationToken.None); store.AddCard("c_1", container.Id); store.AddCard("c_2", container.Id); @@ -309,7 +308,7 @@ public sealed class ContainersServiceTests int moved = await service.DeleteAsync(container.Id, CancellationToken.None); Assert.Equal(2, moved); - Assert.Null(await store.GetContainerAsync(container.Id, CancellationToken.None)); // контейнер удалён + Assert.Null(await store.Store.GetContainerAsync(container.Id, CancellationToken.None)); // контейнер удалён Assert.Contains(store.Cards, card => card.CardId == "c_1" && card.Col == "inbox" && card.IsNew); Assert.Contains(store.Cards, card => card.CardId == "c_2" && card.Col == "inbox" && card.IsNew); Assert.Contains(store.Cards, card => card.CardId == "c_inbox" && card.Col == "inbox"); // inbox не тронут @@ -418,11 +417,11 @@ public sealed class ContainersServiceTests // ─── Хелперы ────────────────────────────────────────────────────────── - private static (ContainersService Service, FakeKanjStore Store, TestSettingsStore Settings) Create() + private static (ContainersService Service, TestKanjStore Store, TestSettingsStore Settings) Create() { - var store = new FakeKanjStore(); + var store = new TestKanjStore(); var settings = new TestSettingsStore(); - return (new ContainersService(store, settings.Store), store, settings); + return (new ContainersService(store.Store, settings.Store), store, settings); } // Патч только с нужными полями: остальные параметры null = «не менять». diff --git a/src/core/tests/Deal.Tests.Unit/Support/ConversionRecomputerTests.cs b/src/core/tests/Deal.Tests.Unit/Support/ConversionRecomputerTests.cs index ffc3b74..a885ee0 100644 --- a/src/core/tests/Deal.Tests.Unit/Support/ConversionRecomputerTests.cs +++ b/src/core/tests/Deal.Tests.Unit/Support/ConversionRecomputerTests.cs @@ -5,7 +5,6 @@ using Deal.Modules.Settings.Application.Abstractions; using Deal.Modules.Settings.Application.Models; using Deal.Tests.Unit.Support; using Deal.Modules.Settings.Application.Services; -using Deal.Tests.Unit.Modules.Cards; using Deal.Tests.Unit.Modules.Settings; namespace Deal.Tests.Unit.Support; @@ -20,7 +19,7 @@ public sealed class ConversionRecomputerTests [Fact] public async Task Recompute_MockRates_ConvertsUsdBudgetToRub() { - (ConversionRecomputer service, FakeKanjStore store, TestSettingsStore settings) = Create(); + (ConversionRecomputer service, TestKanjStore store, TestSettingsStore settings) = Create(); settings.Preload(SettingsKeys.RatesCache, CacheJson(MockRates.Values)); store.SeedCard(Card("l_board", "b_1", from: 100, to: 100, cur: "USD")); store.SeedCard(Card("l_inbox", KanbanColumns.Inbox, from: 50, to: 200, cur: "USD")); @@ -35,7 +34,7 @@ public sealed class ConversionRecomputerTests [Fact] public async Task Recompute_TargetCurrencyFromSettings_ConvertsToConfiguredCurrency() { - (ConversionRecomputer service, FakeKanjStore store, TestSettingsStore settings) = Create(); + (ConversionRecomputer service, TestKanjStore store, TestSettingsStore settings) = Create(); settings.Preload(SettingsKeys.RatesCache, CacheJson(MockRates.Values)); settings.Preload(SettingsKeys.TargetCurrency, "\"EUR\""); // PATCH пишет Upper (ApplyStringKey) store.SeedCard(Card("l_board", "b_1", from: 100, to: 100, cur: "USD")); @@ -50,7 +49,7 @@ public sealed class ConversionRecomputerTests [Fact] public async Task Recompute_NoTargetCurrencySetting_DefaultsToRub() { - (ConversionRecomputer service, FakeKanjStore store, TestSettingsStore settings) = Create(); + (ConversionRecomputer service, TestKanjStore store, TestSettingsStore settings) = Create(); settings.Preload(SettingsKeys.RatesCache, CacheJson(MockRates.Values)); store.SeedCard(Card("l_board", "b_1", from: 100, to: 100, cur: "USD")); @@ -63,7 +62,7 @@ public sealed class ConversionRecomputerTests [Fact] public async Task Recompute_UsdtBudget_TreatedAsUsdRate() { - (ConversionRecomputer service, FakeKanjStore store, TestSettingsStore settings) = Create(); + (ConversionRecomputer service, TestKanjStore store, TestSettingsStore settings) = Create(); var rates = new Dictionary { ["RUB"] = 1.0, ["USD"] = 100.0, ["USDT"] = 90.0 }; settings.Preload(SettingsKeys.RatesCache, CacheJson(rates)); store.SeedCard(Card("l_board", "b_1", from: 100, to: 100, cur: "USDT")); @@ -79,7 +78,7 @@ public sealed class ConversionRecomputerTests [Fact] public async Task Recompute_BudgetWithoutUpperBound_UsesLowerBoundForConvTo() { - (ConversionRecomputer service, FakeKanjStore store, TestSettingsStore settings) = Create(); + (ConversionRecomputer service, TestKanjStore store, TestSettingsStore settings) = Create(); settings.Preload(SettingsKeys.RatesCache, CacheJson(MockRates.Values)); store.SeedCard(Card("l_board", "b_1", from: 100, to: null, cur: "USD")); @@ -94,7 +93,7 @@ public sealed class ConversionRecomputerTests [Fact] public async Task Recompute_ConversionOff_ReturnsZeroAndKeepsCardsUntouched() { - (ConversionRecomputer service, FakeKanjStore store, TestSettingsStore settings) = Create(); + (ConversionRecomputer service, TestKanjStore store, TestSettingsStore settings) = Create(); settings.Preload(SettingsKeys.ConversionOn, "false"); settings.Preload(SettingsKeys.RatesCache, CacheJson(MockRates.Values)); store.SeedCard(Card("l_board", "b_1", from: 100, to: 100, cur: "USD")); @@ -110,7 +109,7 @@ public sealed class ConversionRecomputerTests [Fact] public async Task Recompute_ExcludesArchiveAndTrashCards() { - (ConversionRecomputer service, FakeKanjStore store, TestSettingsStore settings) = Create(); + (ConversionRecomputer service, TestKanjStore store, TestSettingsStore settings) = Create(); settings.Preload(SettingsKeys.RatesCache, CacheJson(MockRates.Values)); store.SeedCard(Card("l_board", "b_1", from: 100, to: 100, cur: "USD")); store.SeedCard(Card("l_archive", KanbanColumns.Archive, from: 100, to: 100, cur: "USD")); @@ -129,7 +128,7 @@ public sealed class ConversionRecomputerTests [Fact] public async Task Recompute_MissingCurrencyInRates_SkipsCardKeepingOldConversion() { - (ConversionRecomputer service, FakeKanjStore store, TestSettingsStore settings) = Create(); + (ConversionRecomputer service, TestKanjStore store, TestSettingsStore settings) = Create(); settings.Preload(SettingsKeys.RatesCache, CacheJson(new Dictionary { ["RUB"] = 1.0, ["USD"] = 92.5 })); store.SeedCard(Card("l_board", "b_1", from: 100, to: 100, cur: "XXX") with { @@ -148,7 +147,7 @@ public sealed class ConversionRecomputerTests [Fact] public async Task Recompute_NoRatesCache_ReturnsZeroAndKeepsCardsUntouched() { - (ConversionRecomputer service, FakeKanjStore store, _) = Create(); + (ConversionRecomputer service, TestKanjStore store, _) = Create(); store.SeedCard(Card("l_board", "b_1", from: 100, to: 100, cur: "USD")); int updated = await service.RecomputeAsync(CancellationToken.None); @@ -160,7 +159,7 @@ public sealed class ConversionRecomputerTests [Fact] public async Task Recompute_CorruptedRatesCache_ReturnsZero() { - (ConversionRecomputer service, FakeKanjStore store, TestSettingsStore settings) = Create(); + (ConversionRecomputer service, TestKanjStore store, TestSettingsStore settings) = Create(); settings.Preload(SettingsKeys.RatesCache, "{это не JSON"); store.SeedCard(Card("l_board", "b_1", from: 100, to: 100, cur: "USD")); @@ -175,7 +174,7 @@ public sealed class ConversionRecomputerTests [Fact] public async Task OnRatesChangedAsync_PerformsFullRecompute() { - (ConversionRecomputer service, FakeKanjStore store, TestSettingsStore settings) = Create(); + (ConversionRecomputer service, TestKanjStore store, TestSettingsStore settings) = Create(); settings.Preload(SettingsKeys.RatesCache, CacheJson(MockRates.Values)); store.SeedCard(Card("l_board", "b_1", from: 100, to: 100, cur: "USD")); @@ -190,7 +189,7 @@ public sealed class ConversionRecomputerTests [Fact] public async Task Recompute_SecondRun_RecomputesSameValues() { - (ConversionRecomputer service, FakeKanjStore store, TestSettingsStore settings) = Create(); + (ConversionRecomputer service, TestKanjStore store, TestSettingsStore settings) = Create(); settings.Preload(SettingsKeys.RatesCache, CacheJson(MockRates.Values)); store.SeedCard(Card("l_board", "b_1", from: 100, to: 100, cur: "USD")); @@ -204,11 +203,11 @@ public sealed class ConversionRecomputerTests // ─── Хелперы ──────────────────────────────────────────────────────────── - private static (ConversionRecomputer Service, FakeKanjStore Store, TestSettingsStore Settings) Create() + private static (ConversionRecomputer Service, TestKanjStore Store, TestSettingsStore Settings) Create() { - var store = new FakeKanjStore(); + var store = new TestKanjStore(); var settings = new TestSettingsStore(); - return (new ConversionRecomputer(settings.Store, store), store, settings); + return (new ConversionRecomputer(settings.Store, store.Store), store, settings); } // Карточка с бюджетом в колонке (BudgetCur задан → Budget не null, как маппинг адаптера). @@ -238,7 +237,7 @@ public sealed class ConversionRecomputerTests // Конверсия (Converted) карточки в фейке после пересчёта. // store: Фейк-хранилище. // cardId: Id карточки. - private static CardBudgetDto? ConvertedOf(FakeKanjStore store, string cardId) => + private static CardBudgetDto? ConvertedOf(TestKanjStore store, string cardId) => store.CardDtos.Single(card => card.Id == cardId).Converted; // Проверяет conv-поля карточки (From/To с допуском 0.001 и валюту). @@ -248,7 +247,7 @@ public sealed class ConversionRecomputerTests // convTo: Ожидаемая верхняя граница. // convCur: Ожидаемая валюта конверсии. private static void AssertConverted( - FakeKanjStore store, + TestKanjStore store, string cardId, double convFrom, double convTo, diff --git a/src/core/tests/Deal.Tests.Unit/Support/GrpcAiClassifierTests.cs b/src/core/tests/Deal.Tests.Unit/Support/GrpcAiClassifierTests.cs index 9c0b2fa..5457266 100644 --- a/src/core/tests/Deal.Tests.Unit/Support/GrpcAiClassifierTests.cs +++ b/src/core/tests/Deal.Tests.Unit/Support/GrpcAiClassifierTests.cs @@ -15,7 +15,6 @@ using Deal.Modules.Settings.Application.Models; using Deal.Modules.Tenants.Application.Services; using Deal.SharedKernel.Tenants.Models; using Deal.Tests.Unit.Grpc; -using Deal.Tests.Unit.Modules.Cards; using Deal.Tests.Unit.Modules.Settings; using Deal.Tests.Unit.Modules.Tenants; using Microsoft.Extensions.Logging.Abstractions; @@ -52,7 +51,7 @@ public sealed class GrpcAiClassifierTests Reason = "реклама", Usage = new Usage { Prompt = 500, Completion = 40, Total = 540 }, }; - (TestSettingsStore settings, ISecretCipher cipher, FakeKanjStore kanjStore) = Context(port); + (TestSettingsStore settings, ISecretCipher cipher, TestKanjStore kanjStore) = Context(port); settings.Preload(SettingsKeys.AiFilterPrompt, Json("Фильтруй. {domain} | {keywords}.")); settings.Preload(SettingsKeys.DomainDescription, Json(TestDomain)); settings.Preload(SettingsKeys.DomainKeywords, Json(TestKeywords)); @@ -88,7 +87,7 @@ public sealed class GrpcAiClassifierTests await AiGrpcTestHost.RunAsync(AiGrpcTestHost.DefaultToken, new RecordingAiService(), async (port, service) => { service.FilterUnavailable = true; - (TestSettingsStore settings, ISecretCipher cipher, FakeKanjStore kanjStore) = Context(port); + (TestSettingsStore settings, ISecretCipher cipher, TestKanjStore kanjStore) = Context(port); IAiClassifier classifier = CreateClassifier(port, settings, cipher, kanjStore); await Assert.ThrowsAsync( @@ -102,7 +101,7 @@ public sealed class GrpcAiClassifierTests await AiGrpcTestHost.RunAsync(AiGrpcTestHost.DefaultToken, new RecordingAiService(), async (port, service) => { string text = new string('а', 5000); - (TestSettingsStore settings, ISecretCipher cipher, FakeKanjStore kanjStore) = Context(port); + (TestSettingsStore settings, ISecretCipher cipher, TestKanjStore kanjStore) = Context(port); IAiClassifier classifier = CreateClassifier(port, settings, cipher, kanjStore); await classifier.FilterAsync(text, CancellationToken.None); @@ -131,7 +130,7 @@ public sealed class GrpcAiClassifierTests """, Usage = new Usage { Prompt = 3000, Completion = 700, Total = 3700 }, }; - (TestSettingsStore settings, ISecretCipher cipher, FakeKanjStore kanjStore) = Context(port); + (TestSettingsStore settings, ISecretCipher cipher, TestKanjStore kanjStore) = Context(port); settings.Preload(SettingsKeys.AiPrompt, Json("Разбор: {domain}.")); settings.Preload(SettingsKeys.CardPrompt, Json("Верни блок «О заявке».")); settings.Preload(SettingsKeys.DomainDescription, Json(TestDomain)); @@ -191,7 +190,7 @@ public sealed class GrpcAiClassifierTests Ok = false, Usage = new Usage { Prompt = 900, Completion = 0, Total = 900 }, }; - (TestSettingsStore settings, ISecretCipher cipher, FakeKanjStore kanjStore) = Context(port); + (TestSettingsStore settings, ISecretCipher cipher, TestKanjStore kanjStore) = Context(port); TestTenantLimitStore limits = new(); IAiClassifier classifier = CreateClassifier(port, settings, cipher, kanjStore, limits); @@ -212,7 +211,7 @@ public sealed class GrpcAiClassifierTests await AiGrpcTestHost.RunAsync(AiGrpcTestHost.DefaultToken, new RecordingAiService(), async (port, service) => { service.ClassifyUnavailable = true; - (TestSettingsStore settings, ISecretCipher cipher, FakeKanjStore kanjStore) = Context(port); + (TestSettingsStore settings, ISecretCipher cipher, TestKanjStore kanjStore) = Context(port); IAiClassifier classifier = CreateClassifier(port, settings, cipher, kanjStore); await Assert.ThrowsAsync( @@ -226,7 +225,7 @@ public sealed class GrpcAiClassifierTests await AiGrpcTestHost.RunAsync(AiGrpcTestHost.DefaultToken, new RecordingAiService(), async (port, service) => { service.ClassifyReply = new ClassifyReply { Ok = true, Json = """{"title":"Заголовок","stack":[],"is_spam":false}""" }; - (TestSettingsStore settings, ISecretCipher cipher, FakeKanjStore kanjStore) = Context(port); + (TestSettingsStore settings, ISecretCipher cipher, TestKanjStore kanjStore) = Context(port); settings.Preload(SettingsKeys.AiProvider, Json("anthropic")); settings.Preload( SettingsKeys.AiConfigs, @@ -263,7 +262,7 @@ public sealed class GrpcAiClassifierTests { service.ClassifyReply = new ClassifyReply { Ok = true, Json = """{"title":"Т","stack":[],"is_spam":false}""" }; string text = new string('б', 6000); - (TestSettingsStore settings, ISecretCipher cipher, FakeKanjStore kanjStore) = Context(port); + (TestSettingsStore settings, ISecretCipher cipher, TestKanjStore kanjStore) = Context(port); IAiClassifier classifier = CreateClassifier(port, settings, cipher, kanjStore); await classifier.ClassifyAsync(text, CancellationToken.None); @@ -286,7 +285,7 @@ public sealed class GrpcAiClassifierTests int port, TestSettingsStore settings, ISecretCipher cipher, - FakeKanjStore kanjStore, + TestKanjStore kanjStore, TestTenantLimitStore? limits = null) { limits ??= new TestTenantLimitStore(); @@ -297,7 +296,7 @@ public sealed class GrpcAiClassifierTests tenantContext, connection, new AiProviderConfigBuilder(settings.Store, cipher), - new AiClassifyContextBuilder(settings.Store, kanjStore), + new AiClassifyContextBuilder(settings.Store, kanjStore.Store), new TokenUsageRecorder(settings.Store, limits.Store, tenantContext, new TokenUsageEventService(new TestTokenUsageEventStore().Store)), NullLogger.Instance); } @@ -305,8 +304,8 @@ public sealed class GrpcAiClassifierTests // Создаёт контекст сценария (пустые фейки; сценарий переопределяет настройки/доски). // port: Порт хоста (не используется контекстом — единый вид хелперов). // Возвращает: Кортеж фейков (настройки, шифр, канбан). - private static (TestSettingsStore Settings, ISecretCipher Cipher, FakeKanjStore Kanj) Context(int port) - => (new TestSettingsStore(), TestCiphers.New(), new FakeKanjStore()); + private static (TestSettingsStore Settings, ISecretCipher Cipher, TestKanjStore Kanj) Context(int port) + => (new TestSettingsStore(), TestCiphers.New(), new TestKanjStore()); // Сериализует значение настройки в JSON-строку (как пишет SettingsStore). // value: Значение (строка/список). diff --git a/src/core/tests/Deal.Tests.Unit/Support/LocalColumnSuggesterTests.cs b/src/core/tests/Deal.Tests.Unit/Support/LocalColumnSuggesterTests.cs index 0bae88d..480b64a 100644 --- a/src/core/tests/Deal.Tests.Unit/Support/LocalColumnSuggesterTests.cs +++ b/src/core/tests/Deal.Tests.Unit/Support/LocalColumnSuggesterTests.cs @@ -6,7 +6,6 @@ using Deal.Modules.Kanban.Application.Models; using Deal.Modules.Kanban.Application.Services; using Deal.Modules.Settings.Application.Models; using Deal.Tests.Unit.Support; -using Deal.Tests.Unit.Modules.Cards; using Deal.Tests.Unit.Modules.Settings; namespace Deal.Tests.Unit.Support; @@ -18,18 +17,18 @@ public sealed class LocalColumnSuggesterTests { // Создаёт контекст теста: in-memory хранилища + адаптер поверх сервиса досок модуля. // Возвращает: Кортеж (хранилище канбана, KV-настройки, адаптер). - private static (FakeKanjStore Store, TestSettingsStore Settings, LocalColumnSuggester Suggester) CreateContext() + private static (TestKanjStore Store, TestSettingsStore Settings, LocalColumnSuggester Suggester) CreateContext() { - var store = new FakeKanjStore(); + var store = new TestKanjStore(); var settings = new TestSettingsStore(); - var suggester = new LocalColumnSuggester(store, settings.Store, new ContainersService(store, settings.Store)); + var suggester = new LocalColumnSuggester(store.Store, settings.Store, new ContainersService(store.Store, settings.Store)); return (store, settings, suggester); } private static void SeedInbox( string id, string text, - FakeKanjStore store, + TestKanjStore store, long receivedAtMs = 0) => store.SeedCard(new CardDto { @@ -58,7 +57,7 @@ public sealed class LocalColumnSuggesterTests [Fact] public async Task SuggestColumns_TooFewCards_ReturnsTooFewCardsReason() { - (FakeKanjStore store, _, LocalColumnSuggester suggester) = CreateContext(); + (TestKanjStore store, _, LocalColumnSuggester suggester) = CreateContext(); for (int i = 1; i < SuggestHeuristics.MinInbox; i++) { SeedInbox($"l_{i}", "нужен python", store); @@ -73,7 +72,7 @@ public sealed class LocalColumnSuggesterTests [Fact] public async Task SuggestColumns_CooldownActive_ReturnsCooldownReason() { - (FakeKanjStore store, TestSettingsStore settings, LocalColumnSuggester suggester) = CreateContext(); + (TestKanjStore store, TestSettingsStore settings, LocalColumnSuggester suggester) = CreateContext(); settings.Preload(SettingsKeys.LastSuggestAt, DateTimeOffset.UtcNow.ToUnixTimeSeconds().ToString(CultureInfo.InvariantCulture)); SeedInbox("l_1", "нужен python", store); @@ -88,7 +87,7 @@ public sealed class LocalColumnSuggesterTests [Fact] public async Task SuggestColumns_NoThemes_ReturnsNothingGroupedReason() { - (FakeKanjStore store, _, LocalColumnSuggester suggester) = CreateContext(); + (TestKanjStore store, _, LocalColumnSuggester suggester) = CreateContext(); string[] uniqueWords = ["python", "vue", "crm", "sql", "php", "mvp"]; for (int i = 0; i < uniqueWords.Length; i++) { @@ -104,7 +103,7 @@ public sealed class LocalColumnSuggesterTests [Fact] public async Task SuggestColumns_SimilarExistingBoard_ReturnsNothingGroupedReason() { - (FakeKanjStore store, _, LocalColumnSuggester suggester) = CreateContext(); + (TestKanjStore store, _, LocalColumnSuggester suggester) = CreateContext(); store.SeedBoard(new ContainerDto { Id = "b_python", @@ -129,7 +128,7 @@ public sealed class LocalColumnSuggesterTests [Fact] public async Task SuggestColumns_Success_CreatesSuggestedBoardsAndPlacesCards() { - (FakeKanjStore store, TestSettingsStore settings, LocalColumnSuggester suggester) = CreateContext(); + (TestKanjStore store, TestSettingsStore settings, LocalColumnSuggester suggester) = CreateContext(); SeedInbox("l_p1", "нужен python", store); SeedInbox("l_p2", "нужен python", store); SeedInbox("l_p3", "нужен python", store); @@ -170,7 +169,7 @@ public sealed class LocalColumnSuggesterTests [Fact] public async Task SuggestColumns_SuccessThenImmediateRepeat_BlockedByCooldown() { - (FakeKanjStore store, _, LocalColumnSuggester suggester) = CreateContext(); + (TestKanjStore store, _, LocalColumnSuggester suggester) = CreateContext(); SeedInbox("l_p1", "нужен python", store); SeedInbox("l_p2", "нужен python", store); SeedInbox("l_p3", "нужен python", store); @@ -192,7 +191,7 @@ public sealed class LocalColumnSuggesterTests [Fact] public async Task SuggestKeywords_Success_ReturnsRepeatedWordMarkers() { - (FakeKanjStore store, _, LocalColumnSuggester suggester) = CreateContext(); + (TestKanjStore store, _, LocalColumnSuggester suggester) = CreateContext(); SeedInbox("l_p1", "нужен python", store); SeedInbox("l_p2", "нужен python", store); SeedInbox("l_p3", "нужен python", store); @@ -210,7 +209,7 @@ public sealed class LocalColumnSuggesterTests [Fact] public async Task SuggestKeywords_TooFewCards_ReturnsReason() { - (FakeKanjStore store, _, LocalColumnSuggester suggester) = CreateContext(); + (TestKanjStore store, _, LocalColumnSuggester suggester) = CreateContext(); SeedInbox("l_1", "нужен python", store); SeedInbox("l_2", "нужен python", store); @@ -224,7 +223,7 @@ public sealed class LocalColumnSuggesterTests [Fact] public async Task SuggestKeywords_TrashAndArchiveCards_AreNotCounted() { - (FakeKanjStore store, _, LocalColumnSuggester suggester) = CreateContext(); + (TestKanjStore store, _, LocalColumnSuggester suggester) = CreateContext(); SeedInbox("l_1", "нужен python", store); SeedInbox("l_2", "нужен vue", store); store.SeedCard(new CardDto { Id = "l_t1", Col = KanbanColumns.Trash, Content = new SourceContent { Text = "ищу такси" } }); @@ -240,7 +239,7 @@ public sealed class LocalColumnSuggesterTests [Fact] public async Task SuggestKeywords_NoRepeatedMarkers_ReturnsReason() { - (FakeKanjStore store, _, LocalColumnSuggester suggester) = CreateContext(); + (TestKanjStore store, _, LocalColumnSuggester suggester) = CreateContext(); string[] uniqueWords = ["python", "vue", "crm", "sql", "php", "mvp"]; for (int i = 0; i < uniqueWords.Length; i++) { diff --git a/src/core/tests/Deal.Tests.Unit/Support/PipelineWorkerSchedulerTests.cs b/src/core/tests/Deal.Tests.Unit/Support/PipelineWorkerSchedulerTests.cs index 2ad302e..9bd8504 100644 --- a/src/core/tests/Deal.Tests.Unit/Support/PipelineWorkerSchedulerTests.cs +++ b/src/core/tests/Deal.Tests.Unit/Support/PipelineWorkerSchedulerTests.cs @@ -18,7 +18,6 @@ using Deal.Modules.Tenants.Application.Models; using Deal.SharedKernel.Tenants.Abstractions; using Deal.SharedKernel.Tenants.Models; using Deal.Tests.Unit.Contracts; -using Deal.Tests.Unit.Modules.Cards; using Deal.Tests.Unit.Modules.Kanban; using Deal.Tests.Unit.Modules.Settings; using Deal.Tests.Unit.Modules.Tenants; @@ -42,10 +41,10 @@ public sealed class PipelineWorkerSchedulerTests private sealed record Context( PipelineWorkerScheduler Scheduler, TestPipelineStore PipelineA, - FakeKanjStore KanjA, + TestKanjStore KanjA, SseSubscription SubscriptionA, TestPipelineStore PipelineB, - FakeKanjStore KanjB, + TestKanjStore KanjB, SseSubscription SubscriptionB, PipelinePumpGate PumpGate, TenantContext TenantContext, @@ -153,8 +152,8 @@ public sealed class PipelineWorkerSchedulerTests var tenantContext = new TenantContext(); var pipelineA = new TestPipelineStore(throwOnList: withThrowingQueueReadA); var pipelineB = new TestPipelineStore(); - var kanjA = new FakeKanjStore(); - var kanjB = new FakeKanjStore(); + var kanjA = new TestKanjStore(); + var kanjB = new TestKanjStore(); var settingsA = new TestSettingsStore(); var settingsB = new TestSettingsStore(); var pumpGate = new PipelinePumpGate(); @@ -172,7 +171,7 @@ public sealed class PipelineWorkerSchedulerTests services.AddSingleton(aiClassifier.Classifier); // Тенант-scoped адаптеры: фейк выбирает хранилище по ITenantContext, который цикл заполняет SetTenant // (эталон StorageTickSchedulerTests/ConnectionStringProvider.ForTenant). - services.AddScoped(provider => TenantOf(provider) == TenantA ? kanjA : kanjB); + services.AddScoped(provider => TenantOf(provider) == TenantA ? kanjA.Store : kanjB.Store); services.AddScoped(provider => TenantOf(provider) == TenantA ? settingsA.Store : settingsB.Store); services.AddScoped(provider => TenantOf(provider) == TenantA ? pipelineA.Store : pipelineB.Store); // Реальные сервисы модуля Pipeline — как AddPipelineModule в Program.cs: цикл резолвит их в tenant-scope. diff --git a/src/core/tests/Deal.Tests.Unit/Support/PipelineWorkerServiceTests.cs b/src/core/tests/Deal.Tests.Unit/Support/PipelineWorkerServiceTests.cs index 475b7da..019c229 100644 --- a/src/core/tests/Deal.Tests.Unit/Support/PipelineWorkerServiceTests.cs +++ b/src/core/tests/Deal.Tests.Unit/Support/PipelineWorkerServiceTests.cs @@ -7,7 +7,6 @@ using Deal.Modules.Pipeline.Application.Parse; using Deal.Modules.Pipeline.Application.Services; using Deal.Modules.Settings.Application.Models; using Deal.Tests.Unit.Contracts; -using Deal.Tests.Unit.Modules.Cards; using Deal.Tests.Unit.Modules.Kanban; using Deal.Tests.Unit.Modules.Settings; @@ -26,7 +25,7 @@ public sealed class PipelineWorkerServiceTests private sealed record Context( PipelineWorkerService Worker, TestPipelineStore PipelineStore, - FakeKanjStore KanjStore, + TestKanjStore KanjStore, TestSettingsStore Settings, TestMlClient MlClient, TestAiClassifier AiClassifier); @@ -38,16 +37,16 @@ public sealed class PipelineWorkerServiceTests { var settings = new TestSettingsStore(); var store = pipelineStore ?? new TestPipelineStore(); - var kanjStore = new FakeKanjStore(); + var kanjStore = new TestKanjStore(); var mlClient = new TestMlClient { Predict = NotReadyPrediction() }; var aiClassifier = new TestAiClassifier(); var rules = new IncomingRules(settings.Store); var fieldsParser = new LocalFieldsParser(settings.Store); var processing = new PipelineProcessingService(store.Store, mlClient.Client, new PipelineIngestService(store.Store)); - var composer = new CardComposer(kanjStore, settings.Store); - var writer = new PipelineCardWriter(kanjStore, store.Store, composer); + var composer = new CardComposer(kanjStore.Store, settings.Store); + var writer = new PipelineCardWriter(kanjStore.Store, store.Store, composer); var worker = new PipelineWorkerService( - store.Store, settings.Store, rules, kanjStore, mlClient.Client, aiClassifier.Classifier, processing, writer, fieldsParser); + store.Store, settings.Store, rules, kanjStore.Store, mlClient.Client, aiClassifier.Classifier, processing, writer, fieldsParser); return new Context(worker, store, kanjStore, settings, mlClient, aiClassifier); } diff --git a/src/core/tests/Deal.Tests.Unit/Support/StorageTickSchedulerTests.cs b/src/core/tests/Deal.Tests.Unit/Support/StorageTickSchedulerTests.cs index 81d900c..e694a76 100644 --- a/src/core/tests/Deal.Tests.Unit/Support/StorageTickSchedulerTests.cs +++ b/src/core/tests/Deal.Tests.Unit/Support/StorageTickSchedulerTests.cs @@ -17,7 +17,6 @@ using Deal.Modules.Tenants.Application.Models; using Deal.SharedKernel.Tenants.Abstractions; using Deal.SharedKernel.Tenants.Models; using Deal.Tests.Unit.Contracts; -using Deal.Tests.Unit.Modules.Cards; using Deal.Tests.Unit.Modules.Kanban; using Deal.Tests.Unit.Modules.Settings; using Deal.Tests.Unit.Modules.Tenants; @@ -43,8 +42,8 @@ public sealed class StorageTickSchedulerTests [Fact] public async Task RunCycle_TicksEveryTenantInOwnScopeAndPublishesToastToEachTenantChannel() { - FakeKanjStore storeA = StoreWithExpiredInboxCard("l_a_old"); - FakeKanjStore storeB = StoreWithExpiredInboxCard("l_b_old"); + TestKanjStore storeA = StoreWithExpiredInboxCard("l_a_old"); + TestKanjStore storeB = StoreWithExpiredInboxCard("l_b_old"); var settings = new TestSettingsStore(); var tenants = new TestTenantRepository(Tenant(TenantA), Tenant(TenantB)); var tenantContext = new TenantContext(); @@ -71,8 +70,8 @@ public sealed class StorageTickSchedulerTests [Fact] public async Task RunCycle_ZeroCountersTenant_PublishesToastOnlyToTenantWithChanges() { - FakeKanjStore storeA = StoreWithExpiredInboxCard("l_a_old"); - var storeB = new FakeKanjStore(); + TestKanjStore storeA = StoreWithExpiredInboxCard("l_a_old"); + var storeB = new TestKanjStore(); storeB.SeedCard(Card("l_b_fresh", KanbanColumns.Inbox, ReceivedAtMsAgo(TimeSpan.FromHours(1)))); var settings = new TestSettingsStore(); var tenantContext = new TenantContext(); @@ -100,16 +99,16 @@ public sealed class StorageTickSchedulerTests public async Task RunCycle_TenantTickFailure_DoesNotAbortOtherTenants() { // У тенанта A настройки падают (имитация сбоя схемы/БД) — тик A логирует ошибку, B обрабатывается. - FakeKanjStore storeB = StoreWithExpiredInboxCard("l_b_old"); + TestKanjStore storeB = StoreWithExpiredInboxCard("l_b_old"); var tenantContext = new TenantContext(); var settingsByTenant = new Dictionary { [TenantA] = new ThrowingSettingsStore(), [TenantB] = new TestSettingsStore().Store, }; - var storesByTenant = new Dictionary + var storesByTenant = new Dictionary { - [TenantA] = new FakeKanjStore(), + [TenantA] = new TestKanjStore(), [TenantB] = storeB, }; await using ServiceProvider provider = BuildProvider( @@ -138,7 +137,7 @@ public sealed class StorageTickSchedulerTests await using ServiceProvider provider = BuildProvider( new ThrowingTenantRepository(), tenantContext, - new Dictionary(), + new Dictionary(), new Dictionary()); StorageTickScheduler scheduler = CreateScheduler(provider); @@ -151,7 +150,7 @@ public sealed class StorageTickSchedulerTests [Fact] public async Task RunCycle_PurgesExpiredRejectedRowsAndPublishesRejectedPurgeToast() { - var kanjStore = new FakeKanjStore(); + var kanjStore = new TestKanjStore(); var settings = new TestSettingsStore(); var tenantContext = new TenantContext(); var pipelineStoreA = new TestPipelineStore(); @@ -161,7 +160,7 @@ public sealed class StorageTickSchedulerTests await using ServiceProvider provider = BuildProvider( new TestTenantRepository(Tenant(TenantA), Tenant(TenantB)).Repository, tenantContext, - new Dictionary { [TenantA] = kanjStore, [TenantB] = new FakeKanjStore() }, + new Dictionary { [TenantA] = kanjStore, [TenantB] = new TestKanjStore() }, new Dictionary { [TenantA] = settings.Store, [TenantB] = settings.Store }, new Dictionary { [TenantA] = pipelineStoreA, [TenantB] = new TestPipelineStore() }); @@ -182,16 +181,16 @@ public sealed class StorageTickSchedulerTests [Fact] public async Task RunCycle_DueReminder_PublishesReminderDueToTenantChannelAndMarksFired() { - var cardStoreA = new FakeKanjStore(); + var cardStoreA = new TestKanjStore(); cardStoreA.SeedCard(HoldCard("c_a_past", title: "Отложенный бот", reminderAtMs: NowMs() - 60_000)); cardStoreA.SeedCard(HoldCard("c_a_future", title: "Будущий", reminderAtMs: NowMs() + 60_000)); cardStoreA.SeedCard(HoldCard("c_a_work", stage: "work", title: "В работе", reminderAtMs: NowMs() - 60_000)); - var cardStoreB = new FakeKanjStore(); + var cardStoreB = new TestKanjStore(); var tenantContext = new TenantContext(); await using ServiceProvider provider = BuildProvider( new TestTenantRepository(Tenant(TenantA), Tenant(TenantB)).Repository, tenantContext, - new Dictionary { [TenantA] = cardStoreA, [TenantB] = cardStoreB }, + new Dictionary { [TenantA] = cardStoreA, [TenantB] = cardStoreB }, new Dictionary { [TenantA] = new TestSettingsStore().Store, [TenantB] = new TestSettingsStore().Store }, new Dictionary { [TenantA] = new(), [TenantB] = new() }); @@ -211,14 +210,14 @@ public sealed class StorageTickSchedulerTests Assert.False(subscriptionB.Events.TryRead(out _)); // «Выстрелившее» помечено fired: повторная выборка due пуста (признак держит строка БД). - Assert.Empty(await cardStoreA.ListDueRemindersAsync(DateTimeOffset.UtcNow, CancellationToken.None)); + Assert.Empty(await cardStoreA.Store.ListDueRemindersAsync(DateTimeOffset.UtcNow, CancellationToken.None)); Assert.False(tenantContext.HasTenant); } [Fact] public async Task RunCycle_RemindersDisabled_ClearsExpiredAndPublishesNoReminderDue() { - var cardStoreA = new FakeKanjStore(); + var cardStoreA = new TestKanjStore(); cardStoreA.SeedCard(HoldCard("c_past", title: "Старое", reminderAtMs: NowMs() - 60_000)); var settingsA = new TestSettingsStore(); settingsA.Preload(SettingsKeys.RemindersEnabled, "false"); @@ -226,7 +225,7 @@ public sealed class StorageTickSchedulerTests await using ServiceProvider provider = BuildProvider( new TestTenantRepository(Tenant(TenantA)).Repository, tenantContext, - new Dictionary { [TenantA] = cardStoreA }, + new Dictionary { [TenantA] = cardStoreA }, new Dictionary { [TenantA] = settingsA.Store }, new Dictionary { [TenantA] = new() }); @@ -246,13 +245,13 @@ public sealed class StorageTickSchedulerTests { // У тенанта A проверка напоминаний падает (имитация сбоя схемы/БД на ListDueAsync) — ветка логируется // и тик A завершается, тенант B обрабатывается (автоархив + тост) — проход жив. - var cardStoreA = new ThrowingDueKanjStore(); - FakeKanjStore storeB = StoreWithExpiredInboxCard("l_b_old"); + var cardStoreA = new TestKanjStore(throwOnDueReminders: true); + TestKanjStore storeB = StoreWithExpiredInboxCard("l_b_old"); var tenantContext = new TenantContext(); await using ServiceProvider provider = BuildProvider( new TestTenantRepository(Tenant(TenantA), Tenant(TenantB)).Repository, tenantContext, - new Dictionary { [TenantA] = cardStoreA, [TenantB] = storeB }, + new Dictionary { [TenantA] = cardStoreA, [TenantB] = storeB }, new Dictionary { [TenantA] = new TestSettingsStore().Store, [TenantB] = new TestSettingsStore().Store }, new Dictionary { [TenantA] = new(), [TenantB] = new() }); @@ -282,14 +281,14 @@ public sealed class StorageTickSchedulerTests private static ServiceProvider BuildProvider( TestTenantRepository tenants, TenantContext tenantContext, - FakeKanjStore storeA, - FakeKanjStore storeB, + TestKanjStore storeA, + TestKanjStore storeB, TestSettingsStore settings) { return BuildProvider( tenants.Repository, tenantContext, - new Dictionary { [TenantA] = storeA, [TenantB] = storeB }, + new Dictionary { [TenantA] = storeA, [TenantB] = storeB }, new Dictionary { [TenantA] = settings.Store, [TenantB] = settings.Store }); } @@ -303,7 +302,7 @@ public sealed class StorageTickSchedulerTests private static ServiceProvider BuildProvider( ITenantRepository tenants, TenantContext tenantContext, - Dictionary storesByTenant, + Dictionary storesByTenant, Dictionary settingsByTenant, Dictionary? pipelineStoresByTenant = null) { @@ -320,7 +319,7 @@ public sealed class StorageTickSchedulerTests services.AddSingleton(tenants); // Тенант-scoped адаптеры: реальные строят TenantDbContext по схеме текущего тенанта — фейк // выбирает хранилище по тому же ITenantContext, который планировщик заполняет SetTenant. - services.AddScoped(provider => storesByTenant[TenantOf(provider)]); + services.AddScoped(provider => storesByTenant[TenantOf(provider)].Store); services.AddScoped(provider => settingsByTenant[TenantOf(provider)]); services.AddScoped(provider => pipelineStoresByTenant[TenantOf(provider)].Store); services.AddSingleton(new TestMlClient().Client); @@ -360,17 +359,7 @@ public sealed class StorageTickSchedulerTests private static TenantRecordDto Tenant(Guid id) => new(id, Name: "tenant", Status: "active", CreatedAt: DateTimeOffset.UtcNow); - // Хранилище карточек со сбоем выборки due-напоминаний: ListDueRemindersAsync бросает (сценарий - // «БД/схема недоступны» на проверке напоминаний — ветка логируется, тик тенанта/проход живы, как в - // AdminTickOrchestratorTests). - private sealed class ThrowingDueKanjStore : FakeKanjStore - { - /// - public override Task> ListDueRemindersAsync(DateTimeOffset now, CancellationToken ct) - { - throw new InvalidOperationException("Тестовый сбой выборки due-напоминаний (ListDueRemindersAsync)."); - } - } + // Сбой выборки due-напоминаний задаётся флагом throwOnDueReminders в TestKanjStore. // Текущее время в epoch-мс (UTC) — для посева напоминаний в прошлом/будущем. private static long NowMs() => DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(); @@ -395,9 +384,9 @@ public sealed class StorageTickSchedulerTests // Хранилище с просроченной карточкой «Неразобранного» — кандидатом автоархива дефолтного тика. // cardId: Id карточки. // Возвращает: Фейк-хранилище с одной старой карточкой inbox. - private static FakeKanjStore StoreWithExpiredInboxCard(string cardId) + private static TestKanjStore StoreWithExpiredInboxCard(string cardId) { - var store = new FakeKanjStore(); + var store = new TestKanjStore(); store.SeedCard(Card(cardId, KanbanColumns.Inbox, ReceivedAtMsAgo(ExpiredAge))); return store; } diff --git a/src/core/tests/Deal.Tests.Unit/Modules/Cards/FakeKanjStore.cs b/src/core/tests/Deal.Tests.Unit/Support/TestKanjStore.cs similarity index 54% rename from src/core/tests/Deal.Tests.Unit/Modules/Cards/FakeKanjStore.cs rename to src/core/tests/Deal.Tests.Unit/Support/TestKanjStore.cs index 0912d7e..4673b1d 100644 --- a/src/core/tests/Deal.Tests.Unit/Modules/Cards/FakeKanjStore.cs +++ b/src/core/tests/Deal.Tests.Unit/Support/TestKanjStore.cs @@ -2,120 +2,262 @@ using Deal.Modules.Cards.Application.Models; using Deal.Modules.Cards.Application.Sources; using Deal.Modules.Kanban.Application.Abstractions; using Deal.Modules.Kanban.Application.Models; +using NSubstitute; -namespace Deal.Tests.Unit.Modules.Cards; +namespace Deal.Tests.Unit.Support; /// -/// In-memory реализация для unit-тестов сервисов единого домена карточки. +/// Подставка на списках: сервисы получают NSubstitute-подставку +/// (), тесты сеют/проверяют состояние через , +/// , , , , +/// , . Сбой записи задаётся . /// -public class FakeKanjStore : ICardStore +public sealed class TestKanjStore { private readonly List _boards = []; private readonly List _cards = []; private readonly List _moves = []; private readonly List<(string Query, int Limit)> _searchCalls = []; - private readonly HashSet _firedById = new(StringComparer.Ordinal); - private readonly Dictionary _archivedAtById = new(StringComparer.Ordinal); + private readonly bool _throwOnDueReminders; /// - /// Доски фейка (копия на момент обращения) — проверки BoardsServiceTests и LocalColumnSuggesterTests. + /// Подставка порта единого домена карточки (создаётся в конструкторе). + /// + public ICardStore Store { get; } + + /// + /// Доски хранилища (копия на момент обращения). /// public IReadOnlyList Boards => _boards.ToList(); /// - /// Карточки фейка как тройки + /// Карточки хранилища как тройки (id, колонка, флаг «новая»). /// public IReadOnlyList<(string CardId, string Col, bool IsNew)> Cards => _cards.Select(card => (card.Id, card.Col, card.IsNew)).ToList(); /// - /// Полные карточки фейка + /// Полные карточки хранилища (копия на момент обращения). /// public IReadOnlyList CardDtos => _cards.ToList(); /// - /// Флаг «сбой записи карточки» + /// Флаг «сбой записи карточки». /// public bool FailAddCard { get; set; } /// - /// Записи журнала CardMoves + /// Записи журнала CardMoves. /// public IReadOnlyList Moves => _moves.ToList(); /// - /// Вызовы SearchCardsAsync как пары + /// Вызовы SearchCardsAsync как пары (запрос, лимит). /// public IReadOnlyList<(string Query, int Limit)> SearchCalls => _searchCalls.ToList(); + /// + /// Создаёт подставку с пустыми списками. + /// + /// Сценарий сбоя выборки due-напоминаний: ListDueRemindersAsync бросает. + public TestKanjStore(bool throwOnDueReminders = false) + { + _throwOnDueReminders = throwOnDueReminders; + Store = Substitute.For(); + ConfigureContainers(); + ConfigureCards(); + ConfigureComments(); + ConfigureMoves(); + ConfigureStorage(); + ConfigureSelected(); + ConfigureReminders(); + } + /// /// Кладёт доску напрямую /// /// Доска как если бы была сохранена в БД. - public void SeedBoard(ContainerDto board) - { - _boards.Add(board); - } + public void SeedBoard(ContainerDto board) => _boards.Add(board); /// /// Кладёт карточку в колонку. /// /// Id карточки. /// Колонка (inbox/доска). - public void AddCard(string cardId, string col) - { - SeedCard(new CardDto { Id = cardId, Col = col }); - } + public void AddCard(string cardId, string col) => SeedCard(new CardDto { Id = cardId, Col = col }); /// /// Кладёт полную карточку. /// /// Карточка как если бы была сохранена в БД (комментарии — приложенным массивом). - public void SeedCard(CardDto card) + public void SeedCard(CardDto card) => _cards.Add(card); + + /// + /// Задаёт метку архивации карточки + /// + /// Id карточки. + /// Метка архивации (когда карточка ушла в архив). + public void SetArchivedAt(string cardId, DateTimeOffset archivedAt) => _archivedAtById[cardId] = archivedAt; + + /// + /// Метка архивации карточки — проверка archived_at после автоархива тика. + /// + /// Id карточки. + /// Метка архивации либо null — карточки нет/не архивирована/метка сброшена. + public DateTimeOffset? ArchivedAtOf(string cardId) => + _archivedAtById.TryGetValue(cardId, out DateTimeOffset archivedAt) ? archivedAt : null; + + private void ConfigureContainers() { - _cards.Add(card); + Store.ListContainersAsync(Arg.Any(), Arg.Any()) + .Returns(ci => ListContainers(ci.ArgAt(0))); + Store.GetContainerAsync(Arg.Any(), Arg.Any()) + .Returns(ci => _boards.FirstOrDefault(board => board.Id == ci.ArgAt(0))); + Store.When(s => s.CreateContainerAsync(Arg.Any(), Arg.Any())) + .Do(ci => _boards.Add(ci.Arg())); + Store.When(s => s.UpdateContainerAsync(Arg.Any(), Arg.Any())) + .Do(ci => UpdateContainer(ci.Arg())); + Store.DeleteContainerAsync(Arg.Any(), Arg.Any()) + .Returns(ci => DeleteContainer(ci.ArgAt(0))); + Store.When(s => s.ReorderContainersAsync( + Arg.Any(), Arg.Any>(), Arg.Any())) + .Do(ci => ReorderContainers(ci.ArgAt(0), ci.ArgAt>(1))); } - // ── Контейнеры (колонки/стадии/зоны) ───────────────────────────────── - - /// - public Task> ListContainersAsync(string? space, CancellationToken ct) + private void ConfigureCards() { - IReadOnlyList ordered = _boards + Store.ListCardsAsync(Arg.Any(), Arg.Any()) + .Returns(ci => ListCards(ci.Arg())); + Store.SearchCardsAsync(Arg.Any(), Arg.Any(), Arg.Any()) + .Returns(ci => SearchCards(ci.ArgAt(0), ci.ArgAt(1))); + Store.GetCardAsync(Arg.Any(), Arg.Any()) + .Returns(ci => _cards.FirstOrDefault(card => card.Id == ci.ArgAt(0))); + Store.GetCardBySourceAsync(Arg.Any(), Arg.Any()) + .Returns(ci => GetCardBySource(ci.Arg())); + Store.When(s => s.AddCardAsync(Arg.Any(), Arg.Any())) + .Do(ci => AddCard(ci.Arg())); + Store.When(s => s.UpdateColumnAsync(Arg.Any(), Arg.Any())) + .Do(ci => UpdateColumn(ci.Arg())); + Store.ApplyReclassificationAsync(Arg.Any(), Arg.Any()) + .Returns(ci => ApplyReclassification(ci.Arg())); + Store.When(s => s.UpdateSeenAsync(Arg.Any(), Arg.Any(), Arg.Any())) + .Do(ci => UpdateSeen(ci.ArgAt(0), ci.ArgAt(1))); + Store.When(s => s.DeleteForeverAsync(Arg.Any(), Arg.Any())) + .Do(ci => DeleteForever(ci.ArgAt(0))); + Store.ClearColAsync(Arg.Any(), Arg.Any()) + .Returns(ci => ClearCol(ci.ArgAt(0))); + Store.CountCardsByColAsync(Arg.Any()) + .Returns(_ => CountCardsByCol()); + } + + private void ConfigureComments() + { + Store.ListCommentsAsync(Arg.Any(), Arg.Any()) + .Returns(ci => _cards.FirstOrDefault(card => card.Id == ci.ArgAt(0))?.Comments + ?? Array.Empty()); + Store.When(s => s.AddCommentAsync( + Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any())) + .Do(ci => AddComment( + ci.ArgAt(0), ci.ArgAt(1), ci.ArgAt(2), ci.ArgAt(3))); + } + + private void ConfigureMoves() + { + Store.When(s => s.AddMoveAsync(Arg.Any(), Arg.Any())) + .Do(ci => _moves.Add(ci.Arg())); + Store.CountMovesAsync(Arg.Any()).Returns(_ => _moves.Count); + Store.GetAiMarkupExamplesAsync(Arg.Any(), Arg.Any()) + .Returns(ci => GetAiMarkupExamples(ci.ArgAt(0))); + } + + private void ConfigureStorage() + { + Store.ListArchiveCandidatesAsync(Arg.Any(), Arg.Any()) + .Returns(ci => ListArchiveCandidates(ci.ArgAt(0))); + Store.ArchiveAsync(Arg.Any>(), Arg.Any(), Arg.Any()) + .Returns(ci => Archive(ci.ArgAt>(0), ci.ArgAt(1))); + Store.ListExpiredArchiveCandidatesAsync(Arg.Any(), Arg.Any()) + .Returns(ci => ListExpiredArchiveCandidates(ci.ArgAt(0))); + Store.ListTrashCandidatesAsync(Arg.Any(), Arg.Any()) + .Returns(ci => ListTrashCandidates(ci.ArgAt(0))); + Store.PurgeAsync(Arg.Any>(), Arg.Any()) + .Returns(ci => Purge(ci.ArgAt>(0))); + Store.ListCardsForConversionAsync(Arg.Any()) + .Returns(_ => ListCardsForConversion()); + Store.When(s => s.UpdateConversionAsync( + Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any())) + .Do(ci => UpdateConversion( + ci.ArgAt(0), ci.ArgAt(1), ci.ArgAt(2), ci.ArgAt(3))); + } + + private void ConfigureSelected() + { + Store.ListSelectedCardsAsync(Arg.Any(), Arg.Any()) + .Returns(ci => ListSelectedCards(ci.ArgAt(0))); + Store.PatchCardAsync(Arg.Any(), Arg.Any(), Arg.Any()) + .Returns(ci => PatchCard(ci.ArgAt(0), ci.Arg())); + Store.AddLinkAsync(Arg.Any(), Arg.Any(), Arg.Any()) + .Returns(ci => AddLink(ci.ArgAt(0), ci.Arg())); + Store.RemoveLinkAsync(Arg.Any(), Arg.Any(), Arg.Any()) + .Returns(ci => RemoveLink(ci.ArgAt(0), ci.ArgAt(1))); + Store.AddFileAsync(Arg.Any(), Arg.Any(), Arg.Any()) + .Returns(ci => AddFile(ci.ArgAt(0), ci.Arg())); + Store.RemoveFileAsync(Arg.Any(), Arg.Any(), Arg.Any()) + .Returns(ci => RemoveFile(ci.ArgAt(0), ci.ArgAt(1))); + Store.MoveCardStageAsync( + Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any()) + .Returns(ci => MoveCardStage( + ci.ArgAt(0), ci.ArgAt(1), ci.Arg(), ci.ArgAt(3))); + } + + private void ConfigureReminders() + { + Store.When(s => s.SetReminderAsync(Arg.Any(), Arg.Any(), Arg.Any())) + .Do(ci => SetReminder(ci.ArgAt(0), ci.ArgAt(1))); + Store.When(s => s.ClearReminderAsync(Arg.Any(), Arg.Any())) + .Do(ci => ClearReminder(ci.ArgAt(0))); + Store.ClearStageAsync(Arg.Any(), Arg.Any()) + .Returns(ci => ClearStage(ci.ArgAt(0))); + if (_throwOnDueReminders) + { + Store.ListDueRemindersAsync(Arg.Any(), Arg.Any()) + .Returns>>(_ => + throw new InvalidOperationException("Тестовый сбой выборки due-напоминаний (ListDueRemindersAsync).")); + } + else + { + Store.ListDueRemindersAsync(Arg.Any(), Arg.Any()) + .Returns(ci => ListDueReminders(ci.ArgAt(0))); + } + + Store.When(s => s.MarkRemindersFiredAsync(Arg.Any>(), Arg.Any())) + .Do(ci => MarkRemindersFired(ci.ArgAt>(0))); + Store.ClearExpiredRemindersAsync(Arg.Any(), Arg.Any()) + .Returns(ci => ClearExpiredReminders(ci.ArgAt(0))); + Store.ListInboxWithSourceAsync(Arg.Any()) + .Returns(_ => ListInboxWithSource()); + } + + private IReadOnlyList ListContainers(string? space) + { + return _boards .Where(board => space is null || board.Space == space) .OrderBy(board => board.Suggested) .ThenBy(board => board.Order) .ToList(); - return Task.FromResult(ordered); } - /// - public Task GetContainerAsync(string containerId, CancellationToken ct) - { - return Task.FromResult(_boards.FirstOrDefault(board => board.Id == containerId)); - } - - /// - public Task CreateContainerAsync(ContainerDto container, CancellationToken ct) - { - _boards.Add(container); - return Task.CompletedTask; - } - - /// - public Task UpdateContainerAsync(ContainerDto container, CancellationToken ct) + private void UpdateContainer(ContainerDto container) { int index = _boards.FindIndex(item => item.Id == container.Id); _boards[index] = container; - return Task.CompletedTask; } - /// - public Task DeleteContainerAsync(string containerId, CancellationToken ct) + private int DeleteContainer(string containerId) { - List moved = _cards.Where(card => card.Col == containerId).ToList(); + int moved = _cards.Count(card => card.Col == containerId); for (int i = 0; i < _cards.Count; i++) { if (_cards[i].Col == containerId) @@ -125,54 +267,39 @@ public class FakeKanjStore : ICardStore } _boards.RemoveAll(board => board.Id == containerId); - return Task.FromResult(moved.Count); + return moved; } - /// - public Task ReorderContainersAsync( - string space, - IReadOnlyList containerIds, - CancellationToken ct) + private void ReorderContainers(string space, IReadOnlyList containerIds) { for (int i = 0; i < containerIds.Count; i++) { - string containerId = containerIds[i]; - int index = _boards.FindIndex(board => board.Id == containerId && board.Space == space); + int index = _boards.FindIndex(board => board.Id == containerIds[i] && board.Space == space); if (index >= 0) { _boards[index] = _boards[index] with { Order = i }; } } - - return Task.CompletedTask; } - // ── Карточки ───────────────────────────────────────────────────────── - - /// - public Task> ListCardsAsync(CardsQuery query, CancellationToken ct) + private IReadOnlyList ListCards(CardsQuery query) { IEnumerable result = query.Col is null ? _cards : _cards.Where(card => card.Col == query.Col); - return Task.FromResult>( - result.OrderByDescending(card => card.ReceivedAtMs).ToList()); + return result.OrderByDescending(card => card.ReceivedAtMs).ToList(); } - /// - public Task> SearchCardsAsync( - string q, - int limit, - CancellationToken ct) + private IReadOnlyList SearchCards(string q, int limit) { _searchCalls.Add((q, limit)); string lowered = q.Trim().ToLowerInvariant(); if (lowered.Length == 0) { - return Task.FromResult>(Array.Empty()); + return Array.Empty(); } - IReadOnlyList result = _cards + return _cards .Where(card => card.Title.ToLowerInvariant().Contains(lowered, StringComparison.Ordinal) || card.Summary.ToLowerInvariant().Contains(lowered, StringComparison.Ordinal) || card.Contact.ToLowerInvariant().Contains(lowered, StringComparison.Ordinal) @@ -180,38 +307,26 @@ public class FakeKanjStore : ICardStore .OrderByDescending(card => card.ReceivedAtMs) .Take(limit) .ToList(); - return Task.FromResult>(result); } - /// - public Task GetCardAsync(string cardId, CancellationToken ct) - { - return Task.FromResult(_cards.FirstOrDefault(card => card.Id == cardId)); - } - - /// - public Task GetCardBySourceAsync( - SourceRef source, - CancellationToken ct) + private CardDto? GetCardBySource(SourceRef source) { if (string.IsNullOrEmpty(source.Kind)) { - return Task.FromResult(null); + return null; } string externalId = source.ExternalId ?? string.Empty; string originRef = source.OriginRef ?? string.Empty; - CardDto? match = _cards + return _cards .Where(card => card.Source.Kind == source.Kind && (card.Source.ExternalId ?? string.Empty) == externalId && (card.Source.OriginRef ?? string.Empty) == originRef) .OrderByDescending(card => card.ReceivedAtMs) .FirstOrDefault(); - return Task.FromResult(match); } - /// - public Task AddCardAsync(CardSnapshot snapshot, CancellationToken ct) + private void AddCard(CardSnapshot snapshot) { if (FailAddCard) { @@ -249,16 +364,14 @@ public class FakeKanjStore : ICardStore CreatedAtMs = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(), UpdatedAtMs = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(), }); - return Task.CompletedTask; } - /// - public Task UpdateColumnAsync(CardColumnUpdateDto update, CancellationToken ct) + private void UpdateColumn(CardColumnUpdateDto update) { int index = _cards.FindIndex(card => card.Id == update.CardId); if (index < 0) { - return Task.CompletedTask; + return; } _cards[index] = _cards[index] with @@ -276,17 +389,14 @@ public class FakeKanjStore : ICardStore { _archivedAtById.Remove(update.CardId); } - - return Task.CompletedTask; } - /// - public Task ApplyReclassificationAsync(CardReclassificationDto update, CancellationToken ct) + private bool ApplyReclassification(CardReclassificationDto update) { int index = _cards.FindIndex(card => card.Id == update.CardId); if (index < 0) { - return Task.FromResult(false); + return false; } _cards[index] = _cards[index] with @@ -304,40 +414,28 @@ public class FakeKanjStore : ICardStore Contacts = update.Contacts, MatchHits = update.MatchHits, }; - return Task.FromResult(true); + return true; } - /// - public Task UpdateSeenAsync( - string? cardId, - string? col, - CancellationToken ct) + private void UpdateSeen(string? cardId, string? col) { for (int i = 0; i < _cards.Count; i++) { - bool matches = cardId is not null - ? _cards[i].Id == cardId - : col is null || _cards[i].Col == col; + bool matches = cardId is not null ? _cards[i].Id == cardId : col is null || _cards[i].Col == col; if (matches && _cards[i].IsNew) { _cards[i] = _cards[i] with { IsNew = false }; } } - - return Task.CompletedTask; } - /// - public Task DeleteForeverAsync(string cardId, CancellationToken ct) + private void DeleteForever(string cardId) { - // Удаляется карточка (с комментариями — они часть карточки в фейке); журнал CardMoves не трогаем. _cards.RemoveAll(card => card.Id == cardId); _archivedAtById.Remove(cardId); - return Task.CompletedTask; } - /// - public Task ClearColAsync(string col, CancellationToken ct) + private int ClearCol(string col) { List removedIds = _cards.Where(card => card.Col == col).Select(card => card.Id).ToList(); _cards.RemoveAll(card => card.Col == col); @@ -346,64 +444,33 @@ public class FakeKanjStore : ICardStore _archivedAtById.Remove(cardId); } - return Task.FromResult(removedIds.Count); + return removedIds.Count; } - /// - public Task> CountCardsByColAsync(CancellationToken ct) + private IReadOnlyDictionary CountCardsByCol() { - IReadOnlyDictionary result = _cards + return _cards .GroupBy(card => card.Col) .ToDictionary( group => group.Key, group => new CardColumnCountDto(group.Count(), group.Count(card => card.IsNew))); - return Task.FromResult(result); } - // ── Комментарии (LeadComments) ──────────────────────────────────────── - - /// - public Task> ListCommentsAsync(string cardId, CancellationToken ct) - { - CardDto? card = _cards.FirstOrDefault(item => item.Id == cardId); - return Task.FromResult(card?.Comments ?? Array.Empty()); - } - - /// - public Task AddCommentAsync( - string commentId, - string cardId, - string by, - string text, - CancellationToken ct) + private void AddComment(string commentId, string cardId, string by, string text) { int index = _cards.FindIndex(card => card.Id == cardId); if (index < 0) { - return Task.CompletedTask; + return; } // Метка времени свежего комментария — «только что» (адаптер считает HumanAge от CreatedAt = UtcNow). CardCommentDto comment = new(commentId, by, text, "только что"); IReadOnlyList updated = _cards[index].Comments.Concat(new[] { comment }).ToList(); _cards[index] = _cards[index] with { Comments = updated }; - return Task.CompletedTask; } - // ── Журнал CardMoves (learning_log) ─────────────────────────────────── - - /// - public Task AddMoveAsync(CardMoveDto move, CancellationToken ct) - { - _moves.Add(move); - return Task.CompletedTask; - } - - /// - public Task CountMovesAsync(CancellationToken ct) => Task.FromResult(_moves.Count); - - /// - public Task> GetAiMarkupExamplesAsync(int limit, CancellationToken ct) + private IReadOnlyList GetAiMarkupExamples(int limit) { var examples = new List(); for (int index = _moves.Count - 1; index >= 0 && examples.Count < limit; index--) @@ -429,31 +496,24 @@ public class FakeKanjStore : ICardStore examples.Add(new AiMarkupExampleDto(sourceText, move.ToCol)); } - return Task.FromResult>(examples); + return examples; } - - /// - public Task> ListArchiveCandidatesAsync(DateTimeOffset receivedBeforeUtc, CancellationToken ct) + private IReadOnlyList ListArchiveCandidates(DateTimeOffset receivedBeforeUtc) { var boardIds = new HashSet(_boards.Select(board => board.Id), StringComparer.Ordinal); - IReadOnlyList result = _cards + return _cards .Where(card => (card.Col == KanbanColumns.Inbox || boardIds.Contains(card.Col)) && ReceivedAtOf(card) < receivedBeforeUtc) .Select(card => card.Id) .ToList(); - return Task.FromResult(result); } - /// - public Task ArchiveAsync( - IReadOnlyList cardIds, - DateTimeOffset archivedAt, - CancellationToken ct) + private int Archive(IReadOnlyList cardIds, DateTimeOffset archivedAt) { if (cardIds.Count == 0) { - return Task.FromResult(0); + return 0; } var ids = new HashSet(cardIds, StringComparer.Ordinal); @@ -475,38 +535,32 @@ public class FakeKanjStore : ICardStore archived++; } - return Task.FromResult(archived); + return archived; } - /// - public Task> ListExpiredArchiveCandidatesAsync(DateTimeOffset archivedBeforeUtc, CancellationToken ct) + private IReadOnlyList ListExpiredArchiveCandidates(DateTimeOffset archivedBeforeUtc) { - IReadOnlyList result = _cards + return _cards .Where(card => card.Col == KanbanColumns.Archive && _archivedAtById.TryGetValue(card.Id, out DateTimeOffset archivedAt) && archivedAt < archivedBeforeUtc) .Select(card => card.Id) .ToList(); - return Task.FromResult(result); } - /// - public Task> ListTrashCandidatesAsync(DateTimeOffset receivedBeforeUtc, CancellationToken ct) + private IReadOnlyList ListTrashCandidates(DateTimeOffset receivedBeforeUtc) { - IReadOnlyList result = _cards + return _cards .Where(card => card.Col == KanbanColumns.Trash && ReceivedAtOf(card) < receivedBeforeUtc) .Select(card => card.Id) .ToList(); - return Task.FromResult(result); } - /// - public Task PurgeAsync(IReadOnlyList cardIds, CancellationToken ct) + private int Purge(IReadOnlyList cardIds) { - // Жёсткое удаление пачки (как KanbanStore.PurgeAsync): комментарии — часть карточки, журнал не трогаем. if (cardIds.Count == 0) { - return Task.FromResult(0); + return 0; } var ids = new HashSet(cardIds, StringComparer.Ordinal); @@ -516,88 +570,45 @@ public class FakeKanjStore : ICardStore _archivedAtById.Remove(cardId); } - return Task.FromResult(removed); + return removed; } - /// - /// Задаёт метку архивации карточки - /// - /// Id карточки. - /// Метка архивации (когда карточка ушла в архив). - public void SetArchivedAt(string cardId, DateTimeOffset archivedAt) => _archivedAtById[cardId] = archivedAt; - - /// - /// Метка архивации карточки — проверка archived_at после автоархива тика. - /// - /// Id карточки. - /// Метка архивации либо null — карточки нет/не архивирована/метка сброшена. - public DateTimeOffset? ArchivedAtOf(string cardId) + private IReadOnlyList ListCardsForConversion() { - return _archivedAtById.TryGetValue(cardId, out DateTimeOffset archivedAt) ? archivedAt : null; - } - - // Момент получения карточки из её epoch-ms (как маппинг адаптера ReceivedAt → ReceivedAtMs). - // card: Карточка. - // Возвращает: ReceivedAt карточки как UTC-момент. - private static DateTimeOffset ReceivedAtOf(CardDto card) => DateTimeOffset.FromUnixTimeMilliseconds(card.ReceivedAtMs); - - - /// - public Task> ListCardsForConversionAsync(CancellationToken ct) - { - IReadOnlyList result = _cards + return _cards .Where(card => card.Budget is not null && card.Col != KanbanColumns.Archive && card.Col != KanbanColumns.Trash) .OrderByDescending(card => card.ReceivedAtMs) .ToList(); - return Task.FromResult(result); } - /// - public Task UpdateConversionAsync( - string cardId, - double? convFrom, - double? convTo, - string convCur, - CancellationToken ct) + private void UpdateConversion(string cardId, double? convFrom, double? convTo, string convCur) { int index = _cards.FindIndex(card => card.Id == cardId); if (index >= 0) { _cards[index] = _cards[index] with { - Converted = convCur.Length == 0 - ? null - : new CardBudgetDto(convFrom, convTo, convCur), + Converted = convCur.Length == 0 ? null : new CardBudgetDto(convFrom, convTo, convCur), }; } - - return Task.CompletedTask; } - - - /// - public Task> ListSelectedCardsAsync(string? containerId, CancellationToken ct) + private IReadOnlyList ListSelectedCards(string? containerId) { IEnumerable query = containerId is null ? _cards.Where(card => CardsDefaultContainers.Contains(card.Col)) : _cards.Where(card => card.Col == containerId); - return Task.FromResult>( - query.OrderByDescending(card => card.UpdatedAtMs).ToList()); + return query.OrderByDescending(card => card.UpdatedAtMs).ToList(); } - /// - public Task PatchCardAsync( - string cardId, - CardPatch patch, - CancellationToken ct) + private bool PatchCard(string cardId, CardPatch patch) { int index = _cards.FindIndex(card => card.Id == cardId); if (index < 0) { - return Task.FromResult(false); + return false; } CardDto card = _cards[index]; @@ -647,19 +658,15 @@ public class FakeKanjStore : ICardStore } _cards[index] = card with { UpdatedAtMs = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds() }; - return Task.FromResult(true); + return true; } - /// - public Task AddLinkAsync( - string cardId, - CardLinkDto link, - CancellationToken ct) + private bool AddLink(string cardId, CardLinkDto link) { int index = _cards.FindIndex(card => card.Id == cardId); if (index < 0) { - return Task.FromResult(false); + return false; } _cards[index] = _cards[index] with @@ -667,19 +674,15 @@ public class FakeKanjStore : ICardStore Links = [.. _cards[index].Links, link], UpdatedAtMs = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(), }; - return Task.FromResult(true); + return true; } - /// - public Task RemoveLinkAsync( - string cardId, - string linkId, - CancellationToken ct) + private bool RemoveLink(string cardId, string linkId) { int index = _cards.FindIndex(card => card.Id == cardId); if (index < 0) { - return Task.FromResult(false); + return false; } _cards[index] = _cards[index] with @@ -687,19 +690,15 @@ public class FakeKanjStore : ICardStore Links = _cards[index].Links.Where(link => link.Id != linkId).ToList(), UpdatedAtMs = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(), }; - return Task.FromResult(true); + return true; } - /// - public Task AddFileAsync( - string cardId, - CardFileDto file, - CancellationToken ct) + private bool AddFile(string cardId, CardFileDto file) { int index = _cards.FindIndex(card => card.Id == cardId); if (index < 0) { - return Task.FromResult(false); + return false; } _cards[index] = _cards[index] with @@ -707,19 +706,15 @@ public class FakeKanjStore : ICardStore Files = [.. _cards[index].Files, file], UpdatedAtMs = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(), }; - return Task.FromResult(true); + return true; } - /// - public Task RemoveFileAsync( - string cardId, - string fileId, - CancellationToken ct) + private bool RemoveFile(string cardId, string fileId) { int index = _cards.FindIndex(card => card.Id == cardId); if (index < 0) { - return Task.FromResult(false); + return false; } _cards[index] = _cards[index] with @@ -727,21 +722,15 @@ public class FakeKanjStore : ICardStore Files = _cards[index].Files.Where(file => file.Id != fileId).ToList(), UpdatedAtMs = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(), }; - return Task.FromResult(true); + return true; } - /// - public Task MoveCardStageAsync( - string cardId, - string containerId, - CardHistoryDto historyEntry, - long atMs, - CancellationToken ct) + private bool MoveCardStage(string cardId, string containerId, CardHistoryDto historyEntry, long atMs) { int index = _cards.FindIndex(card => card.Id == cardId); if (index < 0) { - return Task.FromResult(false); + return false; } CardDto card = _cards[index]; @@ -753,19 +742,15 @@ public class FakeKanjStore : ICardStore UpdatedAtMs = atMs, History = [.. card.History, historyEntry], }; - return Task.FromResult(true); + return true; } - /// - public Task SetReminderAsync( - string cardId, - long atMs, - CancellationToken ct) + private void SetReminder(string cardId, long atMs) { int index = _cards.FindIndex(card => card.Id == cardId); if (index < 0) { - return Task.CompletedTask; + return; } _firedById.Remove(cardId); @@ -774,11 +759,9 @@ public class FakeKanjStore : ICardStore Reminder = new CardReminderDto(atMs), UpdatedAtMs = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(), }; - return Task.CompletedTask; } - /// - public Task ClearReminderAsync(string cardId, CancellationToken ct) + private void ClearReminder(string cardId) { int index = _cards.FindIndex(card => card.Id == cardId); if (index >= 0) @@ -787,20 +770,14 @@ public class FakeKanjStore : ICardStore } _firedById.Remove(cardId); - return Task.CompletedTask; } - /// - public Task ClearStageAsync(string containerId, CancellationToken ct) - { - return Task.FromResult(_cards.RemoveAll(card => card.Col == containerId)); - } + private int ClearStage(string containerId) => _cards.RemoveAll(card => card.Col == containerId); - /// - public virtual Task> ListDueRemindersAsync(DateTimeOffset now, CancellationToken ct) + private IReadOnlyList ListDueReminders(DateTimeOffset now) { long nowMs = now.ToUnixTimeMilliseconds(); - IReadOnlyList due = _cards + return _cards .Where(card => card.Col == CardsDefaultContainers.Hold && card.Reminder is not null && !_firedById.Contains(card.Id) @@ -808,11 +785,9 @@ public class FakeKanjStore : ICardStore .OrderBy(card => card.Reminder!.At) .Select(card => new CardReminderDueDto(card.Id, card.Title, card.Col)) .ToList(); - return Task.FromResult(due); } - /// - public Task MarkRemindersFiredAsync(IReadOnlyList cardIds, CancellationToken ct) + private void MarkRemindersFired(IReadOnlyList cardIds) { foreach (string cardId in cardIds) { @@ -821,12 +796,9 @@ public class FakeKanjStore : ICardStore _firedById.Add(cardId); } } - - return Task.CompletedTask; } - /// - public Task ClearExpiredRemindersAsync(DateTimeOffset now, CancellationToken ct) + private int ClearExpiredReminders(DateTimeOffset now) { long nowMs = now.ToUnixTimeMilliseconds(); int cleared = 0; @@ -840,16 +812,16 @@ public class FakeKanjStore : ICardStore } } - return Task.FromResult(cleared); + return cleared; } - /// - public Task> ListInboxWithSourceAsync(CancellationToken ct) + private IReadOnlyList ListInboxWithSource() { - IReadOnlyList result = _cards + return _cards .Where(card => card.Col == KanbanColumns.Inbox && (card.Content.Text ?? string.Empty) != string.Empty) .OrderByDescending(card => card.ReceivedAtMs) .ToList(); - return Task.FromResult(result); } + + private static DateTimeOffset ReceivedAtOf(CardDto card) => DateTimeOffset.FromUnixTimeMilliseconds(card.ReceivedAtMs); }