Перевести FakeMlClient на NSubstitute
Хелпер Support/TestMlClient: Status/Predict сценарии через свойства, Pushed журнал, PredictCalls через When/Do, ResetAsync бросает NotSupportedException. Потребители (17 файлов) перетипизированы на .Client, фейк удалён, тесты 1340 зелёные.
This commit is contained in:
@@ -104,7 +104,7 @@ public sealed class DiscoveryWorkerSchedulerTests
|
||||
var services = new ServiceCollection();
|
||||
services.AddSingleton<ITenantContext>(tenantContext);
|
||||
services.AddSingleton<ITenantRepository>(tenants.Repository);
|
||||
services.AddSingleton<IMlClient>(new FakeMlClient());
|
||||
services.AddSingleton<IMlClient>(new TestMlClient().Client);
|
||||
services.AddSingleton<IAiTools>(_ => TestAiTools.New());
|
||||
// Тенант-scoped адаптеры: фейк выбирает хранилище по ITenantContext, который цикл заполняет SetTenant
|
||||
// (эталон PipelineWorkerSchedulerTests/ConnectionStringProvider.ForTenant).
|
||||
|
||||
@@ -154,7 +154,7 @@ public sealed class RuntimeDepthsCollectorTests
|
||||
}).ToArray()).Repository);
|
||||
|
||||
services.AddScoped<IPipelineStore>(provider => pipelineByTenant[CurrentTenant(provider)]);
|
||||
services.AddScoped<IMlClient>(_ => new FakeMlClient());
|
||||
services.AddScoped<IMlClient>(_ => new TestMlClient().Client);
|
||||
services.AddScoped<PipelineIngestService>();
|
||||
services.AddScoped<PipelineProcessingService>();
|
||||
services.AddScoped<IMlLearningStore>(provider => outboxByTenant[CurrentTenant(provider)]);
|
||||
@@ -172,4 +172,4 @@ public sealed class RuntimeDepthsCollectorTests
|
||||
ITenantContext context = provider.GetRequiredService<ITenantContext>();
|
||||
return context.TenantId!.Value.Value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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) либо без них.
|
||||
|
||||
@@ -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<string, double>(), Hits: 3, Ready: true, Margin: 0.9,
|
||||
Terms: Array.Empty<string>(), 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).
|
||||
|
||||
@@ -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<IDiscoveryPacer>();
|
||||
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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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<BudgetedAiClassifier>.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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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) — для посева напоминаний в прошлом/будущем.
|
||||
|
||||
@@ -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<MlCandidateDto> 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<MlCandidateDto> 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<MlCandidateDto> 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<MlCandidateDto> 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);
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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<AdminTickOrchestrator>.Instance);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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 пуст).
|
||||
|
||||
@@ -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).
|
||||
|
||||
@@ -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<SseBroker>();
|
||||
services.AddSingleton<ITenantRepository>(tenants.Repository);
|
||||
services.AddSingleton(pumpGate);
|
||||
services.AddSingleton<IMlClient>(new FakeMlClient());
|
||||
services.AddSingleton<IMlClient>(new TestMlClient().Client);
|
||||
services.AddSingleton<IAiClassifier>(aiClassifier.Classifier);
|
||||
// Тенант-scoped адаптеры: фейк выбирает хранилище по ITenantContext, который цикл заполняет SetTenant
|
||||
// (эталон StorageTickSchedulerTests/ConnectionStringProvider.ForTenant).
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -322,7 +322,7 @@ public sealed class StorageTickSchedulerTests
|
||||
services.AddScoped<ICardStore>(provider => storesByTenant[TenantOf(provider)]);
|
||||
services.AddScoped<ISettingsStore>(provider => settingsByTenant[TenantOf(provider)]);
|
||||
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.AddScoped<PipelineIngestService>();
|
||||
services.AddScoped<PipelineProcessingService>();
|
||||
|
||||
+28
-28
@@ -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;
|
||||
|
||||
/// <summary>
|
||||
/// In-memory реализация <see cref="IMlClient"/> для unit-тестов CardsService и воркера pump.
|
||||
/// Подставка <see cref="IMlClient"/>: сервисы получают NSubstitute-подставку (<see cref="Client"/>),
|
||||
/// тесты управляют сценариями через свойства (как раньше — через фейк).
|
||||
/// </summary>
|
||||
public sealed class FakeMlClient : IMlClient
|
||||
public sealed class TestMlClient
|
||||
{
|
||||
private readonly List<(string Text, string Label, double Delta)> _pushed = [];
|
||||
|
||||
/// <summary>
|
||||
/// Подставка порта ML-клиента (создаётся в конструкторе).
|
||||
/// </summary>
|
||||
public IMlClient Client { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Ответ StatusAsync
|
||||
/// </summary>
|
||||
@@ -26,37 +33,30 @@ public sealed class FakeMlClient : IMlClient
|
||||
public int PredictCalls { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Обучающие сигналы, отправленные через <see cref="PushAsync"/>
|
||||
/// Обучающие сигналы, отправленные через <see cref="IMlClient.PushAsync"/>
|
||||
/// </summary>
|
||||
public IReadOnlyList<(string Text, string Label, double Delta)> Pushed => _pushed.ToList();
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<MlStatusResponseDto> StatusAsync(CancellationToken ct) => Task.FromResult(Status);
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task PushAsync(
|
||||
string text,
|
||||
string label,
|
||||
double delta,
|
||||
CancellationToken ct)
|
||||
/// <summary>
|
||||
/// Создаёт подставку с поведением по умолчанию.
|
||||
/// </summary>
|
||||
public TestMlClient()
|
||||
{
|
||||
_pushed.Add((text, label, delta));
|
||||
return Task.CompletedTask;
|
||||
Client = Substitute.For<IMlClient>();
|
||||
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).
|
||||
// Возвращает: Пустой статус (ml/ai/learning = 0), как у LocalMlClient до действий.
|
||||
private static MlStatusResponseDto DefaultStatus()
|
||||
Reference in New Issue
Block a user