diff --git a/src/core/tests/Deal.Tests.Unit/Api/MlOutboxFlushSchedulerTests.cs b/src/core/tests/Deal.Tests.Unit/Api/MlOutboxFlushSchedulerTests.cs index d909057..ef50d41 100644 --- a/src/core/tests/Deal.Tests.Unit/Api/MlOutboxFlushSchedulerTests.cs +++ b/src/core/tests/Deal.Tests.Unit/Api/MlOutboxFlushSchedulerTests.cs @@ -39,14 +39,14 @@ public sealed class MlOutboxFlushSchedulerTests { await MlGrpcTestHost.RunAsync(MlGrpcTestHost.DefaultToken, new RecordingMlService(), async (port, service) => { - var store = new FakeMlLearningStore(); + var store = new TestMlLearningStore(); SeedRows(store, count: 25, prefix: "a"); var tenantContext = new TenantContext(); await using ServiceProvider provider = BuildProvider( port, new TestTenantRepository(Tenant(TenantA)), tenantContext, - new Dictionary { [TenantA] = store }); + new Dictionary { [TenantA] = store }); MlOutboxFlushScheduler scheduler = CreateScheduler(provider); await scheduler.RunCycleAsync(CancellationToken.None); @@ -54,7 +54,7 @@ public sealed class MlOutboxFlushSchedulerTests // 25 строк → 3 батча по 10/10/5 (ml_client.flush_outbox: chunk=10), строки удалены после успеха. Assert.Equal(3, service.TrainCalls); Assert.Equal(new[] { 10, 10, 5 }, service.TrainBatches.Select(batch => batch.Items.Count).ToArray()); - Assert.Equal(0, await store.CountOutboxAsync(CancellationToken.None)); + Assert.Equal(0, await store.Store.CountOutboxAsync(CancellationToken.None)); Assert.False(tenantContext.HasTenant); // контекст AsyncLocal не переживает проход }); } @@ -65,23 +65,23 @@ public sealed class MlOutboxFlushSchedulerTests await MlGrpcTestHost.RunAsync(MlGrpcTestHost.DefaultToken, new RecordingMlService(), async (port, service) => { service.TrainUnavailable = true; - var store = new FakeMlLearningStore(); + var store = new TestMlLearningStore(); SeedRows(store, count: 5, prefix: "a"); var tenantContext = new TenantContext(); await using ServiceProvider provider = BuildProvider( port, new TestTenantRepository(Tenant(TenantA)), tenantContext, - new Dictionary { [TenantA] = store }); + new Dictionary { [TenantA] = store }); MlOutboxFlushScheduler scheduler = CreateScheduler(provider); await scheduler.RunCycleAsync(CancellationToken.None); - Assert.Equal(5, await store.CountOutboxAsync(CancellationToken.None)); // строки остались + Assert.Equal(5, await store.Store.CountOutboxAsync(CancellationToken.None)); // строки остались // Следующий цикл — повторная попытка (ретрай на каждом тике; строки снова не удалены). await scheduler.RunCycleAsync(CancellationToken.None); Assert.Equal(2, service.TrainCalls); - Assert.Equal(5, await store.CountOutboxAsync(CancellationToken.None)); + Assert.Equal(5, await store.Store.CountOutboxAsync(CancellationToken.None)); Assert.False(tenantContext.HasTenant); }); } @@ -91,16 +91,16 @@ public sealed class MlOutboxFlushSchedulerTests { await MlGrpcTestHost.RunAsync(MlGrpcTestHost.DefaultToken, new RecordingMlService(), async (port, service) => { - var storeA = new FakeMlLearningStore(); + var storeA = new TestMlLearningStore(); SeedRows(storeA, count: 12, prefix: "a"); - var storeB = new FakeMlLearningStore(); + var storeB = new TestMlLearningStore(); SeedRows(storeB, count: 3, prefix: "b"); var tenantContext = new TenantContext(); await using ServiceProvider provider = BuildProvider( port, new TestTenantRepository(Tenant(TenantA), Tenant(TenantB)), tenantContext, - new Dictionary { [TenantA] = storeA, [TenantB] = storeB }); + new Dictionary { [TenantA] = storeA, [TenantB] = storeB }); MlOutboxFlushScheduler scheduler = CreateScheduler(provider); await scheduler.RunCycleAsync(CancellationToken.None); @@ -108,8 +108,8 @@ public sealed class MlOutboxFlushSchedulerTests // Каждый тенант выгрузил свою очередь в собственном scope (A: 10+2, B: 3) — строки удалены. Assert.Equal(3, service.TrainCalls); Assert.Equal(new[] { 10, 2, 3 }, service.TrainBatches.Select(batch => batch.Items.Count).ToArray()); - Assert.Equal(0, await storeA.CountOutboxAsync(CancellationToken.None)); - Assert.Equal(0, await storeB.CountOutboxAsync(CancellationToken.None)); + Assert.Equal(0, await storeA.Store.CountOutboxAsync(CancellationToken.None)); + Assert.Equal(0, await storeB.Store.CountOutboxAsync(CancellationToken.None)); Assert.False(tenantContext.HasTenant); }); } @@ -119,20 +119,20 @@ public sealed class MlOutboxFlushSchedulerTests { await MlGrpcTestHost.RunAsync(MlGrpcTestHost.DefaultToken, new RecordingMlService(), async (port, service) => { - var store = new FakeMlLearningStore(); + var store = new TestMlLearningStore(); SeedRows(store, count: 105, prefix: "a"); var tenantContext = new TenantContext(); await using ServiceProvider provider = BuildProvider( port, new TestTenantRepository(Tenant(TenantA)), tenantContext, - new Dictionary { [TenantA] = store }); + new Dictionary { [TenantA] = store }); MlOutboxFlushScheduler scheduler = CreateScheduler(provider); await scheduler.RunCycleAsync(CancellationToken.None); Assert.Equal(10, service.TrainCalls); - Assert.Equal(5, await store.CountOutboxAsync(CancellationToken.None)); + Assert.Equal(5, await store.Store.CountOutboxAsync(CancellationToken.None)); }); } @@ -149,7 +149,7 @@ public sealed class MlOutboxFlushSchedulerTests int port, TestTenantRepository tenants, TenantContext tenantContext, - Dictionary storesByTenant) + Dictionary storesByTenant) { var services = new ServiceCollection(); services.AddSingleton(tenantContext); @@ -162,7 +162,7 @@ public sealed class MlOutboxFlushSchedulerTests services.AddScoped(provider => provider.GetRequiredService()); services.AddScoped(provider => provider.GetRequiredService()); // Tenant-scoped адаптеры: выбирают фейк по тому же ITenantContext, который планировщик заполняет SetTenant. - services.AddScoped(provider => storesByTenant[TenantOf(provider)]); + services.AddScoped(provider => storesByTenant[TenantOf(provider)].Store); services.AddScoped(_ => new TestSettingsStore().Store); services.AddScoped(_ => new TestTenantLimitStore().Store); services.AddScoped(_ => new TokenUsageEventService(new TestTokenUsageEventStore().Store)); @@ -196,7 +196,7 @@ public sealed class MlOutboxFlushSchedulerTests // count: Число строк. // prefix: Префикс id (различает тенантов теста). private static void SeedRows( - FakeMlLearningStore store, + TestMlLearningStore store, int count, string prefix) { diff --git a/src/core/tests/Deal.Tests.Unit/Api/RuntimeDepthsCollectorTests.cs b/src/core/tests/Deal.Tests.Unit/Api/RuntimeDepthsCollectorTests.cs index f2dba86..98cc00e 100644 --- a/src/core/tests/Deal.Tests.Unit/Api/RuntimeDepthsCollectorTests.cs +++ b/src/core/tests/Deal.Tests.Unit/Api/RuntimeDepthsCollectorTests.cs @@ -38,10 +38,10 @@ public sealed class RuntimeDepthsCollectorTests pipelineByTenant[TenantA.ToString("N")].SeedQueue(QueueItem("p_a2")); pipelineByTenant[TenantB.ToString("N")].SeedQueue(QueueItem("p_b1")); - var outboxByTenant = new Dictionary + var outboxByTenant = new Dictionary { - [TenantA.ToString("N")] = new FakeMlLearningStore(), - [TenantB.ToString("N")] = new FakeMlLearningStore(), + [TenantA.ToString("N")] = new TestMlLearningStore(), + [TenantB.ToString("N")] = new TestMlLearningStore(), }; outboxByTenant[TenantA.ToString("N")].SeedOutbox("mle_1", "текст", "spam"); @@ -106,7 +106,7 @@ public sealed class RuntimeDepthsCollectorTests { RuntimeDepthsCollector collector = Build( new Dictionary(), - new Dictionary(), + new Dictionary(), tenants: Array.Empty()); RuntimeDepthsDto depths = await collector.CollectAsync(CancellationToken.None); @@ -126,8 +126,8 @@ public sealed class RuntimeDepthsCollectorTests private static Dictionary PipelineStores(params Guid[] tenants) => tenants.ToDictionary(tenant => tenant.ToString("N"), _ => new FakePipelineStore()); - private static Dictionary OutboxStores(params Guid[] tenants) - => tenants.ToDictionary(tenant => tenant.ToString("N"), _ => new FakeMlLearningStore()); + private static Dictionary OutboxStores(params Guid[] tenants) + => tenants.ToDictionary(tenant => tenant.ToString("N"), _ => new TestMlLearningStore()); private static TenantRecordDto[] TenantRecords(params Guid[] tenants) => tenants @@ -140,7 +140,7 @@ public sealed class RuntimeDepthsCollectorTests // Собирает коллектор поверх tenant-scoped фейков (как реальные адаптеры по ITenantContext). private static RuntimeDepthsCollector Build( IReadOnlyDictionary pipelineByTenant, - IReadOnlyDictionary outboxByTenant, + IReadOnlyDictionary outboxByTenant, IReadOnlyList? tenants = null, ITenantLimitStore? limitStore = null) { @@ -157,7 +157,7 @@ public sealed class RuntimeDepthsCollectorTests services.AddScoped(_ => new TestMlClient().Client); services.AddScoped(); services.AddScoped(); - services.AddScoped(provider => outboxByTenant[CurrentTenant(provider)]); + services.AddScoped(provider => outboxByTenant[CurrentTenant(provider)].Store); services.AddSingleton(limitStore ?? new TestTenantLimitStore().Store); ServiceProvider provider = services.BuildServiceProvider(); diff --git a/src/core/tests/Deal.Tests.Unit/Contracts/GrpcMlClientTests.cs b/src/core/tests/Deal.Tests.Unit/Contracts/GrpcMlClientTests.cs index feaf56d..484de59 100644 --- a/src/core/tests/Deal.Tests.Unit/Contracts/GrpcMlClientTests.cs +++ b/src/core/tests/Deal.Tests.Unit/Contracts/GrpcMlClientTests.cs @@ -113,7 +113,7 @@ public sealed class GrpcMlClientTests var settings = new TestSettingsStore(); settings.Preload(SettingsKeys.MlDecisions, "7"); settings.Preload(SettingsKeys.AiDecisions, "3"); - var learning = new FakeMlLearningStore { LearningCount = 9 }; + var learning = new TestMlLearningStore { LearningCount = 9 }; IMlClient client = CreateClient(port, settings, learning); MlStatusResponseDto status = await client.StatusAsync(CancellationToken.None); @@ -195,7 +195,7 @@ public sealed class GrpcMlClientTests { await MlGrpcTestHost.RunAsync(MlGrpcTestHost.DefaultToken, new RecordingMlService(), async (port, service) => { - var learning = new FakeMlLearningStore(); + var learning = new TestMlLearningStore(); learning.SeedOutbox("mle_1", "текст 1", "b_a", 1.0); learning.SeedOutbox("mle_2", "текст 2", "spam", 1.0); IMlClient client = CreateClient(port, learning: learning); @@ -206,7 +206,7 @@ public sealed class GrpcMlClientTests Assert.True(reset.Ok); Assert.Null(reset.Error); - Assert.Equal(0, await learning.CountOutboxAsync(CancellationToken.None)); // очередь очищена + Assert.Equal(0, await learning.Store.CountOutboxAsync(CancellationToken.None)); // очередь очищена Assert.Equal(1, service.ResetCalls); _ = await client.StatusAsync(CancellationToken.None); @@ -220,7 +220,7 @@ public sealed class GrpcMlClientTests await MlGrpcTestHost.RunAsync(MlGrpcTestHost.DefaultToken, new RecordingMlService(), async (port, service) => { service.ResetReply = new ResetReply { Ok = false, Error = "не удалось пересоздать файл модели" }; - var learning = new FakeMlLearningStore(); + var learning = new TestMlLearningStore(); learning.SeedOutbox("mle_1", "текст", "b_a", 1.0); IMlClient client = CreateClient(port, learning: learning); @@ -228,7 +228,7 @@ public sealed class GrpcMlClientTests Assert.False(reset.Ok); Assert.Equal("не удалось пересоздать файл модели", reset.Error); - Assert.Equal(1, await learning.CountOutboxAsync(CancellationToken.None)); + Assert.Equal(1, await learning.Store.CountOutboxAsync(CancellationToken.None)); }); } @@ -238,7 +238,7 @@ public sealed class GrpcMlClientTests await MlGrpcTestHost.RunAsync(MlGrpcTestHost.DefaultToken, new RecordingMlService(), async (port, service) => { service.ResetUnavailable = true; - var learning = new FakeMlLearningStore(); + var learning = new TestMlLearningStore(); learning.SeedOutbox("mle_1", "текст", "spam", 1.0); IMlClient client = CreateClient(port, learning: learning); @@ -246,7 +246,7 @@ public sealed class GrpcMlClientTests Assert.False(reset.Ok); Assert.Equal("ML-сервис недоступен", reset.Error); - Assert.Equal(1, await learning.CountOutboxAsync(CancellationToken.None)); + Assert.Equal(1, await learning.Store.CountOutboxAsync(CancellationToken.None)); }); } @@ -256,7 +256,7 @@ public sealed class GrpcMlClientTests { await MlGrpcTestHost.RunAsync(MlGrpcTestHost.DefaultToken, new RecordingMlService(), async (port, service) => { - var learning = new FakeMlLearningStore(); + var learning = new TestMlLearningStore(); IMlClient client = CreateClient(port, learning: learning); await client.PushAsync(" нужен python ", "b_junior", 1.0, CancellationToken.None); @@ -310,17 +310,18 @@ public sealed class GrpcMlClientTests private static GrpcMlClient CreateClient( int port, TestSettingsStore? settings = null, - FakeMlLearningStore? learning = null, + TestMlLearningStore? learning = null, MlStatusCache? cache = null) { settings ??= new TestSettingsStore(); + learning ??= new TestMlLearningStore(); ITenantContext tenantContext = new TenantContext(); tenantContext.SetTenant(new TenantId(TenantIdValue)); var options = new MlServiceOptions { UseLocal = false, Endpoint = $"http://127.0.0.1:{port}" }; return new GrpcMlClient( tenantContext, settings.Store, - learning ?? new FakeMlLearningStore(), + learning.Store, new MlGrpcConnection(options), cache ?? new MlStatusCache(), new TokenUsageRecorder( diff --git a/src/core/tests/Deal.Tests.Unit/Contracts/IntegrationsDiTests.cs b/src/core/tests/Deal.Tests.Unit/Contracts/IntegrationsDiTests.cs index 129e462..e2957c6 100644 --- a/src/core/tests/Deal.Tests.Unit/Contracts/IntegrationsDiTests.cs +++ b/src/core/tests/Deal.Tests.Unit/Contracts/IntegrationsDiTests.cs @@ -147,7 +147,7 @@ public sealed class IntegrationsDiTests services.AddScoped(_ => new TestSettingsStore().Store); services.AddScoped(_ => TestCiphers.New()); services.AddScoped(_ => new FakeKanjStore()); - services.AddScoped(_ => new FakeMlLearningStore()); + services.AddScoped(_ => new TestMlLearningStore().Store); services.AddScoped(_ => new TestTenantLimitStore().Store); services.AddScoped(_ => new TokenUsageEventService(new TestTokenUsageEventStore().Store)); services.AddScoped(); diff --git a/src/core/tests/Deal.Tests.Unit/Modules/Kanban/FakeMlLearningStore.cs b/src/core/tests/Deal.Tests.Unit/Modules/Kanban/FakeMlLearningStore.cs deleted file mode 100644 index 43fc9f0..0000000 --- a/src/core/tests/Deal.Tests.Unit/Modules/Kanban/FakeMlLearningStore.cs +++ /dev/null @@ -1,79 +0,0 @@ -using Deal.Modules.Kanban.Application.Abstractions; -using Deal.Modules.Kanban.Application.Models; - -namespace Deal.Tests.Unit.Modules.Kanban; - -/// -/// In-memory реализация для unit-тестов ML-интеграции. -/// -public sealed class FakeMlLearningStore : IMlLearningStore -{ - private readonly List<(string Id, string Text, string Label, double Delta)> _rows = []; - - /// - /// Число записей журнала обучения - /// - public int LearningCount { get; set; } - - /// - /// Строки очереди - /// - public IReadOnlyList<(string Id, string Text, string Label, double Delta)> AddedRows => _rows.ToList(); - - /// - public Task CountLearningAsync(CancellationToken ct) => Task.FromResult(LearningCount); - - /// - public Task CountOutboxAsync(CancellationToken ct) => Task.FromResult(_rows.Count); - - /// - public Task AddOutboxAsync( - string id, - string text, - string label, - double delta, - CancellationToken ct) - { - _rows.Add((id, text, label, delta)); - return Task.CompletedTask; - } - - /// - /// Пишет строку очереди напрямую - /// - /// Id строки (mle_...). - /// Текст обучающего примера. - /// Метка обучения. - /// Вес сигнала. - public void SeedOutbox( - string id, - string text, - string label, - double delta = 1.0) - { - _rows.Add((id, text, label, delta)); - } - - /// - public Task ClearOutboxAsync(CancellationToken ct) - { - _rows.Clear(); - return Task.CompletedTask; - } - - /// - public Task> TakeOutboxBatchAsync(int limit, CancellationToken ct) - { - return Task.FromResult>( - _rows.Take(limit) - .Select(row => new MlOutboxEntryDto(row.Id, row.Text, row.Label, row.Delta)) - .ToList()); - } - - /// - public Task DeleteOutboxAsync(IReadOnlyCollection ids, CancellationToken ct) - { - _rows.RemoveAll(row => ids.Contains(row.Id)); - return Task.CompletedTask; - } -} diff --git a/src/core/tests/Deal.Tests.Unit/Support/LocalMlClientTests.cs b/src/core/tests/Deal.Tests.Unit/Support/LocalMlClientTests.cs index f090de9..aa3e59f 100644 --- a/src/core/tests/Deal.Tests.Unit/Support/LocalMlClientTests.cs +++ b/src/core/tests/Deal.Tests.Unit/Support/LocalMlClientTests.cs @@ -28,8 +28,11 @@ public sealed class LocalMlClientTests // store: Хранилище настроек тенанта. // learning: Хранилище обучения (по умолчанию пустое: счётчики 0). // Возвращает: Экземпляр LocalMlClient. - private static LocalMlClient CreateClient(TestSettingsStore store, FakeMlLearningStore? learning = null) - => new(store.Store, learning ?? new FakeMlLearningStore()); + private static LocalMlClient CreateClient(TestSettingsStore store, TestMlLearningStore? learning = null) + { + learning ??= new TestMlLearningStore(); + return new(store.Store, learning.Store); + } [Fact] @@ -126,7 +129,7 @@ public sealed class LocalMlClientTests public async Task StatusAsync_ReportsLearningAndOutboxFromTables() { var store = new TestSettingsStore(); - var learning = new FakeMlLearningStore { LearningCount = 3 }; // журнал CardMoves: 3 записи + var learning = new TestMlLearningStore { LearningCount = 3 }; // журнал CardMoves: 3 записи LocalMlClient client = CreateClient(store, learning); await client.PushAsync("Разработчик Python, готов к проекту", "b_alpha", 1.0, CancellationToken.None); await client.PushAsync("Спам: купите курсы", "spam", -1.0, CancellationToken.None); @@ -162,7 +165,7 @@ public sealed class LocalMlClientTests [Fact] public async Task PushAsync_WritesOutboxRow_WithMleIdAndTrimmedFields() { - var learning = new FakeMlLearningStore(); + var learning = new TestMlLearningStore(); LocalMlClient client = CreateClient(new TestSettingsStore(), learning); await client.PushAsync(" Python-разработчик, готов к работе ", " b_alpha ", 1.0, CancellationToken.None); @@ -182,7 +185,7 @@ public sealed class LocalMlClientTests [InlineData("текст", " ")] // label из пробелов — пуста после trim public async Task PushAsync_EmptyTextOrLabelAfterTrim_IsNoOp(string text, string label) { - var learning = new FakeMlLearningStore(); + var learning = new TestMlLearningStore(); LocalMlClient client = CreateClient(new TestSettingsStore(), learning); await client.PushAsync(text, label, 1.0, CancellationToken.None); @@ -193,7 +196,7 @@ public sealed class LocalMlClientTests [Fact] public async Task PushAsync_TextOver6000_IsTruncatedToMaxLength() { - var learning = new FakeMlLearningStore(); + var learning = new TestMlLearningStore(); LocalMlClient client = CreateClient(new TestSettingsStore(), learning); string longText = new string('а', MaxLearningTextLength + 1000); @@ -207,7 +210,7 @@ public sealed class LocalMlClientTests [Fact] public async Task PushAsync_TruncationDoesNotSplitSurrogatePair() { - var learning = new FakeMlLearningStore(); + var learning = new TestMlLearningStore(); LocalMlClient client = CreateClient(new TestSettingsStore(), learning); // Длина 6001 в UTF-16: 5999 'x' + «🙂» (суррогатная пара) — срез .NET на 6000 попадает в пару. string text = new string('x', MaxLearningTextLength - 1) + "🙂"; @@ -223,7 +226,7 @@ public sealed class LocalMlClientTests [Fact] public async Task PushAsync_NegativeDelta_IsStoredAsIs() { - var learning = new FakeMlLearningStore(); + var learning = new TestMlLearningStore(); LocalMlClient client = CreateClient(new TestSettingsStore(), learning); await client.PushAsync("Это не спам — вернул из корзины", "spam", -1.0, CancellationToken.None); @@ -262,7 +265,7 @@ public sealed class LocalMlClientTests { var store = new TestSettingsStore(); store.Preload(SettingsKeys.MlDecisions, "7"); - var learning = new FakeMlLearningStore { LearningCount = 4 }; // журнал CardMoves: 4 записи + var learning = new TestMlLearningStore { LearningCount = 4 }; // журнал CardMoves: 4 записи LocalMlClient client = CreateClient(store, learning); await client.PushAsync("текст 1", "b_alpha", 1.0, CancellationToken.None); await client.PushAsync("текст 2", "spam", -1.0, CancellationToken.None); diff --git a/src/core/tests/Deal.Tests.Unit/Support/TestMlLearningStore.cs b/src/core/tests/Deal.Tests.Unit/Support/TestMlLearningStore.cs new file mode 100644 index 0000000..aeebf7f --- /dev/null +++ b/src/core/tests/Deal.Tests.Unit/Support/TestMlLearningStore.cs @@ -0,0 +1,65 @@ +using Deal.Modules.Kanban.Application.Abstractions; +using Deal.Modules.Kanban.Application.Models; +using NSubstitute; + +namespace Deal.Tests.Unit.Support; + +/// +/// Подставка на списках: сервисы получают NSubstitute-подставку +/// (), тесты сеют/проверяют очередь через и . +/// +public sealed class TestMlLearningStore +{ + private readonly List<(string Id, string Text, string Label, double Delta)> _rows = []; + + /// + /// Подставка порта ML-обучения (создаётся в конструкторе). + /// + public IMlLearningStore Store { get; } + + /// + /// Число записей журнала обучения + /// + public int LearningCount { get; set; } + + /// + /// Строки очереди + /// + public IReadOnlyList<(string Id, string Text, string Label, double Delta)> AddedRows => _rows.ToList(); + + /// + /// Создаёт подставку с пустой очередью. + /// + public TestMlLearningStore() + { + Store = Substitute.For(); + Store.CountLearningAsync(Arg.Any()) + .Returns(ci => LearningCount); + Store.CountOutboxAsync(Arg.Any()) + .Returns(_ => _rows.Count); + Store.When(s => s.AddOutboxAsync( + Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any())) + .Do(ci => _rows.Add((ci.ArgAt(0), ci.ArgAt(1), ci.ArgAt(2), ci.ArgAt(3)))); + Store.When(s => s.ClearOutboxAsync(Arg.Any())) + .Do(_ => _rows.Clear()); + Store.TakeOutboxBatchAsync(Arg.Any(), Arg.Any()) + .Returns(ci => _rows + .Take(ci.ArgAt(0)) + .Select(row => new MlOutboxEntryDto(row.Id, row.Text, row.Label, row.Delta)) + .ToList()); + Store.When(s => s.DeleteOutboxAsync(Arg.Any>(), Arg.Any())) + .Do(ci => _rows.RemoveAll(row => ci.Arg>().Contains(row.Id))); + } + + /// + /// Пишет строку очереди напрямую + /// + /// Id строки (mle_...). + /// Текст обучающего примера. + /// Метка обучения. + /// Вес сигнала. + public void SeedOutbox(string id, string text, string label, double delta = 1.0) + { + _rows.Add((id, text, label, delta)); + } +}