Перевести тестовые фейки на моки NSubstitute #13
@@ -104,7 +104,7 @@ public sealed class DiscoveryWorkerSchedulerTests
|
|||||||
var services = new ServiceCollection();
|
var services = new ServiceCollection();
|
||||||
services.AddSingleton<ITenantContext>(tenantContext);
|
services.AddSingleton<ITenantContext>(tenantContext);
|
||||||
services.AddSingleton<ITenantRepository>(tenants.Repository);
|
services.AddSingleton<ITenantRepository>(tenants.Repository);
|
||||||
services.AddSingleton<IMlClient>(new FakeMlClient());
|
services.AddSingleton<IMlClient>(new TestMlClient().Client);
|
||||||
services.AddSingleton<IAiTools>(_ => TestAiTools.New());
|
services.AddSingleton<IAiTools>(_ => TestAiTools.New());
|
||||||
// Тенант-scoped адаптеры: фейк выбирает хранилище по ITenantContext, который цикл заполняет SetTenant
|
// Тенант-scoped адаптеры: фейк выбирает хранилище по ITenantContext, который цикл заполняет SetTenant
|
||||||
// (эталон PipelineWorkerSchedulerTests/ConnectionStringProvider.ForTenant).
|
// (эталон PipelineWorkerSchedulerTests/ConnectionStringProvider.ForTenant).
|
||||||
|
|||||||
@@ -154,7 +154,7 @@ public sealed class RuntimeDepthsCollectorTests
|
|||||||
}).ToArray()).Repository);
|
}).ToArray()).Repository);
|
||||||
|
|
||||||
services.AddScoped<IPipelineStore>(provider => pipelineByTenant[CurrentTenant(provider)]);
|
services.AddScoped<IPipelineStore>(provider => pipelineByTenant[CurrentTenant(provider)]);
|
||||||
services.AddScoped<IMlClient>(_ => new FakeMlClient());
|
services.AddScoped<IMlClient>(_ => new TestMlClient().Client);
|
||||||
services.AddScoped<PipelineIngestService>();
|
services.AddScoped<PipelineIngestService>();
|
||||||
services.AddScoped<PipelineProcessingService>();
|
services.AddScoped<PipelineProcessingService>();
|
||||||
services.AddScoped<IMlLearningStore>(provider => outboxByTenant[CurrentTenant(provider)]);
|
services.AddScoped<IMlLearningStore>(provider => outboxByTenant[CurrentTenant(provider)]);
|
||||||
|
|||||||
@@ -84,7 +84,7 @@ public sealed class CardsServiceTests
|
|||||||
[Fact]
|
[Fact]
|
||||||
public async Task Move_ToBoard_WritesJournalAndPush()
|
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.SeedBoard(Board("b_py", Rules(stack: new[] { "python" })));
|
||||||
store.SeedCard(Card("l_1", "inbox", sourceMsg: "Нужен Python-разработчик"));
|
store.SeedCard(Card("l_1", "inbox", sourceMsg: "Нужен Python-разработчик"));
|
||||||
|
|
||||||
@@ -105,7 +105,7 @@ public sealed class CardsServiceTests
|
|||||||
[Fact]
|
[Fact]
|
||||||
public async Task Move_ToInbox_WritesJournalWithoutPush()
|
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.SeedBoard(Board("b_py", Rules(stack: new[] { "python" })));
|
||||||
store.SeedCard(Card("l_1", "b_py", sourceMsg: "Нужен Python-разработчик", prevCol: "inbox"));
|
store.SeedCard(Card("l_1", "b_py", sourceMsg: "Нужен Python-разработчик", prevCol: "inbox"));
|
||||||
|
|
||||||
@@ -124,7 +124,7 @@ public sealed class CardsServiceTests
|
|||||||
[Fact]
|
[Fact]
|
||||||
public async Task Move_ToUnknownBoard_Returns400Text()
|
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-разработчик"));
|
store.SeedCard(Card("l_1", "inbox", sourceMsg: "Нужен Python-разработчик"));
|
||||||
|
|
||||||
CardResultDto result = await service.MoveDashboardCardAsync("l_1", "b_ghost", CancellationToken.None);
|
CardResultDto result = await service.MoveDashboardCardAsync("l_1", "b_ghost", CancellationToken.None);
|
||||||
@@ -148,7 +148,7 @@ public sealed class CardsServiceTests
|
|||||||
[Fact]
|
[Fact]
|
||||||
public async Task Move_CardMissing_ReturnsNullLead()
|
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"));
|
store.SeedBoard(Board("b_py"));
|
||||||
|
|
||||||
CardResultDto result = await service.MoveDashboardCardAsync("l_ghost", "b_py", CancellationToken.None);
|
CardResultDto result = await service.MoveDashboardCardAsync("l_ghost", "b_py", CancellationToken.None);
|
||||||
@@ -162,7 +162,7 @@ public sealed class CardsServiceTests
|
|||||||
[Fact]
|
[Fact]
|
||||||
public async Task Move_SameColumn_NoOpWithoutJournalAndPush()
|
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.SeedBoard(Board("b_py", Rules(stack: new[] { "python" })));
|
||||||
store.SeedCard(Card("l_1", "b_py", sourceMsg: "Нужен Python-разработчик", isNew: true));
|
store.SeedCard(Card("l_1", "b_py", sourceMsg: "Нужен Python-разработчик", isNew: true));
|
||||||
|
|
||||||
@@ -179,7 +179,7 @@ public sealed class CardsServiceTests
|
|||||||
[Fact]
|
[Fact]
|
||||||
public async Task Move_ToBoardWithoutRules_MatchHitsEmptyButPushSent()
|
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.SeedBoard(Board("b_free")); // правил нет
|
||||||
store.SeedCard(Card("l_1", "inbox", sourceMsg: "Любой текст"));
|
store.SeedCard(Card("l_1", "inbox", sourceMsg: "Любой текст"));
|
||||||
|
|
||||||
@@ -195,7 +195,7 @@ public sealed class CardsServiceTests
|
|||||||
[Fact]
|
[Fact]
|
||||||
public async Task Move_FromBoardToBoard_PrevColIsOldBoardAndPushNewLabel()
|
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_java", Rules(stack: new[] { "java" })));
|
||||||
store.SeedBoard(Board("b_py", Rules(stack: new[] { "python" })));
|
store.SeedBoard(Board("b_py", Rules(stack: new[] { "python" })));
|
||||||
store.SeedCard(Card("l_1", "b_java", sourceMsg: "Нужен Java-разработчик", prevCol: "inbox"));
|
store.SeedCard(Card("l_1", "b_java", sourceMsg: "Нужен Java-разработчик", prevCol: "inbox"));
|
||||||
@@ -214,7 +214,7 @@ public sealed class CardsServiceTests
|
|||||||
[Fact]
|
[Fact]
|
||||||
public async Task Move_EmptySourceText_UsesTitleForMatchHitsAndPush()
|
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.SeedBoard(Board("b_py", Rules(stack: new[] { "python" })));
|
||||||
store.SeedCard(Card("l_1", "inbox", title: "Middle Python-разработчик", sourceMsg: " "));
|
store.SeedCard(Card("l_1", "inbox", title: "Middle Python-разработчик", sourceMsg: " "));
|
||||||
|
|
||||||
@@ -228,7 +228,7 @@ public sealed class CardsServiceTests
|
|||||||
[Fact]
|
[Fact]
|
||||||
public async Task Move_EmptySourceAndTitle_JournalWithoutPush()
|
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.SeedBoard(Board("b_py"));
|
||||||
store.SeedCard(Card("l_1", "inbox"));
|
store.SeedCard(Card("l_1", "inbox"));
|
||||||
|
|
||||||
@@ -263,7 +263,7 @@ public sealed class CardsServiceTests
|
|||||||
[Fact]
|
[Fact]
|
||||||
public async Task Move_FromArchive_Returns400AndWritesNothing()
|
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.SeedBoard(Board("b_py"));
|
||||||
store.SeedCard(Card("l_1", KanbanColumns.Archive, sourceMsg: "Старая вакансия", prevCol: "b_py"));
|
store.SeedCard(Card("l_1", KanbanColumns.Archive, sourceMsg: "Старая вакансия", prevCol: "b_py"));
|
||||||
|
|
||||||
@@ -279,7 +279,7 @@ public sealed class CardsServiceTests
|
|||||||
[Fact]
|
[Fact]
|
||||||
public async Task Move_FromTrash_Returns400AndWritesNothing()
|
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.SeedBoard(Board("b_py"));
|
||||||
store.SeedCard(Card("l_1", KanbanColumns.Trash, sourceMsg: "Спам-текст", prevCol: "b_py"));
|
store.SeedCard(Card("l_1", KanbanColumns.Trash, sourceMsg: "Спам-текст", prevCol: "b_py"));
|
||||||
|
|
||||||
@@ -296,7 +296,7 @@ public sealed class CardsServiceTests
|
|||||||
[Fact]
|
[Fact]
|
||||||
public async Task Trash_FromInbox_MovesToTrashWithJournalAndSpamPush()
|
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));
|
store.SeedCard(Card("l_1", "inbox", sourceMsg: "Предлагаю услуги продвижения", isNew: true));
|
||||||
|
|
||||||
CardDto? result = await service.TrashCardAsync("l_1", CancellationToken.None);
|
CardDto? result = await service.TrashCardAsync("l_1", CancellationToken.None);
|
||||||
@@ -318,7 +318,7 @@ public sealed class CardsServiceTests
|
|||||||
[Fact]
|
[Fact]
|
||||||
public async Task Trash_AlreadyInTrash_NoOpWithoutJournalAndPush()
|
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: "Текст"));
|
store.SeedCard(Card("l_1", KanbanColumns.Trash, sourceMsg: "Текст"));
|
||||||
|
|
||||||
CardDto? result = await service.TrashCardAsync("l_1", CancellationToken.None);
|
CardDto? result = await service.TrashCardAsync("l_1", CancellationToken.None);
|
||||||
@@ -331,7 +331,7 @@ public sealed class CardsServiceTests
|
|||||||
[Fact]
|
[Fact]
|
||||||
public async Task Trash_FromArchive_WritesJournalWithoutPush()
|
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"));
|
store.SeedCard(Card("l_1", KanbanColumns.Archive, sourceMsg: "Старая карточка", prevCol: "b_py"));
|
||||||
|
|
||||||
CardDto? result = await service.TrashCardAsync("l_1", CancellationToken.None);
|
CardDto? result = await service.TrashCardAsync("l_1", CancellationToken.None);
|
||||||
@@ -346,7 +346,7 @@ public sealed class CardsServiceTests
|
|||||||
[Fact]
|
[Fact]
|
||||||
public async Task Trash_CardMissing_ReturnsNull()
|
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);
|
CardDto? result = await service.TrashCardAsync("l_ghost", CancellationToken.None);
|
||||||
|
|
||||||
@@ -358,7 +358,7 @@ public sealed class CardsServiceTests
|
|||||||
[Fact]
|
[Fact]
|
||||||
public async Task Restore_FromTrash_ToPrevColBoard_UnlearnsSpam()
|
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.SeedBoard(Board("b_py", Rules(stack: new[] { "python" })));
|
||||||
store.SeedCard(Card("l_1", KanbanColumns.Trash, sourceMsg: "Нужен Python-разработчик", prevCol: "b_py"));
|
store.SeedCard(Card("l_1", KanbanColumns.Trash, sourceMsg: "Нужен Python-разработчик", prevCol: "b_py"));
|
||||||
|
|
||||||
@@ -383,7 +383,7 @@ public sealed class CardsServiceTests
|
|||||||
[Fact]
|
[Fact]
|
||||||
public async Task Restore_FromArchive_NoPush()
|
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.SeedBoard(Board("b_py"));
|
||||||
store.SeedCard(Card("l_1", KanbanColumns.Archive, sourceMsg: "Старая карточка", prevCol: "b_py"));
|
store.SeedCard(Card("l_1", KanbanColumns.Archive, sourceMsg: "Старая карточка", prevCol: "b_py"));
|
||||||
|
|
||||||
@@ -399,7 +399,7 @@ public sealed class CardsServiceTests
|
|||||||
[Fact]
|
[Fact]
|
||||||
public async Task Restore_FromTrash_PrevColDeletedBoard_FallsBackToInbox()
|
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"));
|
store.SeedCard(Card("l_1", KanbanColumns.Trash, sourceMsg: "Текст", prevCol: "b_gone"));
|
||||||
|
|
||||||
string? back = await service.RestoreCardAsync("l_1", CancellationToken.None);
|
string? back = await service.RestoreCardAsync("l_1", CancellationToken.None);
|
||||||
@@ -618,7 +618,7 @@ public sealed class CardsServiceTests
|
|||||||
[Fact]
|
[Fact]
|
||||||
public async Task Counts_ColumnsNewAndMlStats()
|
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_1", "inbox", isNew: true));
|
||||||
store.SeedCard(Card("l_2", "inbox"));
|
store.SeedCard(Card("l_2", "inbox"));
|
||||||
store.SeedCard(Card("l_3", "b_py"));
|
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 store = new FakeKanjStore();
|
||||||
var settings = new FakeSettingsStore();
|
var settings = new FakeSettingsStore();
|
||||||
var ml = new FakeMlClient();
|
var ml = new TestMlClient();
|
||||||
return (new CardsService(store, settings, ml, new TestFileStorage().Storage), store, settings, ml);
|
return (new CardsService(store, settings, ml.Client, new TestFileStorage().Storage), store, settings, ml);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Доска с правилами (ContainerRulesDto) либо без них.
|
// Доска с правилами (ContainerRulesDto) либо без них.
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ public sealed class DiscoveryEvaluatorTests
|
|||||||
[Fact]
|
[Fact]
|
||||||
public async Task EvaluateMessage_MlSpam_NotFit()
|
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(
|
ml.Predict = new MlPredictResultDto(
|
||||||
Take: true, Label: "spam", Scores: new Dictionary<string, double>(), Hits: 3, Ready: true, Margin: 0.9,
|
Take: true, Label: "spam", Scores: new Dictionary<string, double>(), Hits: 3, Ready: true, Margin: 0.9,
|
||||||
Terms: Array.Empty<string>(), Type: null);
|
Terms: Array.Empty<string>(), Type: null);
|
||||||
@@ -46,7 +46,7 @@ public sealed class DiscoveryEvaluatorTests
|
|||||||
[Fact]
|
[Fact]
|
||||||
public async Task EvaluateMessage_AiFits_ReturnsAiVerdict()
|
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, "тема совпадает"));
|
ai.ReturnsFit(new AiEvaluateFitResultDto(Fit: true, "тема совпадает"));
|
||||||
|
|
||||||
DiscoveryMessageFit fit = await evaluator.EvaluateMessageAsync(
|
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);
|
=> 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();
|
var settings = new FakeSettingsStore();
|
||||||
settings.Preload(SettingsKeys.AiEnabled, aiEnabled ? "true" : "false");
|
settings.Preload(SettingsKeys.AiEnabled, aiEnabled ? "true" : "false");
|
||||||
settings.Preload(SettingsKeys.MlEnabled, mlEnabled ? "true" : "false");
|
settings.Preload(SettingsKeys.MlEnabled, mlEnabled ? "true" : "false");
|
||||||
var ml = new FakeMlClient();
|
var ml = new TestMlClient();
|
||||||
var ai = TestAiTools.New();
|
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).
|
// Задача как строка DiscTasks (адаптер-форма; нужны только keywords/description/threshold).
|
||||||
|
|||||||
@@ -432,7 +432,7 @@ public sealed class DiscoveryWorkerServiceTests
|
|||||||
var settings = new FakeSettingsStore();
|
var settings = new FakeSettingsStore();
|
||||||
settings.Preload(SettingsKeys.MlEnabled, "false");
|
settings.Preload(SettingsKeys.MlEnabled, "false");
|
||||||
settings.Preload(SettingsKeys.AiEnabled, "false");
|
settings.Preload(SettingsKeys.AiEnabled, "false");
|
||||||
var ml = new FakeMlClient();
|
var ml = new TestMlClient();
|
||||||
var ai = TestAiTools.New();
|
var ai = TestAiTools.New();
|
||||||
var log = new DiscoveryLogService(store.Store);
|
var log = new DiscoveryLogService(store.Store);
|
||||||
var blacklist = new DiscoveryBlacklistService(store.Store);
|
var blacklist = new DiscoveryBlacklistService(store.Store);
|
||||||
@@ -443,7 +443,7 @@ public sealed class DiscoveryWorkerServiceTests
|
|||||||
var pacer = Substitute.For<IDiscoveryPacer>();
|
var pacer = Substitute.For<IDiscoveryPacer>();
|
||||||
var gateway = new TestDiscoveryGateway();
|
var gateway = new TestDiscoveryGateway();
|
||||||
var worker = new DiscoveryWorkerService(
|
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);
|
return new Fixture(store, settings, gateway, pacer, worker, tasks);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -167,7 +167,7 @@ public sealed class PipelineWorkerGrpcAiTests
|
|||||||
var settings = new FakeSettingsStore();
|
var settings = new FakeSettingsStore();
|
||||||
var pipelineStore = new TestPipelineStore();
|
var pipelineStore = new TestPipelineStore();
|
||||||
var kanjStore = new FakeKanjStore();
|
var kanjStore = new FakeKanjStore();
|
||||||
var mlClient = new FakeMlClient { Predict = NotReadyPrediction() };
|
var mlClient = new TestMlClient { Predict = NotReadyPrediction() };
|
||||||
var rules = new IncomingRules(settings);
|
var rules = new IncomingRules(settings);
|
||||||
var fieldsParser = new LocalFieldsParser(settings);
|
var fieldsParser = new LocalFieldsParser(settings);
|
||||||
|
|
||||||
@@ -192,11 +192,11 @@ public sealed class PipelineWorkerGrpcAiTests
|
|||||||
NullLogger<BudgetedAiClassifier>.Instance)
|
NullLogger<BudgetedAiClassifier>.Instance)
|
||||||
: grpcClassifier;
|
: 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 composer = new CardComposer(kanjStore, settings);
|
||||||
var writer = new PipelineCardWriter(kanjStore, pipelineStore.Store, composer);
|
var writer = new PipelineCardWriter(kanjStore, pipelineStore.Store, composer);
|
||||||
var worker = new PipelineWorkerService(
|
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);
|
return new Context(worker, pipelineStore, kanjStore, settings, limits);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -82,7 +82,7 @@ public sealed class CardMoverTests
|
|||||||
private static (CardMover Mover, FakeKanjStore Store) Create()
|
private static (CardMover Mover, FakeKanjStore Store) Create()
|
||||||
{
|
{
|
||||||
var store = new FakeKanjStore();
|
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);
|
return (new CardMover(cardsService), store);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -195,7 +195,7 @@ public sealed class CardsServiceRemindersTests
|
|||||||
settings.Preload(SettingsKeys.RemindersEnabled, "false");
|
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) — для посева напоминаний в прошлом/будущем.
|
// Текущее время в epoch-мс (UTC) — для посева напоминаний в прошлом/будущем.
|
||||||
|
|||||||
@@ -21,13 +21,13 @@ public sealed class MlReviewServiceTests
|
|||||||
private static MlReviewService Create(
|
private static MlReviewService Create(
|
||||||
TestPipelineStore pipeline,
|
TestPipelineStore pipeline,
|
||||||
FakeKanjStore kanj,
|
FakeKanjStore kanj,
|
||||||
FakeMlClient ml,
|
TestMlClient ml,
|
||||||
out CardsService cards)
|
out CardsService cards)
|
||||||
{
|
{
|
||||||
var settings = new FakeSettingsStore();
|
var settings = new FakeSettingsStore();
|
||||||
cards = new CardsService(kanj, settings, ml, new TestFileStorage().Storage);
|
cards = new CardsService(kanj, settings, ml.Client, new TestFileStorage().Storage);
|
||||||
var processing = new PipelineProcessingService(pipeline.Store, ml, new PipelineIngestService(pipeline.Store));
|
var processing = new PipelineProcessingService(pipeline.Store, ml.Client, new PipelineIngestService(pipeline.Store));
|
||||||
return new MlReviewService(pipeline.Store, kanj, cards, processing, ml);
|
return new MlReviewService(pipeline.Store, kanj, cards, processing, ml.Client);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static QueueItemDto QueueRow(
|
private static QueueItemDto QueueRow(
|
||||||
@@ -78,7 +78,7 @@ public sealed class MlReviewServiceTests
|
|||||||
pipeline.SeedRejected(RejectedRow(102, "из отсева"));
|
pipeline.SeedRejected(RejectedRow(102, "из отсева"));
|
||||||
var kanj = new FakeKanjStore();
|
var kanj = new FakeKanjStore();
|
||||||
kanj.SeedCard(Card(103, "из карточки"));
|
kanj.SeedCard(Card(103, "из карточки"));
|
||||||
MlReviewService service = Create(pipeline, kanj, new FakeMlClient(), out _);
|
MlReviewService service = Create(pipeline, kanj, new TestMlClient(), out _);
|
||||||
|
|
||||||
IReadOnlyList<MlCandidateDto> items = await service.CandidatesAsync(Dialog, 10, CancellationToken.None);
|
IReadOnlyList<MlCandidateDto> items = await service.CandidatesAsync(Dialog, 10, CancellationToken.None);
|
||||||
|
|
||||||
@@ -97,7 +97,7 @@ public sealed class MlReviewServiceTests
|
|||||||
var pipeline = new TestPipelineStore();
|
var pipeline = new TestPipelineStore();
|
||||||
pipeline.SeedQueue(QueueRow(101, "нужный", Dialog));
|
pipeline.SeedQueue(QueueRow(101, "нужный", Dialog));
|
||||||
pipeline.SeedQueue(QueueRow(201, "другой", "d_2"));
|
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<MlCandidateDto> items = await service.CandidatesAsync(Dialog, 10, CancellationToken.None);
|
IReadOnlyList<MlCandidateDto> items = await service.CandidatesAsync(Dialog, 10, CancellationToken.None);
|
||||||
|
|
||||||
@@ -111,7 +111,7 @@ public sealed class MlReviewServiceTests
|
|||||||
var pipeline = new TestPipelineStore();
|
var pipeline = new TestPipelineStore();
|
||||||
pipeline.SeedQueue(QueueRow(101, "a", "d_1"));
|
pipeline.SeedQueue(QueueRow(101, "a", "d_1"));
|
||||||
pipeline.SeedQueue(QueueRow(201, "b", "d_2"));
|
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<MlCandidateDto> items = await service.CandidatesAsync(null, 10, CancellationToken.None);
|
IReadOnlyList<MlCandidateDto> items = await service.CandidatesAsync(null, 10, CancellationToken.None);
|
||||||
|
|
||||||
@@ -127,7 +127,7 @@ public sealed class MlReviewServiceTests
|
|||||||
pipeline.SeedQueue(QueueRow(100 + i, $"текст {i}"));
|
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<MlCandidateDto> items = await service.CandidatesAsync(Dialog, 2, CancellationToken.None);
|
IReadOnlyList<MlCandidateDto> items = await service.CandidatesAsync(Dialog, 2, CancellationToken.None);
|
||||||
|
|
||||||
@@ -139,7 +139,7 @@ public sealed class MlReviewServiceTests
|
|||||||
{
|
{
|
||||||
var pipeline = new TestPipelineStore();
|
var pipeline = new TestPipelineStore();
|
||||||
pipeline.SeedQueue(QueueRow(101, "текст"));
|
pipeline.SeedQueue(QueueRow(101, "текст"));
|
||||||
var ml = new FakeMlClient();
|
var ml = new TestMlClient();
|
||||||
MlReviewService service = Create(pipeline, new FakeKanjStore(), ml, out _);
|
MlReviewService service = Create(pipeline, new FakeKanjStore(), ml, out _);
|
||||||
|
|
||||||
MlApplyResult? result = await service.ApplyAsync(Dialog, 101, MlReviewService.ActionSkip, CancellationToken.None);
|
MlApplyResult? result = await service.ApplyAsync(Dialog, 101, MlReviewService.ActionSkip, CancellationToken.None);
|
||||||
@@ -156,7 +156,7 @@ public sealed class MlReviewServiceTests
|
|||||||
{
|
{
|
||||||
var kanj = new FakeKanjStore();
|
var kanj = new FakeKanjStore();
|
||||||
kanj.SeedCard(Card(101, "спамный текст"));
|
kanj.SeedCard(Card(101, "спамный текст"));
|
||||||
var ml = new FakeMlClient();
|
var ml = new TestMlClient();
|
||||||
MlReviewService service = Create(new TestPipelineStore(), kanj, ml, out _);
|
MlReviewService service = Create(new TestPipelineStore(), kanj, ml, out _);
|
||||||
|
|
||||||
MlApplyResult? result = await service.ApplyAsync(Dialog, 101, MlReviewService.ActionSpam, CancellationToken.None);
|
MlApplyResult? result = await service.ApplyAsync(Dialog, 101, MlReviewService.ActionSpam, CancellationToken.None);
|
||||||
@@ -174,7 +174,7 @@ public sealed class MlReviewServiceTests
|
|||||||
{
|
{
|
||||||
var pipeline = new TestPipelineStore();
|
var pipeline = new TestPipelineStore();
|
||||||
pipeline.SeedQueue(QueueRow(101, "рекламный текст"));
|
pipeline.SeedQueue(QueueRow(101, "рекламный текст"));
|
||||||
var ml = new FakeMlClient();
|
var ml = new TestMlClient();
|
||||||
MlReviewService service = Create(pipeline, new FakeKanjStore(), ml, out _);
|
MlReviewService service = Create(pipeline, new FakeKanjStore(), ml, out _);
|
||||||
|
|
||||||
MlApplyResult? result = await service.ApplyAsync(Dialog, 101, MlReviewService.ActionSpam, CancellationToken.None);
|
MlApplyResult? result = await service.ApplyAsync(Dialog, 101, MlReviewService.ActionSpam, CancellationToken.None);
|
||||||
@@ -201,7 +201,7 @@ public sealed class MlReviewServiceTests
|
|||||||
Kind = ContainerKinds.Board,
|
Kind = ContainerKinds.Board,
|
||||||
});
|
});
|
||||||
kanj.SeedCard(Card(101, "python разработчик"));
|
kanj.SeedCard(Card(101, "python разработчик"));
|
||||||
var ml = new FakeMlClient();
|
var ml = new TestMlClient();
|
||||||
MlReviewService service = Create(new TestPipelineStore(), kanj, ml, out _);
|
MlReviewService service = Create(new TestPipelineStore(), kanj, ml, out _);
|
||||||
|
|
||||||
MlApplyResult? result = await service.ApplyAsync(Dialog, 101, "board:b_py", CancellationToken.None);
|
MlApplyResult? result = await service.ApplyAsync(Dialog, 101, "board:b_py", CancellationToken.None);
|
||||||
@@ -218,7 +218,7 @@ public sealed class MlReviewServiceTests
|
|||||||
{
|
{
|
||||||
var kanj = new FakeKanjStore();
|
var kanj = new FakeKanjStore();
|
||||||
kanj.SeedCard(Card(101, "текст"));
|
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);
|
MlApplyResult? result = await service.ApplyAsync(Dialog, 101, "board:missing", CancellationToken.None);
|
||||||
|
|
||||||
@@ -232,7 +232,7 @@ public sealed class MlReviewServiceTests
|
|||||||
{
|
{
|
||||||
var pipeline = new TestPipelineStore();
|
var pipeline = new TestPipelineStore();
|
||||||
pipeline.SeedQueue(QueueRow(101, "текст"));
|
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);
|
MlApplyResult? result = await service.ApplyAsync(Dialog, 101, "что-то", CancellationToken.None);
|
||||||
|
|
||||||
@@ -244,7 +244,7 @@ public sealed class MlReviewServiceTests
|
|||||||
[Fact]
|
[Fact]
|
||||||
public async Task Apply_MessageNotFound_ReturnsNull()
|
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);
|
MlApplyResult? result = await service.ApplyAsync(Dialog, 999, MlReviewService.ActionSpam, CancellationToken.None);
|
||||||
|
|
||||||
|
|||||||
@@ -168,7 +168,7 @@ public sealed class PipelineProcessingServiceTests
|
|||||||
[Fact]
|
[Fact]
|
||||||
public async Task ReturnAsync_RecordNotFound_ReturnsNull()
|
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);
|
RejectReturnResultDto? result = await service.ReturnAsync("r_missing", string.Empty, CancellationToken.None);
|
||||||
|
|
||||||
@@ -180,7 +180,7 @@ public sealed class PipelineProcessingServiceTests
|
|||||||
[Fact]
|
[Fact]
|
||||||
public async Task ReturnAsync_AlreadyReturned_Returns400()
|
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: "было"));
|
store.SeedRejected(Rejected("r_1", returned: true, returnReason: "было"));
|
||||||
|
|
||||||
RejectReturnResultDto? result = await service.ReturnAsync("r_1", "ещё раз", CancellationToken.None);
|
RejectReturnResultDto? result = await service.ReturnAsync("r_1", "ещё раз", CancellationToken.None);
|
||||||
@@ -195,7 +195,7 @@ public sealed class PipelineProcessingServiceTests
|
|||||||
[Fact]
|
[Fact]
|
||||||
public async Task ReturnAsync_DuplicateSource_Returns400()
|
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"));
|
store.SeedRejected(Rejected("r_1", decidedBy: "dup", stage: "dup"));
|
||||||
|
|
||||||
RejectReturnResultDto? result = await service.ReturnAsync("r_1", string.Empty, CancellationToken.None);
|
RejectReturnResultDto? result = await service.ReturnAsync("r_1", string.Empty, CancellationToken.None);
|
||||||
@@ -209,7 +209,7 @@ public sealed class PipelineProcessingServiceTests
|
|||||||
[Fact]
|
[Fact]
|
||||||
public async Task ReturnAsync_EmptyText_Returns400()
|
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: " "));
|
store.SeedRejected(Rejected("r_1", text: " "));
|
||||||
|
|
||||||
RejectReturnResultDto? result = await service.ReturnAsync("r_1", string.Empty, CancellationToken.None);
|
RejectReturnResultDto? result = await service.ReturnAsync("r_1", string.Empty, CancellationToken.None);
|
||||||
@@ -223,7 +223,7 @@ public sealed class PipelineProcessingServiceTests
|
|||||||
[Fact]
|
[Fact]
|
||||||
public async Task ReturnAsync_SpamStage_UnlearnsSpamMarksRecordAndEnqueuesForceRow()
|
public async Task ReturnAsync_SpamStage_UnlearnsSpamMarksRecordAndEnqueuesForceRow()
|
||||||
{
|
{
|
||||||
(PipelineProcessingService service, FakePipelineStore store, FakeMlClient ml) = Create();
|
(PipelineProcessingService service, FakePipelineStore store, TestMlClient ml) = Create();
|
||||||
store.SeedRejected(Rejected(
|
store.SeedRejected(Rejected(
|
||||||
"r_1",
|
"r_1",
|
||||||
text: " Продвижение в каналах ",
|
text: " Продвижение в каналах ",
|
||||||
@@ -270,7 +270,7 @@ public sealed class PipelineProcessingServiceTests
|
|||||||
[Fact]
|
[Fact]
|
||||||
public async Task ReturnAsync_NonSpamStage_NoLearningPushButEnqueuesForceRow()
|
public async Task ReturnAsync_NonSpamStage_NoLearningPushButEnqueuesForceRow()
|
||||||
{
|
{
|
||||||
(PipelineProcessingService service, FakePipelineStore store, FakeMlClient ml) = Create();
|
(PipelineProcessingService service, FakePipelineStore store, TestMlClient ml) = Create();
|
||||||
store.SeedRejected(Rejected(
|
store.SeedRejected(Rejected(
|
||||||
"r_1",
|
"r_1",
|
||||||
text: "Короткое сообщение",
|
text: "Короткое сообщение",
|
||||||
@@ -309,7 +309,7 @@ public sealed class PipelineProcessingServiceTests
|
|||||||
[Fact]
|
[Fact]
|
||||||
public async Task ReturnAsync_NoSourceInfo_EnqueuesDirectlyWithDefaultHue()
|
public async Task ReturnAsync_NoSourceInfo_EnqueuesDirectlyWithDefaultHue()
|
||||||
{
|
{
|
||||||
(PipelineProcessingService service, FakePipelineStore store, FakeMlClient ml) = Create();
|
(PipelineProcessingService service, FakePipelineStore store, TestMlClient ml) = Create();
|
||||||
store.SeedRejected(Rejected(
|
store.SeedRejected(Rejected(
|
||||||
"r_old",
|
"r_old",
|
||||||
text: "Старая запись без ссылки на исходное",
|
text: "Старая запись без ссылки на исходное",
|
||||||
@@ -382,11 +382,11 @@ public sealed class PipelineProcessingServiceTests
|
|||||||
// ─── Помощники ──────────────────────────────────────────────────────────
|
// ─── Помощники ──────────────────────────────────────────────────────────
|
||||||
|
|
||||||
// Сервис на общих фейках: очередь/отсев и ML-клиент одного сценария.
|
// Сервис на общих фейках: очередь/отсев и ML-клиент одного сценария.
|
||||||
private static (PipelineProcessingService Service, FakePipelineStore Store, FakeMlClient Ml) Create()
|
private static (PipelineProcessingService Service, FakePipelineStore Store, TestMlClient Ml) Create()
|
||||||
{
|
{
|
||||||
FakePipelineStore store = new();
|
FakePipelineStore store = new();
|
||||||
FakeMlClient ml = new();
|
TestMlClient ml = new();
|
||||||
PipelineProcessingService service = new(store, ml, new PipelineIngestService(store));
|
PipelineProcessingService service = new(store, ml.Client, new PipelineIngestService(store));
|
||||||
return (service, store, ml);
|
return (service, store, ml);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ using Deal.Api.Dtos;
|
|||||||
using Deal.Api.Events;
|
using Deal.Api.Events;
|
||||||
using Deal.Api.Services;
|
using Deal.Api.Services;
|
||||||
using Deal.Contracts.Integrations.Models;
|
using Deal.Contracts.Integrations.Models;
|
||||||
|
using Deal.Tests.Unit.Support;
|
||||||
using Deal.Modules.Kanban.Application.Models;
|
using Deal.Modules.Kanban.Application.Models;
|
||||||
using Deal.Modules.Kanban.Application.Services;
|
using Deal.Modules.Kanban.Application.Services;
|
||||||
using Deal.Modules.Pipeline.Application.Models;
|
using Deal.Modules.Pipeline.Application.Models;
|
||||||
@@ -195,7 +196,7 @@ public sealed class AdminTickOrchestratorTests
|
|||||||
|
|
||||||
// Собирает оркестратор на общих фейках: StorageTickService и воркер делят KanjStore/Settings,
|
// Собирает оркестратор на общих фейках: StorageTickService и воркер делят KanjStore/Settings,
|
||||||
// обработка и воркер — PipelineStore, напоминания — тот же KanjStore/Settings (CardsService);
|
// обработка и воркер — PipelineStore, напоминания — тот же KanjStore/Settings (CardsService);
|
||||||
// ML «не готов» (FakeMlClient.Predict не задан → сбой → «не уверен»).
|
// ML «не готов» (TestMlClient.Predict не задан → сбой → «не уверен»).
|
||||||
// withThrowingQueueRead: true — чтение очереди воркером бросает (сценарий сбоя pump).
|
// withThrowingQueueRead: true — чтение очереди воркером бросает (сценарий сбоя pump).
|
||||||
// withThrowingReminderCheck: true — выборка due-напоминаний бросает (сценарий сбоя проверки).
|
// withThrowingReminderCheck: true — выборка due-напоминаний бросает (сценарий сбоя проверки).
|
||||||
// Возвращает: Оркестратор, фейки и подписку канала тенанта.
|
// Возвращает: Оркестратор, фейки и подписку канала тенанта.
|
||||||
@@ -211,23 +212,23 @@ public sealed class AdminTickOrchestratorTests
|
|||||||
throw new InvalidOperationException("Тестовый сбой чтения очереди (ListAsync)."));
|
throw new InvalidOperationException("Тестовый сбой чтения очереди (ListAsync)."));
|
||||||
}
|
}
|
||||||
var kanjStore = new FakeKanjStore();
|
var kanjStore = new FakeKanjStore();
|
||||||
var mlClient = new FakeMlClient();
|
var mlClient = new TestMlClient();
|
||||||
var aiClassifier = new TestAiClassifier();
|
var aiClassifier = new TestAiClassifier();
|
||||||
var rules = new IncomingRules(settings);
|
var rules = new IncomingRules(settings);
|
||||||
var fieldsParser = new LocalFieldsParser(settings);
|
var fieldsParser = new LocalFieldsParser(settings);
|
||||||
var ingest = new PipelineIngestService(store.Store);
|
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 composer = new CardComposer(kanjStore, settings);
|
||||||
var writer = new PipelineCardWriter(kanjStore, store.Store, composer);
|
var writer = new PipelineCardWriter(kanjStore, store.Store, composer);
|
||||||
var worker = new PipelineWorkerService(
|
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 broker = new SseBroker();
|
||||||
var tickService = new StorageTickService(kanjStore, settings);
|
var tickService = new StorageTickService(kanjStore, settings);
|
||||||
var toastPublisher = new StorageToastPublisher(broker);
|
var toastPublisher = new StorageToastPublisher(broker);
|
||||||
var pumpGate = new PipelinePumpGate();
|
var pumpGate = new PipelinePumpGate();
|
||||||
FakeKanjStore reminderStore = withThrowingReminderCheck ? new ThrowingDueKanjStore() : kanjStore;
|
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(
|
var orchestrator = new AdminTickOrchestrator(
|
||||||
tickService, processing, worker, cardsService, toastPublisher, broker, pumpGate,
|
tickService, processing, worker, cardsService, toastPublisher, broker, pumpGate,
|
||||||
NullLogger<AdminTickOrchestrator>.Instance);
|
NullLogger<AdminTickOrchestrator>.Instance);
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
using Deal.Contracts.Integrations.Models;
|
using Deal.Contracts.Integrations.Models;
|
||||||
|
using Deal.Tests.Unit.Support;
|
||||||
using Deal.Modules.Cards.Application.Sources;
|
using Deal.Modules.Cards.Application.Sources;
|
||||||
using Deal.Modules.Kanban.Application.Models;
|
using Deal.Modules.Kanban.Application.Models;
|
||||||
using Deal.Modules.Kanban.Application.Services;
|
using Deal.Modules.Kanban.Application.Services;
|
||||||
@@ -23,7 +24,7 @@ public sealed class CardReclassifierTests
|
|||||||
CardReclassifier Reclassifier,
|
CardReclassifier Reclassifier,
|
||||||
FakeKanjStore Store,
|
FakeKanjStore Store,
|
||||||
FakeSettingsStore Settings,
|
FakeSettingsStore Settings,
|
||||||
FakeMlClient MlClient,
|
TestMlClient MlClient,
|
||||||
TestAiClassifier AiClassifier,
|
TestAiClassifier AiClassifier,
|
||||||
ReclassifyGate Gate);
|
ReclassifyGate Gate);
|
||||||
|
|
||||||
@@ -33,14 +34,14 @@ public sealed class CardReclassifierTests
|
|||||||
{
|
{
|
||||||
var settings = new FakeSettingsStore();
|
var settings = new FakeSettingsStore();
|
||||||
var store = new FakeKanjStore();
|
var store = new FakeKanjStore();
|
||||||
var mlClient = new FakeMlClient();
|
var mlClient = new TestMlClient();
|
||||||
var aiClassifier = new TestAiClassifier();
|
var aiClassifier = new TestAiClassifier();
|
||||||
var fieldsParser = new LocalFieldsParser(settings);
|
var fieldsParser = new LocalFieldsParser(settings);
|
||||||
var composer = new CardComposer(store, 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 gate = new ReclassifyGate();
|
||||||
var reclassifier = new CardReclassifier(
|
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);
|
return new Context(reclassifier, store, settings, mlClient, aiClassifier, gate);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -242,7 +242,7 @@ public sealed class CardsServiceFilesTests
|
|||||||
{
|
{
|
||||||
var store = new FakeKanjStore();
|
var store = new FakeKanjStore();
|
||||||
var storage = new TestFileStorage();
|
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 пуст).
|
// Карточка с полями по умолчанию (planned, CreatedAtMs=1, UpdatedAtMs=1; Files пуст).
|
||||||
|
|||||||
@@ -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 store = new FakeKanjStore();
|
||||||
var settings = new FakeSettingsStore();
|
var settings = new FakeSettingsStore();
|
||||||
var ml = new FakeMlClient();
|
var ml = new TestMlClient();
|
||||||
return (new CardsService(store, settings, ml, new TestFileStorage().Storage), store, settings, ml);
|
return (new CardsService(store, settings, ml.Client, new TestFileStorage().Storage), store, settings, ml);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Тело PATCH из пар «ключ → значение» (presence = наличие пары; null — явный JSON-null).
|
// Тело PATCH из пар «ключ → значение» (presence = наличие пары; null — явный JSON-null).
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ using Deal.Api.Events;
|
|||||||
using Deal.Api.Services;
|
using Deal.Api.Services;
|
||||||
using Deal.Contracts.Integrations.Abstractions;
|
using Deal.Contracts.Integrations.Abstractions;
|
||||||
using Deal.Contracts.Integrations.Models;
|
using Deal.Contracts.Integrations.Models;
|
||||||
|
using Deal.Tests.Unit.Support;
|
||||||
using Deal.Infrastructure.Data;
|
using Deal.Infrastructure.Data;
|
||||||
using Deal.Modules.Kanban.Application.Abstractions;
|
using Deal.Modules.Kanban.Application.Abstractions;
|
||||||
using Deal.Modules.Kanban.Application.Models;
|
using Deal.Modules.Kanban.Application.Models;
|
||||||
@@ -167,7 +168,7 @@ public sealed class PipelineWorkerSchedulerTests
|
|||||||
services.AddSingleton<SseBroker>();
|
services.AddSingleton<SseBroker>();
|
||||||
services.AddSingleton<ITenantRepository>(tenants.Repository);
|
services.AddSingleton<ITenantRepository>(tenants.Repository);
|
||||||
services.AddSingleton(pumpGate);
|
services.AddSingleton(pumpGate);
|
||||||
services.AddSingleton<IMlClient>(new FakeMlClient());
|
services.AddSingleton<IMlClient>(new TestMlClient().Client);
|
||||||
services.AddSingleton<IAiClassifier>(aiClassifier.Classifier);
|
services.AddSingleton<IAiClassifier>(aiClassifier.Classifier);
|
||||||
// Тенант-scoped адаптеры: фейк выбирает хранилище по ITenantContext, который цикл заполняет SetTenant
|
// Тенант-scoped адаптеры: фейк выбирает хранилище по ITenantContext, который цикл заполняет SetTenant
|
||||||
// (эталон StorageTickSchedulerTests/ConnectionStringProvider.ForTenant).
|
// (эталон StorageTickSchedulerTests/ConnectionStringProvider.ForTenant).
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
using Deal.Contracts.Integrations.Models;
|
using Deal.Contracts.Integrations.Models;
|
||||||
|
using Deal.Tests.Unit.Support;
|
||||||
using Deal.Modules.Kanban.Application.Models;
|
using Deal.Modules.Kanban.Application.Models;
|
||||||
using Deal.Modules.Pipeline.Application.Models;
|
using Deal.Modules.Pipeline.Application.Models;
|
||||||
using Deal.Modules.Pipeline.Application.Parse;
|
using Deal.Modules.Pipeline.Application.Parse;
|
||||||
@@ -27,7 +28,7 @@ public sealed class PipelineWorkerServiceTests
|
|||||||
TestPipelineStore PipelineStore,
|
TestPipelineStore PipelineStore,
|
||||||
FakeKanjStore KanjStore,
|
FakeKanjStore KanjStore,
|
||||||
FakeSettingsStore Settings,
|
FakeSettingsStore Settings,
|
||||||
FakeMlClient MlClient,
|
TestMlClient MlClient,
|
||||||
TestAiClassifier AiClassifier);
|
TestAiClassifier AiClassifier);
|
||||||
|
|
||||||
// Собирает контекст: дефолты настроек/досок, ML «не готов» (как LocalMlClient), ИИ-фильтр пропускает.
|
// Собирает контекст: дефолты настроек/досок, ML «не готов» (как LocalMlClient), ИИ-фильтр пропускает.
|
||||||
@@ -38,15 +39,15 @@ public sealed class PipelineWorkerServiceTests
|
|||||||
var settings = new FakeSettingsStore();
|
var settings = new FakeSettingsStore();
|
||||||
var store = pipelineStore ?? new TestPipelineStore();
|
var store = pipelineStore ?? new TestPipelineStore();
|
||||||
var kanjStore = new FakeKanjStore();
|
var kanjStore = new FakeKanjStore();
|
||||||
var mlClient = new FakeMlClient { Predict = NotReadyPrediction() };
|
var mlClient = new TestMlClient { Predict = NotReadyPrediction() };
|
||||||
var aiClassifier = new TestAiClassifier();
|
var aiClassifier = new TestAiClassifier();
|
||||||
var rules = new IncomingRules(settings);
|
var rules = new IncomingRules(settings);
|
||||||
var fieldsParser = new LocalFieldsParser(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 composer = new CardComposer(kanjStore, settings);
|
||||||
var writer = new PipelineCardWriter(kanjStore, store.Store, composer);
|
var writer = new PipelineCardWriter(kanjStore, store.Store, composer);
|
||||||
var worker = new PipelineWorkerService(
|
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);
|
return new Context(worker, store, kanjStore, settings, mlClient, aiClassifier);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -322,7 +322,7 @@ public sealed class StorageTickSchedulerTests
|
|||||||
services.AddScoped<ICardStore>(provider => storesByTenant[TenantOf(provider)]);
|
services.AddScoped<ICardStore>(provider => storesByTenant[TenantOf(provider)]);
|
||||||
services.AddScoped<ISettingsStore>(provider => settingsByTenant[TenantOf(provider)]);
|
services.AddScoped<ISettingsStore>(provider => settingsByTenant[TenantOf(provider)]);
|
||||||
services.AddScoped<IPipelineStore>(provider => pipelineStoresByTenant[TenantOf(provider)].Store);
|
services.AddScoped<IPipelineStore>(provider => pipelineStoresByTenant[TenantOf(provider)].Store);
|
||||||
services.AddSingleton<IMlClient>(new FakeMlClient());
|
services.AddSingleton<IMlClient>(new TestMlClient().Client);
|
||||||
services.AddSingleton<IFileStorage>(new TestFileStorage().Storage);
|
services.AddSingleton<IFileStorage>(new TestFileStorage().Storage);
|
||||||
services.AddScoped<PipelineIngestService>();
|
services.AddScoped<PipelineIngestService>();
|
||||||
services.AddScoped<PipelineProcessingService>();
|
services.AddScoped<PipelineProcessingService>();
|
||||||
|
|||||||
+28
-28
@@ -1,15 +1,22 @@
|
|||||||
using Deal.Contracts.Integrations.Abstractions;
|
using Deal.Contracts.Integrations.Abstractions;
|
||||||
using Deal.Contracts.Integrations.Models;
|
using Deal.Contracts.Integrations.Models;
|
||||||
|
using NSubstitute;
|
||||||
|
|
||||||
namespace Deal.Tests.Unit.Contracts;
|
namespace Deal.Tests.Unit.Support;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// In-memory реализация <see cref="IMlClient"/> для unit-тестов CardsService и воркера pump.
|
/// Подставка <see cref="IMlClient"/>: сервисы получают NSubstitute-подставку (<see cref="Client"/>),
|
||||||
|
/// тесты управляют сценариями через свойства (как раньше — через фейк).
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public sealed class FakeMlClient : IMlClient
|
public sealed class TestMlClient
|
||||||
{
|
{
|
||||||
private readonly List<(string Text, string Label, double Delta)> _pushed = [];
|
private readonly List<(string Text, string Label, double Delta)> _pushed = [];
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Подставка порта ML-клиента (создаётся в конструкторе).
|
||||||
|
/// </summary>
|
||||||
|
public IMlClient Client { get; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Ответ StatusAsync
|
/// Ответ StatusAsync
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -26,37 +33,30 @@ public sealed class FakeMlClient : IMlClient
|
|||||||
public int PredictCalls { get; private set; }
|
public int PredictCalls { get; private set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Обучающие сигналы, отправленные через <see cref="PushAsync"/>
|
/// Обучающие сигналы, отправленные через <see cref="IMlClient.PushAsync"/>
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public IReadOnlyList<(string Text, string Label, double Delta)> Pushed => _pushed.ToList();
|
public IReadOnlyList<(string Text, string Label, double Delta)> Pushed => _pushed.ToList();
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <summary>
|
||||||
public Task<MlStatusResponseDto> StatusAsync(CancellationToken ct) => Task.FromResult(Status);
|
/// Создаёт подставку с поведением по умолчанию.
|
||||||
|
/// </summary>
|
||||||
/// <inheritdoc />
|
public TestMlClient()
|
||||||
public Task PushAsync(
|
|
||||||
string text,
|
|
||||||
string label,
|
|
||||||
double delta,
|
|
||||||
CancellationToken ct)
|
|
||||||
{
|
{
|
||||||
_pushed.Add((text, label, delta));
|
Client = Substitute.For<IMlClient>();
|
||||||
return Task.CompletedTask;
|
Client.StatusAsync(Arg.Any<CancellationToken>())
|
||||||
|
.Returns(ci => Status);
|
||||||
|
Client.When(c => c.PushAsync(Arg.Any<string>(), Arg.Any<string>(), Arg.Any<double>(), Arg.Any<CancellationToken>()))
|
||||||
|
.Do(ci => _pushed.Add((ci.ArgAt<string>(0), ci.ArgAt<string>(1), ci.ArgAt<double>(2))));
|
||||||
|
Client.PredictAsync(Arg.Any<string>(), Arg.Any<CancellationToken>())
|
||||||
|
.Returns<MlPredictResultDto>(ci =>
|
||||||
|
{
|
||||||
|
PredictCalls++;
|
||||||
|
return Predict ?? throw new NotSupportedException();
|
||||||
|
});
|
||||||
|
Client.ResetAsync(Arg.Any<CancellationToken>())
|
||||||
|
.Returns<MlResetResultDto>(_ => throw new NotSupportedException());
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public Task<MlPredictResultDto> PredictAsync(string text, CancellationToken ct)
|
|
||||||
{
|
|
||||||
PredictCalls++;
|
|
||||||
return Predict is null
|
|
||||||
? throw new NotSupportedException()
|
|
||||||
: Task.FromResult(Predict);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public Task<MlResetResultDto> ResetAsync(CancellationToken ct) =>
|
|
||||||
throw new NotSupportedException();
|
|
||||||
|
|
||||||
// Статус по умолчанию: модель не готова, все счётчики 0 (форма MlStatusResponseDto).
|
// Статус по умолчанию: модель не готова, все счётчики 0 (форма MlStatusResponseDto).
|
||||||
// Возвращает: Пустой статус (ml/ai/learning = 0), как у LocalMlClient до действий.
|
// Возвращает: Пустой статус (ml/ai/learning = 0), как у LocalMlClient до действий.
|
||||||
private static MlStatusResponseDto DefaultStatus()
|
private static MlStatusResponseDto DefaultStatus()
|
||||||
Reference in New Issue
Block a user