diff --git a/src/core/tests/Deal.Tests.Unit/Api/DiscoveryWorkerSchedulerTests.cs b/src/core/tests/Deal.Tests.Unit/Api/DiscoveryWorkerSchedulerTests.cs index e954560..68fc2e1 100644 --- a/src/core/tests/Deal.Tests.Unit/Api/DiscoveryWorkerSchedulerTests.cs +++ b/src/core/tests/Deal.Tests.Unit/Api/DiscoveryWorkerSchedulerTests.cs @@ -104,7 +104,7 @@ public sealed class DiscoveryWorkerSchedulerTests var services = new ServiceCollection(); services.AddSingleton(tenantContext); services.AddSingleton(tenants.Repository); - services.AddSingleton(new FakeMlClient()); + services.AddSingleton(new TestMlClient().Client); services.AddSingleton(_ => TestAiTools.New()); // Тенант-scoped адаптеры: фейк выбирает хранилище по ITenantContext, который цикл заполняет SetTenant // (эталон PipelineWorkerSchedulerTests/ConnectionStringProvider.ForTenant). diff --git a/src/core/tests/Deal.Tests.Unit/Api/RuntimeDepthsCollectorTests.cs b/src/core/tests/Deal.Tests.Unit/Api/RuntimeDepthsCollectorTests.cs index 7fe73cc..f2dba86 100644 --- a/src/core/tests/Deal.Tests.Unit/Api/RuntimeDepthsCollectorTests.cs +++ b/src/core/tests/Deal.Tests.Unit/Api/RuntimeDepthsCollectorTests.cs @@ -154,7 +154,7 @@ public sealed class RuntimeDepthsCollectorTests }).ToArray()).Repository); services.AddScoped(provider => pipelineByTenant[CurrentTenant(provider)]); - services.AddScoped(_ => new FakeMlClient()); + services.AddScoped(_ => new TestMlClient().Client); services.AddScoped(); services.AddScoped(); services.AddScoped(provider => outboxByTenant[CurrentTenant(provider)]); @@ -172,4 +172,4 @@ public sealed class RuntimeDepthsCollectorTests ITenantContext context = provider.GetRequiredService(); return context.TenantId!.Value.Value; } -} \ No newline at end of file +} diff --git a/src/core/tests/Deal.Tests.Unit/Contracts/CardsServiceTests.cs b/src/core/tests/Deal.Tests.Unit/Contracts/CardsServiceTests.cs index 8ae48b4..4c5e0c1 100644 --- a/src/core/tests/Deal.Tests.Unit/Contracts/CardsServiceTests.cs +++ b/src/core/tests/Deal.Tests.Unit/Contracts/CardsServiceTests.cs @@ -84,7 +84,7 @@ public sealed class CardsServiceTests [Fact] public async Task Move_ToBoard_WritesJournalAndPush() { - (CardsService service, FakeKanjStore store, _, FakeMlClient ml) = Create(); + (CardsService service, FakeKanjStore store, _, TestMlClient ml) = Create(); store.SeedBoard(Board("b_py", Rules(stack: new[] { "python" }))); store.SeedCard(Card("l_1", "inbox", sourceMsg: "Нужен Python-разработчик")); @@ -105,7 +105,7 @@ public sealed class CardsServiceTests [Fact] public async Task Move_ToInbox_WritesJournalWithoutPush() { - (CardsService service, FakeKanjStore store, _, FakeMlClient ml) = Create(); + (CardsService service, FakeKanjStore 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 +124,7 @@ public sealed class CardsServiceTests [Fact] public async Task Move_ToUnknownBoard_Returns400Text() { - (CardsService service, FakeKanjStore store, _, FakeMlClient ml) = Create(); + (CardsService service, FakeKanjStore 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 +148,7 @@ public sealed class CardsServiceTests [Fact] public async Task Move_CardMissing_ReturnsNullLead() { - (CardsService service, FakeKanjStore store, _, FakeMlClient ml) = Create(); + (CardsService service, FakeKanjStore store, _, TestMlClient ml) = Create(); store.SeedBoard(Board("b_py")); CardResultDto result = await service.MoveDashboardCardAsync("l_ghost", "b_py", CancellationToken.None); @@ -162,7 +162,7 @@ public sealed class CardsServiceTests [Fact] public async Task Move_SameColumn_NoOpWithoutJournalAndPush() { - (CardsService service, FakeKanjStore store, _, FakeMlClient ml) = Create(); + (CardsService service, FakeKanjStore 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 +179,7 @@ public sealed class CardsServiceTests [Fact] public async Task Move_ToBoardWithoutRules_MatchHitsEmptyButPushSent() { - (CardsService service, FakeKanjStore store, _, FakeMlClient ml) = Create(); + (CardsService service, FakeKanjStore store, _, TestMlClient ml) = Create(); store.SeedBoard(Board("b_free")); // правил нет store.SeedCard(Card("l_1", "inbox", sourceMsg: "Любой текст")); @@ -195,7 +195,7 @@ public sealed class CardsServiceTests [Fact] public async Task Move_FromBoardToBoard_PrevColIsOldBoardAndPushNewLabel() { - (CardsService service, FakeKanjStore store, _, FakeMlClient ml) = Create(); + (CardsService service, FakeKanjStore 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 +214,7 @@ public sealed class CardsServiceTests [Fact] public async Task Move_EmptySourceText_UsesTitleForMatchHitsAndPush() { - (CardsService service, FakeKanjStore store, _, FakeMlClient ml) = Create(); + (CardsService service, FakeKanjStore 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 +228,7 @@ public sealed class CardsServiceTests [Fact] public async Task Move_EmptySourceAndTitle_JournalWithoutPush() { - (CardsService service, FakeKanjStore store, _, FakeMlClient ml) = Create(); + (CardsService service, FakeKanjStore store, _, TestMlClient ml) = Create(); store.SeedBoard(Board("b_py")); store.SeedCard(Card("l_1", "inbox")); @@ -263,7 +263,7 @@ public sealed class CardsServiceTests [Fact] public async Task Move_FromArchive_Returns400AndWritesNothing() { - (CardsService service, FakeKanjStore store, _, FakeMlClient ml) = Create(); + (CardsService service, FakeKanjStore store, _, TestMlClient ml) = Create(); store.SeedBoard(Board("b_py")); store.SeedCard(Card("l_1", KanbanColumns.Archive, sourceMsg: "Старая вакансия", prevCol: "b_py")); @@ -279,7 +279,7 @@ public sealed class CardsServiceTests [Fact] public async Task Move_FromTrash_Returns400AndWritesNothing() { - (CardsService service, FakeKanjStore store, _, FakeMlClient ml) = Create(); + (CardsService service, FakeKanjStore store, _, TestMlClient ml) = Create(); store.SeedBoard(Board("b_py")); store.SeedCard(Card("l_1", KanbanColumns.Trash, sourceMsg: "Спам-текст", prevCol: "b_py")); @@ -296,7 +296,7 @@ public sealed class CardsServiceTests [Fact] public async Task Trash_FromInbox_MovesToTrashWithJournalAndSpamPush() { - (CardsService service, FakeKanjStore store, _, FakeMlClient ml) = Create(); + (CardsService service, FakeKanjStore store, _, TestMlClient ml) = Create(); store.SeedCard(Card("l_1", "inbox", sourceMsg: "Предлагаю услуги продвижения", isNew: true)); CardDto? result = await service.TrashCardAsync("l_1", CancellationToken.None); @@ -318,7 +318,7 @@ public sealed class CardsServiceTests [Fact] public async Task Trash_AlreadyInTrash_NoOpWithoutJournalAndPush() { - (CardsService service, FakeKanjStore store, _, FakeMlClient ml) = Create(); + (CardsService service, FakeKanjStore store, _, TestMlClient ml) = Create(); store.SeedCard(Card("l_1", KanbanColumns.Trash, sourceMsg: "Текст")); CardDto? result = await service.TrashCardAsync("l_1", CancellationToken.None); @@ -331,7 +331,7 @@ public sealed class CardsServiceTests [Fact] public async Task Trash_FromArchive_WritesJournalWithoutPush() { - (CardsService service, FakeKanjStore store, _, FakeMlClient ml) = Create(); + (CardsService service, FakeKanjStore store, _, TestMlClient ml) = Create(); store.SeedCard(Card("l_1", KanbanColumns.Archive, sourceMsg: "Старая карточка", prevCol: "b_py")); CardDto? result = await service.TrashCardAsync("l_1", CancellationToken.None); @@ -346,7 +346,7 @@ public sealed class CardsServiceTests [Fact] public async Task Trash_CardMissing_ReturnsNull() { - (CardsService service, _, _, FakeMlClient ml) = Create(); + (CardsService service, _, _, TestMlClient ml) = Create(); CardDto? result = await service.TrashCardAsync("l_ghost", CancellationToken.None); @@ -358,7 +358,7 @@ public sealed class CardsServiceTests [Fact] public async Task Restore_FromTrash_ToPrevColBoard_UnlearnsSpam() { - (CardsService service, FakeKanjStore store, _, FakeMlClient ml) = Create(); + (CardsService service, FakeKanjStore 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 +383,7 @@ public sealed class CardsServiceTests [Fact] public async Task Restore_FromArchive_NoPush() { - (CardsService service, FakeKanjStore store, _, FakeMlClient ml) = Create(); + (CardsService service, FakeKanjStore store, _, TestMlClient ml) = Create(); store.SeedBoard(Board("b_py")); store.SeedCard(Card("l_1", KanbanColumns.Archive, sourceMsg: "Старая карточка", prevCol: "b_py")); @@ -399,7 +399,7 @@ public sealed class CardsServiceTests [Fact] public async Task Restore_FromTrash_PrevColDeletedBoard_FallsBackToInbox() { - (CardsService service, FakeKanjStore store, _, FakeMlClient ml) = Create(); + (CardsService service, FakeKanjStore store, _, TestMlClient ml) = Create(); store.SeedCard(Card("l_1", KanbanColumns.Trash, sourceMsg: "Текст", prevCol: "b_gone")); string? back = await service.RestoreCardAsync("l_1", CancellationToken.None); @@ -618,7 +618,7 @@ public sealed class CardsServiceTests [Fact] public async Task Counts_ColumnsNewAndMlStats() { - (CardsService service, FakeKanjStore store, _, FakeMlClient ml) = Create(); + (CardsService service, FakeKanjStore 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")); @@ -711,12 +711,12 @@ public sealed class CardsServiceTests // ─── Хелперы ────────────────────────────────────────────────────────── - private static (CardsService Service, FakeKanjStore Store, FakeSettingsStore Settings, FakeMlClient Ml) Create() + private static (CardsService Service, FakeKanjStore Store, FakeSettingsStore Settings, TestMlClient Ml) Create() { var store = new FakeKanjStore(); var settings = new FakeSettingsStore(); - var ml = new FakeMlClient(); - return (new CardsService(store, settings, ml, new TestFileStorage().Storage), store, settings, ml); + var ml = new TestMlClient(); + return (new CardsService(store, settings, ml.Client, new TestFileStorage().Storage), store, settings, ml); } // Доска с правилами (ContainerRulesDto) либо без них. diff --git a/src/core/tests/Deal.Tests.Unit/Contracts/DiscoveryEvaluatorTests.cs b/src/core/tests/Deal.Tests.Unit/Contracts/DiscoveryEvaluatorTests.cs index 3520c6e..fb8b4e1 100644 --- a/src/core/tests/Deal.Tests.Unit/Contracts/DiscoveryEvaluatorTests.cs +++ b/src/core/tests/Deal.Tests.Unit/Contracts/DiscoveryEvaluatorTests.cs @@ -30,7 +30,7 @@ public sealed class DiscoveryEvaluatorTests [Fact] public async Task EvaluateMessage_MlSpam_NotFit() { - (DiscoveryEvaluator evaluator, _, FakeMlClient ml, _) = Create(aiEnabled: false, mlEnabled: true); + (DiscoveryEvaluator evaluator, _, TestMlClient ml, _) = Create(aiEnabled: false, mlEnabled: true); ml.Predict = new MlPredictResultDto( Take: true, Label: "spam", Scores: new Dictionary(), Hits: 3, Ready: true, Margin: 0.9, Terms: Array.Empty(), Type: null); @@ -46,7 +46,7 @@ public sealed class DiscoveryEvaluatorTests [Fact] public async Task EvaluateMessage_AiFits_ReturnsAiVerdict() { - (DiscoveryEvaluator evaluator, _, FakeMlClient ml, IAiTools ai) = Create(aiEnabled: true, mlEnabled: false); + (DiscoveryEvaluator evaluator, _, TestMlClient ml, IAiTools ai) = Create(aiEnabled: true, mlEnabled: false); ai.ReturnsFit(new AiEvaluateFitResultDto(Fit: true, "тема совпадает")); DiscoveryMessageFit fit = await evaluator.EvaluateMessageAsync( @@ -168,14 +168,14 @@ public sealed class DiscoveryEvaluatorTests => new(id, text, DateMs: 1_700_000_000_000 + id, topic, TopicTitle: null); // Создаёт оценщик на фейках: флаги веток выставляются сценарием. - private static (DiscoveryEvaluator Evaluator, FakeSettingsStore Settings, FakeMlClient Ml, IAiTools Ai) Create(bool aiEnabled, bool mlEnabled) + private static (DiscoveryEvaluator Evaluator, FakeSettingsStore Settings, TestMlClient Ml, IAiTools Ai) Create(bool aiEnabled, bool mlEnabled) { var settings = new FakeSettingsStore(); settings.Preload(SettingsKeys.AiEnabled, aiEnabled ? "true" : "false"); settings.Preload(SettingsKeys.MlEnabled, mlEnabled ? "true" : "false"); - var ml = new FakeMlClient(); + var ml = new TestMlClient(); var ai = TestAiTools.New(); - return (new DiscoveryEvaluator(settings, ml, ai), settings, ml, ai); + return (new DiscoveryEvaluator(settings, ml.Client, ai), settings, ml, ai); } // Задача как строка DiscTasks (адаптер-форма; нужны только keywords/description/threshold). diff --git a/src/core/tests/Deal.Tests.Unit/Contracts/DiscoveryWorkerServiceTests.cs b/src/core/tests/Deal.Tests.Unit/Contracts/DiscoveryWorkerServiceTests.cs index 698a23a..6e3d55a 100644 --- a/src/core/tests/Deal.Tests.Unit/Contracts/DiscoveryWorkerServiceTests.cs +++ b/src/core/tests/Deal.Tests.Unit/Contracts/DiscoveryWorkerServiceTests.cs @@ -432,7 +432,7 @@ public sealed class DiscoveryWorkerServiceTests var settings = new FakeSettingsStore(); settings.Preload(SettingsKeys.MlEnabled, "false"); settings.Preload(SettingsKeys.AiEnabled, "false"); - var ml = new FakeMlClient(); + var ml = new TestMlClient(); var ai = TestAiTools.New(); var log = new DiscoveryLogService(store.Store); var blacklist = new DiscoveryBlacklistService(store.Store); @@ -443,7 +443,7 @@ public sealed class DiscoveryWorkerServiceTests var pacer = Substitute.For(); var gateway = new TestDiscoveryGateway(); var worker = new DiscoveryWorkerService( - store.Store, tasks, candidates, log, new DiscoveryEvaluator(settings, ml, ai), banGuard, pacer, gateway.Gateway); + store.Store, tasks, candidates, log, new DiscoveryEvaluator(settings, ml.Client, ai), banGuard, pacer, gateway.Gateway); return new Fixture(store, settings, gateway, pacer, worker, tasks); } diff --git a/src/core/tests/Deal.Tests.Unit/Contracts/PipelineWorkerGrpcAiTests.cs b/src/core/tests/Deal.Tests.Unit/Contracts/PipelineWorkerGrpcAiTests.cs index 4ea2314..834776d 100644 --- a/src/core/tests/Deal.Tests.Unit/Contracts/PipelineWorkerGrpcAiTests.cs +++ b/src/core/tests/Deal.Tests.Unit/Contracts/PipelineWorkerGrpcAiTests.cs @@ -167,7 +167,7 @@ public sealed class PipelineWorkerGrpcAiTests var settings = new FakeSettingsStore(); var pipelineStore = new TestPipelineStore(); var kanjStore = new FakeKanjStore(); - var mlClient = new FakeMlClient { Predict = NotReadyPrediction() }; + var mlClient = new TestMlClient { Predict = NotReadyPrediction() }; var rules = new IncomingRules(settings); var fieldsParser = new LocalFieldsParser(settings); @@ -192,11 +192,11 @@ public sealed class PipelineWorkerGrpcAiTests NullLogger.Instance) : grpcClassifier; - var processing = new PipelineProcessingService(pipelineStore.Store, mlClient, new PipelineIngestService(pipelineStore.Store)); + var processing = new PipelineProcessingService(pipelineStore.Store, mlClient.Client, new PipelineIngestService(pipelineStore.Store)); var composer = new CardComposer(kanjStore, settings); var writer = new PipelineCardWriter(kanjStore, pipelineStore.Store, composer); var worker = new PipelineWorkerService( - pipelineStore.Store, settings, rules, kanjStore, mlClient, aiClassifier, processing, writer, fieldsParser); + pipelineStore.Store, settings, rules, kanjStore, 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 a44505c..96baee1 100644 --- a/src/core/tests/Deal.Tests.Unit/Infrastructure/CardMoverTests.cs +++ b/src/core/tests/Deal.Tests.Unit/Infrastructure/CardMoverTests.cs @@ -82,7 +82,7 @@ public sealed class CardMoverTests private static (CardMover Mover, FakeKanjStore Store) Create() { var store = new FakeKanjStore(); - var cardsService = new CardsService(store, new FakeSettingsStore(), new FakeMlClient(), new TestFileStorage().Storage); + var cardsService = new CardsService(store, new FakeSettingsStore(), new TestMlClient().Client, new TestFileStorage().Storage); return (new CardMover(cardsService), store); } } 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 af636eb..3f99402 100644 --- a/src/core/tests/Deal.Tests.Unit/Modules/Kanban/CardsServiceRemindersTests.cs +++ b/src/core/tests/Deal.Tests.Unit/Modules/Kanban/CardsServiceRemindersTests.cs @@ -195,7 +195,7 @@ public sealed class CardsServiceRemindersTests settings.Preload(SettingsKeys.RemindersEnabled, "false"); } - return (new CardsService(store, settings, new FakeMlClient(), new TestFileStorage().Storage), store, settings); + return (new CardsService(store, settings, 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 757fa62..ce11110 100644 --- a/src/core/tests/Deal.Tests.Unit/Modules/Kanban/MlReviewServiceTests.cs +++ b/src/core/tests/Deal.Tests.Unit/Modules/Kanban/MlReviewServiceTests.cs @@ -21,13 +21,13 @@ public sealed class MlReviewServiceTests private static MlReviewService Create( TestPipelineStore pipeline, FakeKanjStore kanj, - FakeMlClient ml, + TestMlClient ml, out CardsService cards) { var settings = new FakeSettingsStore(); - cards = new CardsService(kanj, settings, ml, new TestFileStorage().Storage); - var processing = new PipelineProcessingService(pipeline.Store, ml, new PipelineIngestService(pipeline.Store)); - return new MlReviewService(pipeline.Store, kanj, cards, processing, ml); + cards = new CardsService(kanj, settings, 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); } private static QueueItemDto QueueRow( @@ -78,7 +78,7 @@ public sealed class MlReviewServiceTests pipeline.SeedRejected(RejectedRow(102, "из отсева")); var kanj = new FakeKanjStore(); kanj.SeedCard(Card(103, "из карточки")); - MlReviewService service = Create(pipeline, kanj, new FakeMlClient(), out _); + MlReviewService service = Create(pipeline, kanj, new TestMlClient(), out _); IReadOnlyList items = await service.CandidatesAsync(Dialog, 10, CancellationToken.None); @@ -97,7 +97,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 FakeMlClient(), out _); + MlReviewService service = Create(pipeline, new FakeKanjStore(), new TestMlClient(), out _); IReadOnlyList items = await service.CandidatesAsync(Dialog, 10, CancellationToken.None); @@ -111,7 +111,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 FakeMlClient(), out _); + MlReviewService service = Create(pipeline, new FakeKanjStore(), new TestMlClient(), out _); IReadOnlyList items = await service.CandidatesAsync(null, 10, CancellationToken.None); @@ -127,7 +127,7 @@ public sealed class MlReviewServiceTests pipeline.SeedQueue(QueueRow(100 + i, $"текст {i}")); } - MlReviewService service = Create(pipeline, new FakeKanjStore(), new FakeMlClient(), out _); + MlReviewService service = Create(pipeline, new FakeKanjStore(), new TestMlClient(), out _); IReadOnlyList items = await service.CandidatesAsync(Dialog, 2, CancellationToken.None); @@ -139,7 +139,7 @@ public sealed class MlReviewServiceTests { var pipeline = new TestPipelineStore(); pipeline.SeedQueue(QueueRow(101, "текст")); - var ml = new FakeMlClient(); + var ml = new TestMlClient(); MlReviewService service = Create(pipeline, new FakeKanjStore(), ml, out _); MlApplyResult? result = await service.ApplyAsync(Dialog, 101, MlReviewService.ActionSkip, CancellationToken.None); @@ -156,7 +156,7 @@ public sealed class MlReviewServiceTests { var kanj = new FakeKanjStore(); kanj.SeedCard(Card(101, "спамный текст")); - var ml = new FakeMlClient(); + var ml = new TestMlClient(); MlReviewService service = Create(new TestPipelineStore(), kanj, ml, out _); MlApplyResult? result = await service.ApplyAsync(Dialog, 101, MlReviewService.ActionSpam, CancellationToken.None); @@ -174,7 +174,7 @@ public sealed class MlReviewServiceTests { var pipeline = new TestPipelineStore(); pipeline.SeedQueue(QueueRow(101, "рекламный текст")); - var ml = new FakeMlClient(); + var ml = new TestMlClient(); MlReviewService service = Create(pipeline, new FakeKanjStore(), ml, out _); MlApplyResult? result = await service.ApplyAsync(Dialog, 101, MlReviewService.ActionSpam, CancellationToken.None); @@ -201,7 +201,7 @@ public sealed class MlReviewServiceTests Kind = ContainerKinds.Board, }); kanj.SeedCard(Card(101, "python разработчик")); - var ml = new FakeMlClient(); + var ml = new TestMlClient(); MlReviewService service = Create(new TestPipelineStore(), kanj, ml, out _); MlApplyResult? result = await service.ApplyAsync(Dialog, 101, "board:b_py", CancellationToken.None); @@ -218,7 +218,7 @@ public sealed class MlReviewServiceTests { var kanj = new FakeKanjStore(); kanj.SeedCard(Card(101, "текст")); - MlReviewService service = Create(new TestPipelineStore(), kanj, new FakeMlClient(), out _); + MlReviewService service = Create(new TestPipelineStore(), kanj, new TestMlClient(), out _); MlApplyResult? result = await service.ApplyAsync(Dialog, 101, "board:missing", CancellationToken.None); @@ -232,7 +232,7 @@ public sealed class MlReviewServiceTests { var pipeline = new TestPipelineStore(); pipeline.SeedQueue(QueueRow(101, "текст")); - MlReviewService service = Create(pipeline, new FakeKanjStore(), new FakeMlClient(), out _); + MlReviewService service = Create(pipeline, new FakeKanjStore(), new TestMlClient(), out _); MlApplyResult? result = await service.ApplyAsync(Dialog, 101, "что-то", CancellationToken.None); @@ -244,7 +244,7 @@ public sealed class MlReviewServiceTests [Fact] public async Task Apply_MessageNotFound_ReturnsNull() { - MlReviewService service = Create(new TestPipelineStore(), new FakeKanjStore(), new FakeMlClient(), out _); + MlReviewService service = Create(new TestPipelineStore(), new FakeKanjStore(), new TestMlClient(), out _); MlApplyResult? result = await service.ApplyAsync(Dialog, 999, MlReviewService.ActionSpam, CancellationToken.None); diff --git a/src/core/tests/Deal.Tests.Unit/Modules/Pipeline/PipelineProcessingServiceTests.cs b/src/core/tests/Deal.Tests.Unit/Modules/Pipeline/PipelineProcessingServiceTests.cs index a0d8c0a..1e96f9a 100644 --- a/src/core/tests/Deal.Tests.Unit/Modules/Pipeline/PipelineProcessingServiceTests.cs +++ b/src/core/tests/Deal.Tests.Unit/Modules/Pipeline/PipelineProcessingServiceTests.cs @@ -168,7 +168,7 @@ public sealed class PipelineProcessingServiceTests [Fact] public async Task ReturnAsync_RecordNotFound_ReturnsNull() { - (PipelineProcessingService service, FakePipelineStore store, FakeMlClient ml) = Create(); + (PipelineProcessingService service, FakePipelineStore store, TestMlClient ml) = Create(); RejectReturnResultDto? result = await service.ReturnAsync("r_missing", string.Empty, CancellationToken.None); @@ -180,7 +180,7 @@ public sealed class PipelineProcessingServiceTests [Fact] public async Task ReturnAsync_AlreadyReturned_Returns400() { - (PipelineProcessingService service, FakePipelineStore store, FakeMlClient ml) = Create(); + (PipelineProcessingService service, FakePipelineStore store, TestMlClient ml) = Create(); store.SeedRejected(Rejected("r_1", returned: true, returnReason: "было")); RejectReturnResultDto? result = await service.ReturnAsync("r_1", "ещё раз", CancellationToken.None); @@ -195,7 +195,7 @@ public sealed class PipelineProcessingServiceTests [Fact] public async Task ReturnAsync_DuplicateSource_Returns400() { - (PipelineProcessingService service, FakePipelineStore store, FakeMlClient ml) = Create(); + (PipelineProcessingService service, FakePipelineStore store, TestMlClient ml) = Create(); store.SeedRejected(Rejected("r_1", decidedBy: "dup", stage: "dup")); RejectReturnResultDto? result = await service.ReturnAsync("r_1", string.Empty, CancellationToken.None); @@ -209,7 +209,7 @@ public sealed class PipelineProcessingServiceTests [Fact] public async Task ReturnAsync_EmptyText_Returns400() { - (PipelineProcessingService service, FakePipelineStore store, FakeMlClient ml) = Create(); + (PipelineProcessingService service, FakePipelineStore store, TestMlClient ml) = Create(); store.SeedRejected(Rejected("r_1", text: " ")); RejectReturnResultDto? result = await service.ReturnAsync("r_1", string.Empty, CancellationToken.None); @@ -223,7 +223,7 @@ public sealed class PipelineProcessingServiceTests [Fact] public async Task ReturnAsync_SpamStage_UnlearnsSpamMarksRecordAndEnqueuesForceRow() { - (PipelineProcessingService service, FakePipelineStore store, FakeMlClient ml) = Create(); + (PipelineProcessingService service, FakePipelineStore store, TestMlClient ml) = Create(); store.SeedRejected(Rejected( "r_1", text: " Продвижение в каналах ", @@ -270,7 +270,7 @@ public sealed class PipelineProcessingServiceTests [Fact] public async Task ReturnAsync_NonSpamStage_NoLearningPushButEnqueuesForceRow() { - (PipelineProcessingService service, FakePipelineStore store, FakeMlClient ml) = Create(); + (PipelineProcessingService service, FakePipelineStore store, TestMlClient ml) = Create(); store.SeedRejected(Rejected( "r_1", text: "Короткое сообщение", @@ -309,7 +309,7 @@ public sealed class PipelineProcessingServiceTests [Fact] public async Task ReturnAsync_NoSourceInfo_EnqueuesDirectlyWithDefaultHue() { - (PipelineProcessingService service, FakePipelineStore store, FakeMlClient ml) = Create(); + (PipelineProcessingService service, FakePipelineStore store, TestMlClient ml) = Create(); store.SeedRejected(Rejected( "r_old", text: "Старая запись без ссылки на исходное", @@ -382,11 +382,11 @@ public sealed class PipelineProcessingServiceTests // ─── Помощники ────────────────────────────────────────────────────────── // Сервис на общих фейках: очередь/отсев и ML-клиент одного сценария. - private static (PipelineProcessingService Service, FakePipelineStore Store, FakeMlClient Ml) Create() + private static (PipelineProcessingService Service, FakePipelineStore Store, TestMlClient Ml) Create() { FakePipelineStore store = new(); - FakeMlClient ml = new(); - PipelineProcessingService service = new(store, ml, new PipelineIngestService(store)); + TestMlClient ml = new(); + PipelineProcessingService service = new(store, ml.Client, new PipelineIngestService(store)); return (service, store, ml); } diff --git a/src/core/tests/Deal.Tests.Unit/Support/AdminTickOrchestratorTests.cs b/src/core/tests/Deal.Tests.Unit/Support/AdminTickOrchestratorTests.cs index 6e04a29..6a70087 100644 --- a/src/core/tests/Deal.Tests.Unit/Support/AdminTickOrchestratorTests.cs +++ b/src/core/tests/Deal.Tests.Unit/Support/AdminTickOrchestratorTests.cs @@ -3,6 +3,7 @@ using Deal.Api.Dtos; using Deal.Api.Events; using Deal.Api.Services; using Deal.Contracts.Integrations.Models; +using Deal.Tests.Unit.Support; using Deal.Modules.Kanban.Application.Models; using Deal.Modules.Kanban.Application.Services; using Deal.Modules.Pipeline.Application.Models; @@ -195,7 +196,7 @@ public sealed class AdminTickOrchestratorTests // Собирает оркестратор на общих фейках: StorageTickService и воркер делят KanjStore/Settings, // обработка и воркер — PipelineStore, напоминания — тот же KanjStore/Settings (CardsService); - // ML «не готов» (FakeMlClient.Predict не задан → сбой → «не уверен»). + // ML «не готов» (TestMlClient.Predict не задан → сбой → «не уверен»). // withThrowingQueueRead: true — чтение очереди воркером бросает (сценарий сбоя pump). // withThrowingReminderCheck: true — выборка due-напоминаний бросает (сценарий сбоя проверки). // Возвращает: Оркестратор, фейки и подписку канала тенанта. @@ -211,23 +212,23 @@ public sealed class AdminTickOrchestratorTests throw new InvalidOperationException("Тестовый сбой чтения очереди (ListAsync).")); } var kanjStore = new FakeKanjStore(); - var mlClient = new FakeMlClient(); + var mlClient = new TestMlClient(); var aiClassifier = new TestAiClassifier(); var rules = new IncomingRules(settings); var fieldsParser = new LocalFieldsParser(settings); var ingest = new PipelineIngestService(store.Store); - var processing = new PipelineProcessingService(store.Store, mlClient, ingest); + var processing = new PipelineProcessingService(store.Store, mlClient.Client, ingest); var composer = new CardComposer(kanjStore, settings); var writer = new PipelineCardWriter(kanjStore, store.Store, composer); var worker = new PipelineWorkerService( - store.Store, settings, rules, kanjStore, mlClient, aiClassifier.Classifier, processing, writer, fieldsParser); + store.Store, settings, rules, kanjStore, mlClient.Client, aiClassifier.Classifier, processing, writer, fieldsParser); var broker = new SseBroker(); var tickService = new StorageTickService(kanjStore, settings); var toastPublisher = new StorageToastPublisher(broker); var pumpGate = new PipelinePumpGate(); FakeKanjStore reminderStore = withThrowingReminderCheck ? new ThrowingDueKanjStore() : kanjStore; - var cardsService = new CardsService(reminderStore, settings, mlClient, new TestFileStorage().Storage); + var cardsService = new CardsService(reminderStore, settings, mlClient.Client, new TestFileStorage().Storage); var orchestrator = new AdminTickOrchestrator( tickService, processing, worker, cardsService, toastPublisher, broker, pumpGate, NullLogger.Instance); diff --git a/src/core/tests/Deal.Tests.Unit/Support/CardReclassifierTests.cs b/src/core/tests/Deal.Tests.Unit/Support/CardReclassifierTests.cs index 900cd3f..6123faa 100644 --- a/src/core/tests/Deal.Tests.Unit/Support/CardReclassifierTests.cs +++ b/src/core/tests/Deal.Tests.Unit/Support/CardReclassifierTests.cs @@ -1,5 +1,6 @@ using System.Text.Json; using Deal.Contracts.Integrations.Models; +using Deal.Tests.Unit.Support; using Deal.Modules.Cards.Application.Sources; using Deal.Modules.Kanban.Application.Models; using Deal.Modules.Kanban.Application.Services; @@ -23,7 +24,7 @@ public sealed class CardReclassifierTests CardReclassifier Reclassifier, FakeKanjStore Store, FakeSettingsStore Settings, - FakeMlClient MlClient, + TestMlClient MlClient, TestAiClassifier AiClassifier, ReclassifyGate Gate); @@ -33,14 +34,14 @@ public sealed class CardReclassifierTests { var settings = new FakeSettingsStore(); var store = new FakeKanjStore(); - var mlClient = new FakeMlClient(); + var mlClient = new TestMlClient(); var aiClassifier = new TestAiClassifier(); var fieldsParser = new LocalFieldsParser(settings); var composer = new CardComposer(store, settings); - var cardsService = new CardsService(store, settings, mlClient, new TestFileStorage().Storage); + var cardsService = new CardsService(store, settings, mlClient.Client, new TestFileStorage().Storage); var gate = new ReclassifyGate(); var reclassifier = new CardReclassifier( - store, settings, aiClassifier.Classifier, fieldsParser, composer, cardsService, mlClient, gate); + store, settings, 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 11ad5f7..fb54620 100644 --- a/src/core/tests/Deal.Tests.Unit/Support/CardsServiceFilesTests.cs +++ b/src/core/tests/Deal.Tests.Unit/Support/CardsServiceFilesTests.cs @@ -242,7 +242,7 @@ public sealed class CardsServiceFilesTests { var store = new FakeKanjStore(); var storage = new TestFileStorage(); - return (new CardsService(store, new FakeSettingsStore(), new FakeMlClient(), storage.Storage), store, storage); + return (new CardsService(store, new FakeSettingsStore(), 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 75765cb..8e2969d 100644 --- a/src/core/tests/Deal.Tests.Unit/Support/CardsServiceSelectedTests.cs +++ b/src/core/tests/Deal.Tests.Unit/Support/CardsServiceSelectedTests.cs @@ -531,12 +531,12 @@ public sealed class CardsServiceSelectedTests // ─── Хелперы ────────────────────────────────────────────────────────── - private static (CardsService Service, FakeKanjStore Store, FakeSettingsStore Settings, FakeMlClient Ml) Create() + private static (CardsService Service, FakeKanjStore Store, FakeSettingsStore Settings, TestMlClient Ml) Create() { var store = new FakeKanjStore(); var settings = new FakeSettingsStore(); - var ml = new FakeMlClient(); - return (new CardsService(store, settings, ml, new TestFileStorage().Storage), store, settings, ml); + var ml = new TestMlClient(); + return (new CardsService(store, settings, ml.Client, new TestFileStorage().Storage), store, settings, ml); } // Тело PATCH из пар «ключ → значение» (presence = наличие пары; null — явный JSON-null). diff --git a/src/core/tests/Deal.Tests.Unit/Support/PipelineWorkerSchedulerTests.cs b/src/core/tests/Deal.Tests.Unit/Support/PipelineWorkerSchedulerTests.cs index 6e70d73..06d1752 100644 --- a/src/core/tests/Deal.Tests.Unit/Support/PipelineWorkerSchedulerTests.cs +++ b/src/core/tests/Deal.Tests.Unit/Support/PipelineWorkerSchedulerTests.cs @@ -3,6 +3,7 @@ using Deal.Api.Events; using Deal.Api.Services; using Deal.Contracts.Integrations.Abstractions; using Deal.Contracts.Integrations.Models; +using Deal.Tests.Unit.Support; using Deal.Infrastructure.Data; using Deal.Modules.Kanban.Application.Abstractions; using Deal.Modules.Kanban.Application.Models; @@ -167,7 +168,7 @@ public sealed class PipelineWorkerSchedulerTests services.AddSingleton(); services.AddSingleton(tenants.Repository); services.AddSingleton(pumpGate); - services.AddSingleton(new FakeMlClient()); + services.AddSingleton(new TestMlClient().Client); services.AddSingleton(aiClassifier.Classifier); // Тенант-scoped адаптеры: фейк выбирает хранилище по ITenantContext, который цикл заполняет SetTenant // (эталон StorageTickSchedulerTests/ConnectionStringProvider.ForTenant). diff --git a/src/core/tests/Deal.Tests.Unit/Support/PipelineWorkerServiceTests.cs b/src/core/tests/Deal.Tests.Unit/Support/PipelineWorkerServiceTests.cs index 110df58..c053e42 100644 --- a/src/core/tests/Deal.Tests.Unit/Support/PipelineWorkerServiceTests.cs +++ b/src/core/tests/Deal.Tests.Unit/Support/PipelineWorkerServiceTests.cs @@ -1,5 +1,6 @@ using System.Text.Json; using Deal.Contracts.Integrations.Models; +using Deal.Tests.Unit.Support; using Deal.Modules.Kanban.Application.Models; using Deal.Modules.Pipeline.Application.Models; using Deal.Modules.Pipeline.Application.Parse; @@ -27,7 +28,7 @@ public sealed class PipelineWorkerServiceTests TestPipelineStore PipelineStore, FakeKanjStore KanjStore, FakeSettingsStore Settings, - FakeMlClient MlClient, + TestMlClient MlClient, TestAiClassifier AiClassifier); // Собирает контекст: дефолты настроек/досок, ML «не готов» (как LocalMlClient), ИИ-фильтр пропускает. @@ -38,15 +39,15 @@ public sealed class PipelineWorkerServiceTests var settings = new FakeSettingsStore(); var store = pipelineStore ?? new TestPipelineStore(); var kanjStore = new FakeKanjStore(); - var mlClient = new FakeMlClient { Predict = NotReadyPrediction() }; + var mlClient = new TestMlClient { Predict = NotReadyPrediction() }; var aiClassifier = new TestAiClassifier(); var rules = new IncomingRules(settings); var fieldsParser = new LocalFieldsParser(settings); - var processing = new PipelineProcessingService(store.Store, mlClient, new PipelineIngestService(store.Store)); + var processing = new PipelineProcessingService(store.Store, mlClient.Client, new PipelineIngestService(store.Store)); var composer = new CardComposer(kanjStore, settings); var writer = new PipelineCardWriter(kanjStore, store.Store, composer); var worker = new PipelineWorkerService( - store.Store, settings, rules, kanjStore, mlClient, aiClassifier.Classifier, processing, writer, fieldsParser); + store.Store, settings, rules, kanjStore, 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 22e6d47..2f582aa 100644 --- a/src/core/tests/Deal.Tests.Unit/Support/StorageTickSchedulerTests.cs +++ b/src/core/tests/Deal.Tests.Unit/Support/StorageTickSchedulerTests.cs @@ -322,7 +322,7 @@ public sealed class StorageTickSchedulerTests services.AddScoped(provider => storesByTenant[TenantOf(provider)]); services.AddScoped(provider => settingsByTenant[TenantOf(provider)]); services.AddScoped(provider => pipelineStoresByTenant[TenantOf(provider)].Store); - services.AddSingleton(new FakeMlClient()); + services.AddSingleton(new TestMlClient().Client); services.AddSingleton(new TestFileStorage().Storage); services.AddScoped(); services.AddScoped(); diff --git a/src/core/tests/Deal.Tests.Unit/Contracts/FakeMlClient.cs b/src/core/tests/Deal.Tests.Unit/Support/TestMlClient.cs similarity index 54% rename from src/core/tests/Deal.Tests.Unit/Contracts/FakeMlClient.cs rename to src/core/tests/Deal.Tests.Unit/Support/TestMlClient.cs index 38dde69..15ccccb 100644 --- a/src/core/tests/Deal.Tests.Unit/Contracts/FakeMlClient.cs +++ b/src/core/tests/Deal.Tests.Unit/Support/TestMlClient.cs @@ -1,15 +1,22 @@ using Deal.Contracts.Integrations.Abstractions; using Deal.Contracts.Integrations.Models; +using NSubstitute; -namespace Deal.Tests.Unit.Contracts; +namespace Deal.Tests.Unit.Support; /// -/// In-memory реализация для unit-тестов CardsService и воркера pump. +/// Подставка : сервисы получают NSubstitute-подставку (), +/// тесты управляют сценариями через свойства (как раньше — через фейк). /// -public sealed class FakeMlClient : IMlClient +public sealed class TestMlClient { private readonly List<(string Text, string Label, double Delta)> _pushed = []; + /// + /// Подставка порта ML-клиента (создаётся в конструкторе). + /// + public IMlClient Client { get; } + /// /// Ответ StatusAsync /// @@ -26,37 +33,30 @@ public sealed class FakeMlClient : IMlClient public int PredictCalls { get; private set; } /// - /// Обучающие сигналы, отправленные через + /// Обучающие сигналы, отправленные через /// public IReadOnlyList<(string Text, string Label, double Delta)> Pushed => _pushed.ToList(); - /// - public Task StatusAsync(CancellationToken ct) => Task.FromResult(Status); - - /// - public Task PushAsync( - string text, - string label, - double delta, - CancellationToken ct) + /// + /// Создаёт подставку с поведением по умолчанию. + /// + public TestMlClient() { - _pushed.Add((text, label, delta)); - return Task.CompletedTask; + Client = Substitute.For(); + Client.StatusAsync(Arg.Any()) + .Returns(ci => Status); + Client.When(c => c.PushAsync(Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any())) + .Do(ci => _pushed.Add((ci.ArgAt(0), ci.ArgAt(1), ci.ArgAt(2)))); + Client.PredictAsync(Arg.Any(), Arg.Any()) + .Returns(ci => + { + PredictCalls++; + return Predict ?? throw new NotSupportedException(); + }); + Client.ResetAsync(Arg.Any()) + .Returns(_ => throw new NotSupportedException()); } - /// - public Task PredictAsync(string text, CancellationToken ct) - { - PredictCalls++; - return Predict is null - ? throw new NotSupportedException() - : Task.FromResult(Predict); - } - - /// - public Task ResetAsync(CancellationToken ct) => - throw new NotSupportedException(); - // Статус по умолчанию: модель не готова, все счётчики 0 (форма MlStatusResponseDto). // Возвращает: Пустой статус (ml/ai/learning = 0), как у LocalMlClient до действий. private static MlStatusResponseDto DefaultStatus()