Перевести FakeSettingsStore на NSubstitute (42 файла потребителей)
Хелпер Support/TestSettingsStore: словарь настроек, GetAsync/GetAllAsync/SetAsync/ RemoveAsync читают состояние на момент вызова (GetAllAsync — ленивый, ключ для снапшот-сервисов). DI получает .Store, тесты сохраняют Preload/GetStoredJson.
This commit is contained in:
@@ -94,8 +94,8 @@ public sealed class DiscoveryWorkerSchedulerTests
|
|||||||
var tenantContext = new TenantContext();
|
var tenantContext = new TenantContext();
|
||||||
var storeA = new TestDiscoveryStore();
|
var storeA = new TestDiscoveryStore();
|
||||||
var storeB = new TestDiscoveryStore();
|
var storeB = new TestDiscoveryStore();
|
||||||
var settingsA = new FakeSettingsStore();
|
var settingsA = new TestSettingsStore();
|
||||||
var settingsB = new FakeSettingsStore();
|
var settingsB = new TestSettingsStore();
|
||||||
var gatewayA = new TestDiscoveryGateway();
|
var gatewayA = new TestDiscoveryGateway();
|
||||||
var gatewayB = new TestDiscoveryGateway();
|
var gatewayB = new TestDiscoveryGateway();
|
||||||
PreloadDiscoveryEvalOff(settingsA);
|
PreloadDiscoveryEvalOff(settingsA);
|
||||||
@@ -109,7 +109,7 @@ public sealed class DiscoveryWorkerSchedulerTests
|
|||||||
// Тенант-scoped адаптеры: фейк выбирает хранилище по ITenantContext, который цикл заполняет SetTenant
|
// Тенант-scoped адаптеры: фейк выбирает хранилище по ITenantContext, который цикл заполняет SetTenant
|
||||||
// (эталон PipelineWorkerSchedulerTests/ConnectionStringProvider.ForTenant).
|
// (эталон PipelineWorkerSchedulerTests/ConnectionStringProvider.ForTenant).
|
||||||
services.AddScoped<IDiscoveryStore>(provider => TenantOf(provider) == TenantA ? storeA.Store : storeB.Store);
|
services.AddScoped<IDiscoveryStore>(provider => TenantOf(provider) == TenantA ? storeA.Store : storeB.Store);
|
||||||
services.AddScoped<ISettingsStore>(provider => TenantOf(provider) == TenantA ? settingsA : settingsB);
|
services.AddScoped<ISettingsStore>(provider => TenantOf(provider) == TenantA ? settingsA.Store : settingsB.Store);
|
||||||
services.AddScoped<ITelegramGateway>(provider => TenantOf(provider) == TenantA ? gatewayA.Gateway : gatewayB.Gateway);
|
services.AddScoped<ITelegramGateway>(provider => TenantOf(provider) == TenantA ? gatewayA.Gateway : gatewayB.Gateway);
|
||||||
// Реальные сервисы модуля Discovery — как AddDiscoveryModule в Program.cs: цикл резолвит их в tenant-scope.
|
// Реальные сервисы модуля Discovery — как AddDiscoveryModule в Program.cs: цикл резолвит их в tenant-scope.
|
||||||
services.AddDiscoveryModule();
|
services.AddDiscoveryModule();
|
||||||
@@ -124,7 +124,7 @@ public sealed class DiscoveryWorkerSchedulerTests
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ML/ИИ выключены в настройках тенанта (оценка содержания — эвристика по ключам).
|
// ML/ИИ выключены в настройках тенанта (оценка содержания — эвристика по ключам).
|
||||||
private static void PreloadDiscoveryEvalOff(FakeSettingsStore settings)
|
private static void PreloadDiscoveryEvalOff(TestSettingsStore settings)
|
||||||
{
|
{
|
||||||
settings.Preload(SettingsKeys.MlEnabled, "false");
|
settings.Preload(SettingsKeys.MlEnabled, "false");
|
||||||
settings.Preload(SettingsKeys.AiEnabled, "false");
|
settings.Preload(SettingsKeys.AiEnabled, "false");
|
||||||
|
|||||||
@@ -163,7 +163,7 @@ public sealed class MlOutboxFlushSchedulerTests
|
|||||||
services.AddScoped<IMlTrainClient>(provider => provider.GetRequiredService<GrpcMlClient>());
|
services.AddScoped<IMlTrainClient>(provider => provider.GetRequiredService<GrpcMlClient>());
|
||||||
// Tenant-scoped адаптеры: выбирают фейк по тому же ITenantContext, который планировщик заполняет SetTenant.
|
// Tenant-scoped адаптеры: выбирают фейк по тому же ITenantContext, который планировщик заполняет SetTenant.
|
||||||
services.AddScoped<IMlLearningStore>(provider => storesByTenant[TenantOf(provider)]);
|
services.AddScoped<IMlLearningStore>(provider => storesByTenant[TenantOf(provider)]);
|
||||||
services.AddScoped<ISettingsStore>(_ => new FakeSettingsStore());
|
services.AddScoped<ISettingsStore>(_ => new TestSettingsStore().Store);
|
||||||
services.AddScoped<ITenantLimitStore>(_ => new TestTenantLimitStore().Store);
|
services.AddScoped<ITenantLimitStore>(_ => new TestTenantLimitStore().Store);
|
||||||
services.AddScoped<TokenUsageEventService>(_ => new TokenUsageEventService(new TestTokenUsageEventStore().Store));
|
services.AddScoped<TokenUsageEventService>(_ => new TokenUsageEventService(new TestTokenUsageEventStore().Store));
|
||||||
services.AddScoped<TokenUsageRecorder>();
|
services.AddScoped<TokenUsageRecorder>();
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ public sealed class TgStatusServiceTests
|
|||||||
[Fact]
|
[Fact]
|
||||||
public async Task GetAsync_GatewayUnavailable_ReturnsIdleFormWithKvAccountCountAndKeys()
|
public async Task GetAsync_GatewayUnavailable_ReturnsIdleFormWithKvAccountCountAndKeys()
|
||||||
{
|
{
|
||||||
(TgStatusService service, TestTelegramStore store, FakeSettingsStore settings, TestTelegramGateway gateway, _, TestGlobalSettingsStore globalSettings) = Create();
|
(TgStatusService service, TestTelegramStore store, TestSettingsStore settings, TestTelegramGateway gateway, _, TestGlobalSettingsStore globalSettings) = Create();
|
||||||
store.Seed(Dialog("d_1", "Канал", "channel", Monitor: true));
|
store.Seed(Dialog("d_1", "Канал", "channel", Monitor: true));
|
||||||
store.Seed(Dialog("d_2", "Группа", "group", Monitor: true));
|
store.Seed(Dialog("d_2", "Группа", "group", Monitor: true));
|
||||||
store.Seed(Dialog("d_off", "Выключен", "channel", Monitor: false));
|
store.Seed(Dialog("d_off", "Выключен", "channel", Monitor: false));
|
||||||
@@ -69,7 +69,7 @@ public sealed class TgStatusServiceTests
|
|||||||
[Fact]
|
[Fact]
|
||||||
public async Task GetAsync_ReadyGateway_ComposesLiveFieldsWithKvAccountMonitoredAndKeys()
|
public async Task GetAsync_ReadyGateway_ComposesLiveFieldsWithKvAccountMonitoredAndKeys()
|
||||||
{
|
{
|
||||||
(TgStatusService service, TestTelegramStore store, FakeSettingsStore settings, TestTelegramGateway gateway, _, TestGlobalSettingsStore globalSettings) = Create();
|
(TgStatusService service, TestTelegramStore store, TestSettingsStore settings, TestTelegramGateway gateway, _, TestGlobalSettingsStore globalSettings) = Create();
|
||||||
store.Seed(Dialog("d_1", "Канал", "channel", Monitor: true));
|
store.Seed(Dialog("d_1", "Канал", "channel", Monitor: true));
|
||||||
settings.Preload(SettingsKeys.TgAccount, "\"@realuser\"");
|
settings.Preload(SettingsKeys.TgAccount, "\"@realuser\"");
|
||||||
PreloadKeys(globalSettings, "123456", "abcdefghijklmnop");
|
PreloadKeys(globalSettings, "123456", "abcdefghijklmnop");
|
||||||
@@ -106,16 +106,16 @@ public sealed class TgStatusServiceTests
|
|||||||
// ─── Хелперы ────────────────────────────────────────────────────────────
|
// ─── Хелперы ────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
// Собирает сервис на фейках (FakeTelegramGateway/Store/SettingsStore/GlobalSettingsStore + ISecretCipher).
|
// Собирает сервис на фейках (FakeTelegramGateway/Store/SettingsStore/GlobalSettingsStore + ISecretCipher).
|
||||||
private static (TgStatusService Service, TestTelegramStore Store, FakeSettingsStore Settings, TestTelegramGateway Gateway, ISecretCipher Cipher, TestGlobalSettingsStore GlobalSettings) Create()
|
private static (TgStatusService Service, TestTelegramStore Store, TestSettingsStore Settings, TestTelegramGateway Gateway, ISecretCipher Cipher, TestGlobalSettingsStore GlobalSettings) Create()
|
||||||
{
|
{
|
||||||
var store = new TestTelegramStore();
|
var store = new TestTelegramStore();
|
||||||
var settings = new FakeSettingsStore();
|
var settings = new TestSettingsStore();
|
||||||
var gateway = new TestTelegramGateway();
|
var gateway = new TestTelegramGateway();
|
||||||
var cipher = TestCiphers.New();
|
var cipher = TestCiphers.New();
|
||||||
var globalSettings = new TestGlobalSettingsStore();
|
var globalSettings = new TestGlobalSettingsStore();
|
||||||
var keys = new TelegramKeysService(globalSettings.Store, cipher);
|
var keys = new TelegramKeysService(globalSettings.Store, cipher);
|
||||||
var dialogs = new DialogsService(store.Store, settings, gateway.Gateway, NullLogger<DialogsService>.Instance);
|
var dialogs = new DialogsService(store.Store, settings.Store, gateway.Gateway, NullLogger<DialogsService>.Instance);
|
||||||
var service = new TgStatusService(gateway.Gateway, dialogs, settings, keys);
|
var service = new TgStatusService(gateway.Gateway, dialogs, settings.Store, keys);
|
||||||
return (service, store, settings, gateway, cipher, globalSettings);
|
return (service, store, settings, gateway, cipher, globalSettings);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -128,7 +128,7 @@ public sealed class BudgetedAiClassifierTests
|
|||||||
Context ctx = Create(
|
Context ctx = Create(
|
||||||
limits => limits.Preload(
|
limits => limits.Preload(
|
||||||
TenantGuid, budgetTokens: 0, TenantLimitPeriods.Month, PeriodStart, usedTokens: 0));
|
TenantGuid, budgetTokens: 0, TenantLimitPeriods.Month, PeriodStart, usedTokens: 0));
|
||||||
var standaloneLocal = new LocalAiClassifier(new LocalFieldsParser(ctx.Settings));
|
var standaloneLocal = new LocalAiClassifier(new LocalFieldsParser(ctx.Settings.Store));
|
||||||
const string text = "Ищем Python-разработчика на проект в команду\nБюджет: до 2к$";
|
const string text = "Ищем Python-разработчика на проект в команду\nБюджет: до 2к$";
|
||||||
|
|
||||||
AiParsedCardDto result = await ctx.Decorator.ClassifyAsync(text, CancellationToken.None);
|
AiParsedCardDto result = await ctx.Decorator.ClassifyAsync(text, CancellationToken.None);
|
||||||
@@ -152,7 +152,7 @@ public sealed class BudgetedAiClassifierTests
|
|||||||
private sealed record Context(
|
private sealed record Context(
|
||||||
IAiClassifier Decorator,
|
IAiClassifier Decorator,
|
||||||
TestAiClassifier Paid,
|
TestAiClassifier Paid,
|
||||||
FakeSettingsStore Settings);
|
TestSettingsStore Settings);
|
||||||
|
|
||||||
// Собирает контекст: платный фейк + реальный Local-классификатор + фейк лимитов и tenant-контекст
|
// Собирает контекст: платный фейк + реальный Local-классификатор + фейк лимитов и tenant-контекст
|
||||||
// (зеркало регистрации AddDealIntegrations при UseLocal=false).
|
// (зеркало регистрации AddDealIntegrations при UseLocal=false).
|
||||||
@@ -160,7 +160,7 @@ public sealed class BudgetedAiClassifierTests
|
|||||||
// Возвращает: Контекст теста.
|
// Возвращает: Контекст теста.
|
||||||
private static Context Create(Action<TestTenantLimitStore>? configure = null)
|
private static Context Create(Action<TestTenantLimitStore>? configure = null)
|
||||||
{
|
{
|
||||||
var settings = new FakeSettingsStore();
|
var settings = new TestSettingsStore();
|
||||||
var limits = new TestTenantLimitStore();
|
var limits = new TestTenantLimitStore();
|
||||||
configure?.Invoke(limits);
|
configure?.Invoke(limits);
|
||||||
ITenantContext tenantContext = new TenantContext();
|
ITenantContext tenantContext = new TenantContext();
|
||||||
@@ -168,7 +168,7 @@ public sealed class BudgetedAiClassifierTests
|
|||||||
var paid = new TestAiClassifier();
|
var paid = new TestAiClassifier();
|
||||||
IAiClassifier decorator = new BudgetedAiClassifier(
|
IAiClassifier decorator = new BudgetedAiClassifier(
|
||||||
paid.Classifier,
|
paid.Classifier,
|
||||||
new LocalAiClassifier(new LocalFieldsParser(settings)),
|
new LocalAiClassifier(new LocalFieldsParser(settings.Store)),
|
||||||
limits.Store,
|
limits.Store,
|
||||||
tenantContext,
|
tenantContext,
|
||||||
NullLogger<BudgetedAiClassifier>.Instance);
|
NullLogger<BudgetedAiClassifier>.Instance);
|
||||||
|
|||||||
@@ -193,7 +193,7 @@ public sealed class CardComposerTests
|
|||||||
[Fact]
|
[Fact]
|
||||||
public async Task BuildAsync_ConversionDisabled_LeavesConvFieldsEmpty()
|
public async Task BuildAsync_ConversionDisabled_LeavesConvFieldsEmpty()
|
||||||
{
|
{
|
||||||
(CardComposer composer, _, FakeSettingsStore settings) = Create();
|
(CardComposer composer, _, TestSettingsStore settings) = Create();
|
||||||
settings.Preload(SettingsKeys.ConversionOn, "false"); // budget_to_target L348–349
|
settings.Preload(SettingsKeys.ConversionOn, "false"); // budget_to_target L348–349
|
||||||
|
|
||||||
CardSnapshot snapshot = await composer.BuildAsync(
|
CardSnapshot snapshot = await composer.BuildAsync(
|
||||||
@@ -232,11 +232,11 @@ public sealed class CardComposerTests
|
|||||||
|
|
||||||
// Создаёт контекст теста: пустые хранилища (дефолты: конверсия включена, курсы — мок).
|
// Создаёт контекст теста: пустые хранилища (дефолты: конверсия включена, курсы — мок).
|
||||||
// Возвращает: Кортеж (композитор, канбан-хранилище, KV-настройки).
|
// Возвращает: Кортеж (композитор, канбан-хранилище, KV-настройки).
|
||||||
private static (CardComposer Composer, FakeKanjStore Store, FakeSettingsStore Settings) Create()
|
private static (CardComposer Composer, FakeKanjStore Store, TestSettingsStore Settings) Create()
|
||||||
{
|
{
|
||||||
var store = new FakeKanjStore();
|
var store = new FakeKanjStore();
|
||||||
var settings = new FakeSettingsStore();
|
var settings = new TestSettingsStore();
|
||||||
return (new CardComposer(store, settings), store, settings);
|
return (new CardComposer(store, settings.Store), store, settings);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Разбор карточки со значениями по умолчанию (сценарий теста перекрывает нужные поля).
|
// Разбор карточки со значениями по умолчанию (сценарий теста перекрывает нужные поля).
|
||||||
|
|||||||
@@ -711,12 +711,12 @@ public sealed class CardsServiceTests
|
|||||||
|
|
||||||
// ─── Хелперы ──────────────────────────────────────────────────────────
|
// ─── Хелперы ──────────────────────────────────────────────────────────
|
||||||
|
|
||||||
private static (CardsService Service, FakeKanjStore Store, FakeSettingsStore Settings, TestMlClient Ml) Create()
|
private static (CardsService Service, FakeKanjStore Store, TestSettingsStore Settings, TestMlClient Ml) Create()
|
||||||
{
|
{
|
||||||
var store = new FakeKanjStore();
|
var store = new FakeKanjStore();
|
||||||
var settings = new FakeSettingsStore();
|
var settings = new TestSettingsStore();
|
||||||
var ml = new TestMlClient();
|
var ml = new TestMlClient();
|
||||||
return (new CardsService(store, settings, ml.Client, new TestFileStorage().Storage), store, settings, ml);
|
return (new CardsService(store, settings.Store, ml.Client, new TestFileStorage().Storage), store, settings, ml);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Доска с правилами (ContainerRulesDto) либо без них.
|
// Доска с правилами (ContainerRulesDto) либо без них.
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ public sealed class DialogsServiceTests
|
|||||||
[Fact]
|
[Fact]
|
||||||
public async Task Sync_AutoMonitorNewFalse_AddsNewDialogsUnmonitored()
|
public async Task Sync_AutoMonitorNewFalse_AddsNewDialogsUnmonitored()
|
||||||
{
|
{
|
||||||
(DialogsService service, TestTelegramStore store, FakeSettingsStore settings, _) = Create();
|
(DialogsService service, TestTelegramStore store, TestSettingsStore settings, _) = Create();
|
||||||
settings.Preload(SettingsKeys.AutoMonitorNew, "false");
|
settings.Preload(SettingsKeys.AutoMonitorNew, "false");
|
||||||
|
|
||||||
int count = await service.SyncFromTelegramAsync(Catalog(Entry("d_1", "Канал", "channel")), CancellationToken.None);
|
int count = await service.SyncFromTelegramAsync(Catalog(Entry("d_1", "Канал", "channel")), CancellationToken.None);
|
||||||
@@ -497,12 +497,12 @@ public sealed class DialogsServiceTests
|
|||||||
// ─── Хелперы ────────────────────────────────────────────────────────────
|
// ─── Хелперы ────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
// Собирает сервис на фейках (TestTelegramStore — семантика адаптера; настройки/гейт чистые).
|
// Собирает сервис на фейках (TestTelegramStore — семантика адаптера; настройки/гейт чистые).
|
||||||
private static (DialogsService Service, TestTelegramStore Store, FakeSettingsStore Settings, TestTelegramGateway Gateway) Create()
|
private static (DialogsService Service, TestTelegramStore Store, TestSettingsStore Settings, TestTelegramGateway Gateway) Create()
|
||||||
{
|
{
|
||||||
var store = new TestTelegramStore();
|
var store = new TestTelegramStore();
|
||||||
var settings = new FakeSettingsStore();
|
var settings = new TestSettingsStore();
|
||||||
var gateway = new TestTelegramGateway();
|
var gateway = new TestTelegramGateway();
|
||||||
return (new DialogsService(store.Store, settings, gateway.Gateway, NullLogger<DialogsService>.Instance), store, settings, gateway);
|
return (new DialogsService(store.Store, settings.Store, gateway.Gateway, NullLogger<DialogsService>.Instance), store, settings, gateway);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Строка каталога минимально (id/имя/kind + флаги/хвост каталога).
|
// Строка каталога минимально (id/имя/kind + флаги/хвост каталога).
|
||||||
|
|||||||
@@ -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, TestMlClient Ml, IAiTools Ai) Create(bool aiEnabled, bool mlEnabled)
|
private static (DiscoveryEvaluator Evaluator, TestSettingsStore Settings, TestMlClient Ml, IAiTools Ai) Create(bool aiEnabled, bool mlEnabled)
|
||||||
{
|
{
|
||||||
var settings = new FakeSettingsStore();
|
var settings = new TestSettingsStore();
|
||||||
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 TestMlClient();
|
var ml = new TestMlClient();
|
||||||
var ai = TestAiTools.New();
|
var ai = TestAiTools.New();
|
||||||
return (new DiscoveryEvaluator(settings, ml.Client, ai), settings, ml, ai);
|
return (new DiscoveryEvaluator(settings.Store, ml.Client, ai), settings, ml, ai);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Задача как строка DiscTasks (адаптер-форма; нужны только keywords/description/threshold).
|
// Задача как строка DiscTasks (адаптер-форма; нужны только keywords/description/threshold).
|
||||||
|
|||||||
@@ -419,7 +419,7 @@ public sealed class DiscoveryWorkerServiceTests
|
|||||||
// Контекст теста: воркер на фейках + ссылки для проверок состояния.
|
// Контекст теста: воркер на фейках + ссылки для проверок состояния.
|
||||||
private sealed record Fixture(
|
private sealed record Fixture(
|
||||||
TestDiscoveryStore Store,
|
TestDiscoveryStore Store,
|
||||||
FakeSettingsStore Settings,
|
TestSettingsStore Settings,
|
||||||
TestDiscoveryGateway Gateway,
|
TestDiscoveryGateway Gateway,
|
||||||
IDiscoveryPacer Pacer,
|
IDiscoveryPacer Pacer,
|
||||||
DiscoveryWorkerService Worker,
|
DiscoveryWorkerService Worker,
|
||||||
@@ -429,7 +429,7 @@ public sealed class DiscoveryWorkerServiceTests
|
|||||||
private static Fixture Create()
|
private static Fixture Create()
|
||||||
{
|
{
|
||||||
var store = new TestDiscoveryStore();
|
var store = new TestDiscoveryStore();
|
||||||
var settings = new FakeSettingsStore();
|
var settings = new TestSettingsStore();
|
||||||
settings.Preload(SettingsKeys.MlEnabled, "false");
|
settings.Preload(SettingsKeys.MlEnabled, "false");
|
||||||
settings.Preload(SettingsKeys.AiEnabled, "false");
|
settings.Preload(SettingsKeys.AiEnabled, "false");
|
||||||
var ml = new TestMlClient();
|
var ml = new TestMlClient();
|
||||||
@@ -437,13 +437,13 @@ public sealed class DiscoveryWorkerServiceTests
|
|||||||
var log = new DiscoveryLogService(store.Store);
|
var log = new DiscoveryLogService(store.Store);
|
||||||
var blacklist = new DiscoveryBlacklistService(store.Store);
|
var blacklist = new DiscoveryBlacklistService(store.Store);
|
||||||
var candidates = new DiscoveryCandidatesService(store.Store, log, blacklist);
|
var candidates = new DiscoveryCandidatesService(store.Store, log, blacklist);
|
||||||
var planGuard = new DiscoveryPlanGuard(store.Store, settings);
|
var planGuard = new DiscoveryPlanGuard(store.Store, settings.Store);
|
||||||
var tasks = new DiscoveryTasksService(store.Store, planGuard, settings);
|
var tasks = new DiscoveryTasksService(store.Store, planGuard, settings.Store);
|
||||||
var banGuard = new DiscoveryBanGuard(store.Store, settings);
|
var banGuard = new DiscoveryBanGuard(store.Store, settings.Store);
|
||||||
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.Client, ai), banGuard, pacer, gateway.Gateway);
|
store.Store, tasks, candidates, log, new DiscoveryEvaluator(settings.Store, 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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ public sealed class GrpcAiToolsTests
|
|||||||
reply.Keywords.Add("python разработка");
|
reply.Keywords.Add("python разработка");
|
||||||
reply.Keywords.Add("telegram каналы");
|
reply.Keywords.Add("telegram каналы");
|
||||||
service.GenerateKeywordsReply = reply;
|
service.GenerateKeywordsReply = reply;
|
||||||
FakeSettingsStore settings = new();
|
TestSettingsStore settings = new();
|
||||||
ISecretCipher cipher = TestCiphers.New();
|
ISecretCipher cipher = TestCiphers.New();
|
||||||
TestTenantLimitStore limits = new();
|
TestTenantLimitStore limits = new();
|
||||||
|
|
||||||
@@ -66,7 +66,7 @@ public sealed class GrpcAiToolsTests
|
|||||||
await AiGrpcTestHost.RunAsync(AiGrpcTestHost.DefaultToken, new RecordingAiService(), async (port, service) =>
|
await AiGrpcTestHost.RunAsync(AiGrpcTestHost.DefaultToken, new RecordingAiService(), async (port, service) =>
|
||||||
{
|
{
|
||||||
service.KeywordsUnavailable = true;
|
service.KeywordsUnavailable = true;
|
||||||
IAiTools tools = CreateTools(port, new FakeSettingsStore(), TestCiphers.New());
|
IAiTools tools = CreateTools(port, new TestSettingsStore(), TestCiphers.New());
|
||||||
|
|
||||||
AiGenerateKeywordsResultDto result = await tools.GenerateKeywordsAsync("описание", CancellationToken.None);
|
AiGenerateKeywordsResultDto result = await tools.GenerateKeywordsAsync("описание", CancellationToken.None);
|
||||||
|
|
||||||
@@ -89,7 +89,7 @@ public sealed class GrpcAiToolsTests
|
|||||||
Usage = new Usage { Prompt = 200, Completion = 10, Total = 210 },
|
Usage = new Usage { Prompt = 200, Completion = 10, Total = 210 },
|
||||||
};
|
};
|
||||||
TestTenantLimitStore limits = new();
|
TestTenantLimitStore limits = new();
|
||||||
IAiTools tools = CreateTools(port, new FakeSettingsStore(), TestCiphers.New(), limits);
|
IAiTools tools = CreateTools(port, new TestSettingsStore(), TestCiphers.New(), limits);
|
||||||
|
|
||||||
AiEvaluateFitResultDto result = await tools.EvaluateFitAsync(
|
AiEvaluateFitResultDto result = await tools.EvaluateFitAsync(
|
||||||
"Ищу дизайнера для лендинга", "Разработка сайтов на Python", new[] { "python", "бэкенд" }, CancellationToken.None);
|
"Ищу дизайнера для лендинга", "Разработка сайтов на Python", new[] { "python", "бэкенд" }, CancellationToken.None);
|
||||||
@@ -113,7 +113,7 @@ public sealed class GrpcAiToolsTests
|
|||||||
await AiGrpcTestHost.RunAsync(AiGrpcTestHost.DefaultToken, new RecordingAiService(), async (port, service) =>
|
await AiGrpcTestHost.RunAsync(AiGrpcTestHost.DefaultToken, new RecordingAiService(), async (port, service) =>
|
||||||
{
|
{
|
||||||
service.FitUnavailable = true;
|
service.FitUnavailable = true;
|
||||||
IAiTools tools = CreateTools(port, new FakeSettingsStore(), TestCiphers.New());
|
IAiTools tools = CreateTools(port, new TestSettingsStore(), TestCiphers.New());
|
||||||
|
|
||||||
await Assert.ThrowsAsync<AiUnavailableException>(
|
await Assert.ThrowsAsync<AiUnavailableException>(
|
||||||
() => tools.EvaluateFitAsync("текст", "описание", new[] { "ключ" }, CancellationToken.None));
|
() => tools.EvaluateFitAsync("текст", "описание", new[] { "ключ" }, CancellationToken.None));
|
||||||
@@ -128,7 +128,7 @@ public sealed class GrpcAiToolsTests
|
|||||||
// Возвращает: Экземпляр GrpcAiTools.
|
// Возвращает: Экземпляр GrpcAiTools.
|
||||||
private static GrpcAiTools CreateTools(
|
private static GrpcAiTools CreateTools(
|
||||||
int port,
|
int port,
|
||||||
FakeSettingsStore settings,
|
TestSettingsStore settings,
|
||||||
ISecretCipher cipher,
|
ISecretCipher cipher,
|
||||||
TestTenantLimitStore? limits = null)
|
TestTenantLimitStore? limits = null)
|
||||||
{
|
{
|
||||||
@@ -139,8 +139,8 @@ public sealed class GrpcAiToolsTests
|
|||||||
return new GrpcAiTools(
|
return new GrpcAiTools(
|
||||||
tenantContext,
|
tenantContext,
|
||||||
connection,
|
connection,
|
||||||
new AiProviderConfigBuilder(settings, cipher),
|
new AiProviderConfigBuilder(settings.Store, cipher),
|
||||||
new TokenUsageRecorder(settings, limits.Store, tenantContext, new TokenUsageEventService(new TestTokenUsageEventStore().Store)),
|
new TokenUsageRecorder(settings.Store, limits.Store, tenantContext, new TokenUsageEventService(new TestTokenUsageEventStore().Store)),
|
||||||
NullLogger<GrpcAiTools>.Instance);
|
NullLogger<GrpcAiTools>.Instance);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -110,7 +110,7 @@ public sealed class GrpcMlClientTests
|
|||||||
Eval = new ModelEval { Count = 5, Correct = 4, Accuracy = 0.8 },
|
Eval = new ModelEval { Count = 5, Correct = 4, Accuracy = 0.8 },
|
||||||
};
|
};
|
||||||
|
|
||||||
var settings = new FakeSettingsStore();
|
var settings = new TestSettingsStore();
|
||||||
settings.Preload(SettingsKeys.MlDecisions, "7");
|
settings.Preload(SettingsKeys.MlDecisions, "7");
|
||||||
settings.Preload(SettingsKeys.AiDecisions, "3");
|
settings.Preload(SettingsKeys.AiDecisions, "3");
|
||||||
var learning = new FakeMlLearningStore { LearningCount = 9 };
|
var learning = new FakeMlLearningStore { LearningCount = 9 };
|
||||||
@@ -309,21 +309,22 @@ public sealed class GrpcMlClientTests
|
|||||||
// Возвращает: Экземпляр GrpcMlClient в tenant-контексте теста.
|
// Возвращает: Экземпляр GrpcMlClient в tenant-контексте теста.
|
||||||
private static GrpcMlClient CreateClient(
|
private static GrpcMlClient CreateClient(
|
||||||
int port,
|
int port,
|
||||||
FakeSettingsStore? settings = null,
|
TestSettingsStore? settings = null,
|
||||||
FakeMlLearningStore? learning = null,
|
FakeMlLearningStore? learning = null,
|
||||||
MlStatusCache? cache = null)
|
MlStatusCache? cache = null)
|
||||||
{
|
{
|
||||||
|
settings ??= new TestSettingsStore();
|
||||||
ITenantContext tenantContext = new TenantContext();
|
ITenantContext tenantContext = new TenantContext();
|
||||||
tenantContext.SetTenant(new TenantId(TenantIdValue));
|
tenantContext.SetTenant(new TenantId(TenantIdValue));
|
||||||
var options = new MlServiceOptions { UseLocal = false, Endpoint = $"http://127.0.0.1:{port}" };
|
var options = new MlServiceOptions { UseLocal = false, Endpoint = $"http://127.0.0.1:{port}" };
|
||||||
return new GrpcMlClient(
|
return new GrpcMlClient(
|
||||||
tenantContext,
|
tenantContext,
|
||||||
settings ?? new FakeSettingsStore(),
|
settings.Store,
|
||||||
learning ?? new FakeMlLearningStore(),
|
learning ?? new FakeMlLearningStore(),
|
||||||
new MlGrpcConnection(options),
|
new MlGrpcConnection(options),
|
||||||
cache ?? new MlStatusCache(),
|
cache ?? new MlStatusCache(),
|
||||||
new TokenUsageRecorder(
|
new TokenUsageRecorder(
|
||||||
settings ?? new FakeSettingsStore(),
|
settings.Store,
|
||||||
new TestTenantLimitStore().Store,
|
new TestTenantLimitStore().Store,
|
||||||
tenantContext,
|
tenantContext,
|
||||||
new TokenUsageEventService(new TestTokenUsageEventStore().Store)),
|
new TokenUsageEventService(new TestTokenUsageEventStore().Store)),
|
||||||
|
|||||||
@@ -144,7 +144,7 @@ public sealed class IntegrationsDiTests
|
|||||||
ITenantContext tenantContext = new TenantContext();
|
ITenantContext tenantContext = new TenantContext();
|
||||||
tenantContext.SetTenant(new TenantId(Guid.NewGuid().ToString("N")));
|
tenantContext.SetTenant(new TenantId(Guid.NewGuid().ToString("N")));
|
||||||
services.AddSingleton<ITenantContext>(tenantContext);
|
services.AddSingleton<ITenantContext>(tenantContext);
|
||||||
services.AddScoped<ISettingsStore>(_ => new FakeSettingsStore());
|
services.AddScoped<ISettingsStore>(_ => new TestSettingsStore().Store);
|
||||||
services.AddScoped<ISecretCipher>(_ => TestCiphers.New());
|
services.AddScoped<ISecretCipher>(_ => TestCiphers.New());
|
||||||
services.AddScoped<ICardStore>(_ => new FakeKanjStore());
|
services.AddScoped<ICardStore>(_ => new FakeKanjStore());
|
||||||
services.AddScoped<IMlLearningStore>(_ => new FakeMlLearningStore());
|
services.AddScoped<IMlLearningStore>(_ => new FakeMlLearningStore());
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ using Deal.Contracts.Integrations.Models;
|
|||||||
using Deal.Infrastructure.Integrations.Services;
|
using Deal.Infrastructure.Integrations.Services;
|
||||||
using Deal.Modules.Pipeline.Application.Parse;
|
using Deal.Modules.Pipeline.Application.Parse;
|
||||||
using Deal.Modules.Settings.Application.Models;
|
using Deal.Modules.Settings.Application.Models;
|
||||||
|
using Deal.Tests.Unit.Support;
|
||||||
using Deal.Tests.Unit.Modules.Settings;
|
using Deal.Tests.Unit.Modules.Settings;
|
||||||
|
|
||||||
namespace Deal.Tests.Unit.Contracts;
|
namespace Deal.Tests.Unit.Contracts;
|
||||||
@@ -13,10 +14,10 @@ public sealed class LocalAiClassifierTests
|
|||||||
{
|
{
|
||||||
// Создаёт контекст теста: пустое KV-хранилище (дефолты маркеров) + адаптер.
|
// Создаёт контекст теста: пустое KV-хранилище (дефолты маркеров) + адаптер.
|
||||||
// Возвращает: Кортеж (KV-настройки, классификатор).
|
// Возвращает: Кортеж (KV-настройки, классификатор).
|
||||||
private static (FakeSettingsStore Settings, LocalAiClassifier Classifier) CreateContext()
|
private static (TestSettingsStore Settings, LocalAiClassifier Classifier) CreateContext()
|
||||||
{
|
{
|
||||||
var settings = new FakeSettingsStore();
|
var settings = new TestSettingsStore();
|
||||||
var classifier = new LocalAiClassifier(new LocalFieldsParser(settings));
|
var classifier = new LocalAiClassifier(new LocalFieldsParser(settings.Store));
|
||||||
return (settings, classifier);
|
return (settings, classifier);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -140,7 +141,7 @@ public sealed class LocalAiClassifierTests
|
|||||||
[Fact]
|
[Fact]
|
||||||
public async Task Classify_CustomHireMarkersOverride_AreUsed()
|
public async Task Classify_CustomHireMarkersOverride_AreUsed()
|
||||||
{
|
{
|
||||||
(FakeSettingsStore settings, LocalAiClassifier classifier) = CreateContext();
|
(TestSettingsStore settings, LocalAiClassifier classifier) = CreateContext();
|
||||||
// Дефолтные маркеры заменены: «вакансия» больше не маркер найма, свой — маркер.
|
// Дефолтные маркеры заменены: «вакансия» больше не маркер найма, свой — маркер.
|
||||||
settings.Preload(SettingsKeys.HireMarkers, "[\"нанимаем джунов\"]");
|
settings.Preload(SettingsKeys.HireMarkers, "[\"нанимаем джунов\"]");
|
||||||
|
|
||||||
|
|||||||
@@ -88,7 +88,7 @@ public sealed class PipelineCardWriterTests
|
|||||||
{
|
{
|
||||||
var store = new FakeKanjStore();
|
var store = new FakeKanjStore();
|
||||||
var pipelineStore = new FakePipelineStore();
|
var pipelineStore = new FakePipelineStore();
|
||||||
var composer = new CardComposer(store, new FakeSettingsStore());
|
var composer = new CardComposer(store, new TestSettingsStore().Store);
|
||||||
return (new PipelineCardWriter(store, pipelineStore, composer), store, pipelineStore);
|
return (new PipelineCardWriter(store, pipelineStore, composer), store, pipelineStore);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -156,7 +156,7 @@ public sealed class PipelineWorkerGrpcAiTests
|
|||||||
PipelineWorkerService Worker,
|
PipelineWorkerService Worker,
|
||||||
TestPipelineStore PipelineStore,
|
TestPipelineStore PipelineStore,
|
||||||
FakeKanjStore KanjStore,
|
FakeKanjStore KanjStore,
|
||||||
FakeSettingsStore Settings,
|
TestSettingsStore Settings,
|
||||||
TestTenantLimitStore Limits);
|
TestTenantLimitStore Limits);
|
||||||
|
|
||||||
private static Context CreateContext(
|
private static Context CreateContext(
|
||||||
@@ -164,12 +164,12 @@ public sealed class PipelineWorkerGrpcAiTests
|
|||||||
TestTenantLimitStore? limits = null,
|
TestTenantLimitStore? limits = null,
|
||||||
bool budgeted = false)
|
bool budgeted = false)
|
||||||
{
|
{
|
||||||
var settings = new FakeSettingsStore();
|
var settings = new TestSettingsStore();
|
||||||
var pipelineStore = new TestPipelineStore();
|
var pipelineStore = new TestPipelineStore();
|
||||||
var kanjStore = new FakeKanjStore();
|
var kanjStore = new FakeKanjStore();
|
||||||
var mlClient = new TestMlClient { Predict = NotReadyPrediction() };
|
var mlClient = new TestMlClient { Predict = NotReadyPrediction() };
|
||||||
var rules = new IncomingRules(settings);
|
var rules = new IncomingRules(settings.Store);
|
||||||
var fieldsParser = new LocalFieldsParser(settings);
|
var fieldsParser = new LocalFieldsParser(settings.Store);
|
||||||
|
|
||||||
ITenantContext tenantContext = new TenantContext();
|
ITenantContext tenantContext = new TenantContext();
|
||||||
tenantContext.SetTenant(new TenantId(TenantIdValue));
|
tenantContext.SetTenant(new TenantId(TenantIdValue));
|
||||||
@@ -179,9 +179,9 @@ public sealed class PipelineWorkerGrpcAiTests
|
|||||||
var grpcClassifier = new GrpcAiClassifier(
|
var grpcClassifier = new GrpcAiClassifier(
|
||||||
tenantContext,
|
tenantContext,
|
||||||
connection,
|
connection,
|
||||||
new AiProviderConfigBuilder(settings, TestCiphers.New()),
|
new AiProviderConfigBuilder(settings.Store, TestCiphers.New()),
|
||||||
new AiClassifyContextBuilder(settings, kanjStore),
|
new AiClassifyContextBuilder(settings.Store, kanjStore),
|
||||||
new TokenUsageRecorder(settings, limits.Store, tenantContext, new TokenUsageEventService(new TestTokenUsageEventStore().Store)),
|
new TokenUsageRecorder(settings.Store, limits.Store, tenantContext, new TokenUsageEventService(new TestTokenUsageEventStore().Store)),
|
||||||
NullLogger<GrpcAiClassifier>.Instance);
|
NullLogger<GrpcAiClassifier>.Instance);
|
||||||
IAiClassifier aiClassifier = budgeted
|
IAiClassifier aiClassifier = budgeted
|
||||||
? new BudgetedAiClassifier(
|
? new BudgetedAiClassifier(
|
||||||
@@ -193,10 +193,10 @@ public sealed class PipelineWorkerGrpcAiTests
|
|||||||
: grpcClassifier;
|
: grpcClassifier;
|
||||||
|
|
||||||
var processing = new PipelineProcessingService(pipelineStore.Store, mlClient.Client, 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.Store);
|
||||||
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.Client, aiClassifier, processing, writer, fieldsParser);
|
pipelineStore.Store, settings.Store, 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 TestMlClient().Client, new TestFileStorage().Storage);
|
var cardsService = new CardsService(store, new TestSettingsStore().Store, new TestMlClient().Client, new TestFileStorage().Storage);
|
||||||
return (new CardMover(cardsService), store);
|
return (new CardMover(cardsService), store);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -40,8 +40,8 @@ public sealed class DiscoveryBanGuardTests
|
|||||||
|
|
||||||
// Вчерашние вступления в суточную квоту не входят: 49 сегодня → ещё можно.
|
// Вчерашние вступления в суточную квоту не входят: 49 сегодня → ещё можно.
|
||||||
TestDiscoveryStore yesterdayStore = new(() => Now);
|
TestDiscoveryStore yesterdayStore = new(() => Now);
|
||||||
var settings = new FakeSettingsStore();
|
var settings = new TestSettingsStore();
|
||||||
var secondGuard = new DiscoveryBanGuard(yesterdayStore.Store, settings, () => Now);
|
var secondGuard = new DiscoveryBanGuard(yesterdayStore.Store, settings.Store, () => Now);
|
||||||
for (int i = 0; i < 49; i++)
|
for (int i = 0; i < 49; i++)
|
||||||
{
|
{
|
||||||
yesterdayStore.SeedLog(DiscoveryLogEvents.JoinAuto, TodayStart.AddMinutes(i));
|
yesterdayStore.SeedLog(DiscoveryLogEvents.JoinAuto, TodayStart.AddMinutes(i));
|
||||||
@@ -67,7 +67,7 @@ public sealed class DiscoveryBanGuardTests
|
|||||||
[Fact]
|
[Fact]
|
||||||
public async Task NoteFlood_BlocksAutoJoinsUntilEndOfDay()
|
public async Task NoteFlood_BlocksAutoJoinsUntilEndOfDay()
|
||||||
{
|
{
|
||||||
(DiscoveryBanGuard guard, _, FakeSettingsStore settings) = Create();
|
(DiscoveryBanGuard guard, _, TestSettingsStore settings) = Create();
|
||||||
|
|
||||||
await guard.NoteFloodAsync(CancellationToken.None);
|
await guard.NoteFloodAsync(CancellationToken.None);
|
||||||
|
|
||||||
@@ -79,7 +79,7 @@ public sealed class DiscoveryBanGuardTests
|
|||||||
[Fact]
|
[Fact]
|
||||||
public async Task FloodYesterday_DoesNotBlockToday()
|
public async Task FloodYesterday_DoesNotBlockToday()
|
||||||
{
|
{
|
||||||
(DiscoveryBanGuard guard, _, FakeSettingsStore settings) = Create();
|
(DiscoveryBanGuard guard, _, TestSettingsStore settings) = Create();
|
||||||
settings.Preload(SettingsKeys.DiscFloodDay, TodayStart.AddDays(-1).ToUnixTimeMilliseconds().ToString());
|
settings.Preload(SettingsKeys.DiscFloodDay, TodayStart.AddDays(-1).ToUnixTimeMilliseconds().ToString());
|
||||||
|
|
||||||
Assert.False(await guard.FloodTodayAsync(CancellationToken.None));
|
Assert.False(await guard.FloodTodayAsync(CancellationToken.None));
|
||||||
@@ -89,7 +89,7 @@ public sealed class DiscoveryBanGuardTests
|
|||||||
[Fact]
|
[Fact]
|
||||||
public async Task GlobalPaused_BlocksAutoJoins()
|
public async Task GlobalPaused_BlocksAutoJoins()
|
||||||
{
|
{
|
||||||
(DiscoveryBanGuard guard, _, FakeSettingsStore settings) = Create();
|
(DiscoveryBanGuard guard, _, TestSettingsStore settings) = Create();
|
||||||
settings.Preload(SettingsKeys.DiscPaused, "true");
|
settings.Preload(SettingsKeys.DiscPaused, "true");
|
||||||
|
|
||||||
Assert.True(await guard.GlobalPausedAsync(CancellationToken.None));
|
Assert.True(await guard.GlobalPausedAsync(CancellationToken.None));
|
||||||
@@ -99,7 +99,7 @@ public sealed class DiscoveryBanGuardTests
|
|||||||
[Fact]
|
[Fact]
|
||||||
public async Task CustomJoinLimit_AppliedWhenSettingStored()
|
public async Task CustomJoinLimit_AppliedWhenSettingStored()
|
||||||
{
|
{
|
||||||
(DiscoveryBanGuard guard, TestDiscoveryStore store, FakeSettingsStore settings) = Create();
|
(DiscoveryBanGuard guard, TestDiscoveryStore store, TestSettingsStore settings) = Create();
|
||||||
settings.Preload(SettingsKeys.DiscJoinLimit, "2");
|
settings.Preload(SettingsKeys.DiscJoinLimit, "2");
|
||||||
|
|
||||||
Assert.True(await guard.CanAutoJoinAsync(CancellationToken.None));
|
Assert.True(await guard.CanAutoJoinAsync(CancellationToken.None));
|
||||||
@@ -109,11 +109,11 @@ public sealed class DiscoveryBanGuardTests
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Создаёт связку гард + фейки на фиксированных «часах» (store и гард смотрят одно время).
|
// Создаёт связку гард + фейки на фиксированных «часах» (store и гард смотрят одно время).
|
||||||
private static (DiscoveryBanGuard Guard, TestDiscoveryStore Store, FakeSettingsStore Settings) Create()
|
private static (DiscoveryBanGuard Guard, TestDiscoveryStore Store, TestSettingsStore Settings) Create()
|
||||||
{
|
{
|
||||||
var store = new TestDiscoveryStore(() => Now);
|
var store = new TestDiscoveryStore(() => Now);
|
||||||
var settings = new FakeSettingsStore();
|
var settings = new TestSettingsStore();
|
||||||
var guard = new DiscoveryBanGuard(store.Store, settings, () => Now);
|
var guard = new DiscoveryBanGuard(store.Store, settings.Store, () => Now);
|
||||||
return (guard, store, settings);
|
return (guard, store, settings);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -117,7 +117,7 @@ public sealed class DiscoveryTasksServiceTests
|
|||||||
[Fact]
|
[Fact]
|
||||||
public async Task Create_CustomThresholdAndSample_Respected()
|
public async Task Create_CustomThresholdAndSample_Respected()
|
||||||
{
|
{
|
||||||
(DiscoveryTasksService service, TestDiscoveryStore store, FakeSettingsStore settings) = Create();
|
(DiscoveryTasksService service, TestDiscoveryStore store, TestSettingsStore settings) = Create();
|
||||||
settings.Preload("discEvalThreshold", "60");
|
settings.Preload("discEvalThreshold", "60");
|
||||||
settings.Preload("discEvalSample", "20");
|
settings.Preload("discEvalSample", "20");
|
||||||
|
|
||||||
@@ -330,12 +330,12 @@ public sealed class DiscoveryTasksServiceTests
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Создаёт связку сервис + фейки (пустые настройки → дефолты SettingsDefaults).
|
// Создаёт связку сервис + фейки (пустые настройки → дефолты SettingsDefaults).
|
||||||
private static (DiscoveryTasksService Service, TestDiscoveryStore Store, FakeSettingsStore Settings) Create()
|
private static (DiscoveryTasksService Service, TestDiscoveryStore Store, TestSettingsStore Settings) Create()
|
||||||
{
|
{
|
||||||
var store = new TestDiscoveryStore();
|
var store = new TestDiscoveryStore();
|
||||||
var settings = new FakeSettingsStore();
|
var settings = new TestSettingsStore();
|
||||||
var planGuard = new DiscoveryPlanGuard(store.Store, settings);
|
var planGuard = new DiscoveryPlanGuard(store.Store, settings.Store);
|
||||||
return (new DiscoveryTasksService(store.Store, planGuard, settings), store, settings);
|
return (new DiscoveryTasksService(store.Store, planGuard, settings.Store), store, settings);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Draft задачи с дефолтными полями (name/planJoins переопределяются).
|
// Draft задачи с дефолтными полями (name/planJoins переопределяются).
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ using Deal.Modules.Cards.Application.Sources;
|
|||||||
using Deal.Modules.Kanban.Application.Models;
|
using Deal.Modules.Kanban.Application.Models;
|
||||||
using Deal.Modules.Pipeline.Application.Services;
|
using Deal.Modules.Pipeline.Application.Services;
|
||||||
using Deal.Modules.Settings.Application.Models;
|
using Deal.Modules.Settings.Application.Models;
|
||||||
|
using Deal.Tests.Unit.Support;
|
||||||
using Deal.Modules.Settings.Application.Services;
|
using Deal.Modules.Settings.Application.Services;
|
||||||
using Deal.Tests.Unit.Modules.Cards;
|
using Deal.Tests.Unit.Modules.Cards;
|
||||||
using Deal.Tests.Unit.Modules.Settings;
|
using Deal.Tests.Unit.Modules.Settings;
|
||||||
@@ -24,11 +25,11 @@ public sealed class AiClassifyContextBuilderTests
|
|||||||
[Fact]
|
[Fact]
|
||||||
public async Task BuildFilterPrompt_FillsDomainAndKeywords()
|
public async Task BuildFilterPrompt_FillsDomainAndKeywords()
|
||||||
{
|
{
|
||||||
FakeSettingsStore settings = Context(out FakeKanjStore kanj);
|
TestSettingsStore settings = Context(out FakeKanjStore kanj);
|
||||||
settings.Preload(SettingsKeys.AiFilterPrompt, Json("Страж. Сфера: {domain}. Маркеры: {keywords}."));
|
settings.Preload(SettingsKeys.AiFilterPrompt, Json("Страж. Сфера: {domain}. Маркеры: {keywords}."));
|
||||||
settings.Preload(SettingsKeys.DomainDescription, Json(Domain));
|
settings.Preload(SettingsKeys.DomainDescription, Json(Domain));
|
||||||
settings.Preload(SettingsKeys.DomainKeywords, Json(Keywords));
|
settings.Preload(SettingsKeys.DomainKeywords, Json(Keywords));
|
||||||
var builder = new AiClassifyContextBuilder(settings, kanj);
|
var builder = new AiClassifyContextBuilder(settings.Store, kanj);
|
||||||
|
|
||||||
string prompt = await builder.BuildFilterPromptAsync(CancellationToken.None);
|
string prompt = await builder.BuildFilterPromptAsync(CancellationToken.None);
|
||||||
|
|
||||||
@@ -38,9 +39,9 @@ public sealed class AiClassifyContextBuilderTests
|
|||||||
[Fact]
|
[Fact]
|
||||||
public async Task BuildFilterPrompt_NoDomain_UsesFallbackTextAndHint()
|
public async Task BuildFilterPrompt_NoDomain_UsesFallbackTextAndHint()
|
||||||
{
|
{
|
||||||
FakeSettingsStore settings = Context(out FakeKanjStore kanj);
|
TestSettingsStore settings = Context(out FakeKanjStore kanj);
|
||||||
settings.Preload(SettingsKeys.AiFilterPrompt, Json("[{domain}] {keywords}"));
|
settings.Preload(SettingsKeys.AiFilterPrompt, Json("[{domain}] {keywords}"));
|
||||||
var builder = new AiClassifyContextBuilder(settings, kanj);
|
var builder = new AiClassifyContextBuilder(settings.Store, kanj);
|
||||||
|
|
||||||
string prompt = await builder.BuildFilterPromptAsync(CancellationToken.None);
|
string prompt = await builder.BuildFilterPromptAsync(CancellationToken.None);
|
||||||
|
|
||||||
@@ -52,12 +53,12 @@ public sealed class AiClassifyContextBuilderTests
|
|||||||
[Fact]
|
[Fact]
|
||||||
public async Task BuildClassifySystemPrompt_AppendsCardPromptWhenSet()
|
public async Task BuildClassifySystemPrompt_AppendsCardPromptWhenSet()
|
||||||
{
|
{
|
||||||
FakeSettingsStore settings = Context(out FakeKanjStore kanj);
|
TestSettingsStore settings = Context(out FakeKanjStore kanj);
|
||||||
settings.Preload(SettingsKeys.AiPrompt, Json("Классифицируй {domain}."));
|
settings.Preload(SettingsKeys.AiPrompt, Json("Классифицируй {domain}."));
|
||||||
settings.Preload(SettingsKeys.CardPrompt, Json("Верни блок «О заявке» {keywords}."));
|
settings.Preload(SettingsKeys.CardPrompt, Json("Верни блок «О заявке» {keywords}."));
|
||||||
settings.Preload(SettingsKeys.DomainDescription, Json(Domain));
|
settings.Preload(SettingsKeys.DomainDescription, Json(Domain));
|
||||||
settings.Preload(SettingsKeys.DomainKeywords, Json(Keywords));
|
settings.Preload(SettingsKeys.DomainKeywords, Json(Keywords));
|
||||||
var builder = new AiClassifyContextBuilder(settings, kanj);
|
var builder = new AiClassifyContextBuilder(settings.Store, kanj);
|
||||||
|
|
||||||
string prompt = await builder.BuildClassifySystemPromptAsync(CancellationToken.None);
|
string prompt = await builder.BuildClassifySystemPromptAsync(CancellationToken.None);
|
||||||
|
|
||||||
@@ -69,8 +70,8 @@ public sealed class AiClassifyContextBuilderTests
|
|||||||
[Fact]
|
[Fact]
|
||||||
public async Task BuildClassifyUserContext_EmptyBoards_ShowsNoBoardsPhrase()
|
public async Task BuildClassifyUserContext_EmptyBoards_ShowsNoBoardsPhrase()
|
||||||
{
|
{
|
||||||
FakeSettingsStore settings = Context(out FakeKanjStore kanj);
|
TestSettingsStore settings = Context(out FakeKanjStore kanj);
|
||||||
var builder = new AiClassifyContextBuilder(settings, kanj);
|
var builder = new AiClassifyContextBuilder(settings.Store, kanj);
|
||||||
|
|
||||||
string context = await builder.BuildClassifyUserContextAsync("Нужен разработчик", CancellationToken.None);
|
string context = await builder.BuildClassifyUserContextAsync("Нужен разработчик", CancellationToken.None);
|
||||||
|
|
||||||
@@ -82,7 +83,7 @@ public sealed class AiClassifyContextBuilderTests
|
|||||||
[Fact]
|
[Fact]
|
||||||
public async Task BuildClassifyUserContext_ListsBoardsWithKeywordsAndRules()
|
public async Task BuildClassifyUserContext_ListsBoardsWithKeywordsAndRules()
|
||||||
{
|
{
|
||||||
FakeSettingsStore settings = Context(out FakeKanjStore kanj);
|
TestSettingsStore settings = Context(out FakeKanjStore kanj);
|
||||||
kanj.SeedBoard(new ContainerDto
|
kanj.SeedBoard(new ContainerDto
|
||||||
{
|
{
|
||||||
Id = "b_py",
|
Id = "b_py",
|
||||||
@@ -113,7 +114,7 @@ public sealed class AiClassifyContextBuilderTests
|
|||||||
Order = 1,
|
Order = 1,
|
||||||
});
|
});
|
||||||
kanj.SeedBoard(new ContainerDto { Id = "b_sug", Name = "Предложение", Suggested = true, Order = 2 });
|
kanj.SeedBoard(new ContainerDto { Id = "b_sug", Name = "Предложение", Suggested = true, Order = 2 });
|
||||||
var builder = new AiClassifyContextBuilder(settings, kanj);
|
var builder = new AiClassifyContextBuilder(settings.Store, kanj);
|
||||||
|
|
||||||
string context = await builder.BuildClassifyUserContextAsync("Ищу Python-разработчика", CancellationToken.None);
|
string context = await builder.BuildClassifyUserContextAsync("Ищу Python-разработчика", CancellationToken.None);
|
||||||
|
|
||||||
@@ -128,10 +129,10 @@ public sealed class AiClassifyContextBuilderTests
|
|||||||
[Fact]
|
[Fact]
|
||||||
public async Task BuildClassifyUserContext_IncludesMarkupExamplesNewestFirst()
|
public async Task BuildClassifyUserContext_IncludesMarkupExamplesNewestFirst()
|
||||||
{
|
{
|
||||||
FakeSettingsStore settings = Context(out FakeKanjStore kanj);
|
TestSettingsStore settings = Context(out FakeKanjStore kanj);
|
||||||
SeedCardWithMove(kanj, "l_1", "b_py", "Пример: middle python (старый)");
|
SeedCardWithMove(kanj, "l_1", "b_py", "Пример: middle python (старый)");
|
||||||
SeedCardWithMove(kanj, "l_2", "b_py", "Пример: senior go (свежий)");
|
SeedCardWithMove(kanj, "l_2", "b_py", "Пример: senior go (свежий)");
|
||||||
var builder = new AiClassifyContextBuilder(settings, kanj);
|
var builder = new AiClassifyContextBuilder(settings.Store, kanj);
|
||||||
|
|
||||||
string context = await builder.BuildClassifyUserContextAsync("Ищу разработчика", CancellationToken.None);
|
string context = await builder.BuildClassifyUserContextAsync("Ищу разработчика", CancellationToken.None);
|
||||||
|
|
||||||
@@ -144,8 +145,8 @@ public sealed class AiClassifyContextBuilderTests
|
|||||||
[Fact]
|
[Fact]
|
||||||
public async Task BuildClassifyUserContext_LongMessage_TruncatesTo5000CodePoints()
|
public async Task BuildClassifyUserContext_LongMessage_TruncatesTo5000CodePoints()
|
||||||
{
|
{
|
||||||
FakeSettingsStore settings = Context(out FakeKanjStore kanj);
|
TestSettingsStore settings = Context(out FakeKanjStore kanj);
|
||||||
var builder = new AiClassifyContextBuilder(settings, kanj);
|
var builder = new AiClassifyContextBuilder(settings.Store, kanj);
|
||||||
string text = new string('д', 6000);
|
string text = new string('д', 6000);
|
||||||
|
|
||||||
string context = await builder.BuildClassifyUserContextAsync(text, CancellationToken.None);
|
string context = await builder.BuildClassifyUserContextAsync(text, CancellationToken.None);
|
||||||
@@ -158,10 +159,10 @@ public sealed class AiClassifyContextBuilderTests
|
|||||||
// Создаёт контекст: пустое KV + пустой канбан-фейк.
|
// Создаёт контекст: пустое KV + пустой канбан-фейк.
|
||||||
// kanj: Канбан-фейк (доски/журнал примеров).
|
// kanj: Канбан-фейк (доски/журнал примеров).
|
||||||
// Возвращает: KV-хранилище тенанта.
|
// Возвращает: KV-хранилище тенанта.
|
||||||
private static FakeSettingsStore Context(out FakeKanjStore kanj)
|
private static TestSettingsStore Context(out FakeKanjStore kanj)
|
||||||
{
|
{
|
||||||
kanj = new FakeKanjStore();
|
kanj = new FakeKanjStore();
|
||||||
return new FakeSettingsStore();
|
return new TestSettingsStore();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Кладёт карточку с исходным текстом и запись журнала move на неё (источник few-shot-примера).
|
// Кладёт карточку с исходным текстом и запись журнала move на неё (источник few-shot-примера).
|
||||||
|
|||||||
@@ -186,16 +186,16 @@ public sealed class CardsServiceRemindersTests
|
|||||||
|
|
||||||
// ─── Хелперы ──────────────────────────────────────────────────────────
|
// ─── Хелперы ──────────────────────────────────────────────────────────
|
||||||
|
|
||||||
private static (CardsService Service, FakeKanjStore Store, FakeSettingsStore Settings) Create(bool remindersEnabled = true)
|
private static (CardsService Service, FakeKanjStore Store, TestSettingsStore Settings) Create(bool remindersEnabled = true)
|
||||||
{
|
{
|
||||||
var store = new FakeKanjStore();
|
var store = new FakeKanjStore();
|
||||||
var settings = new FakeSettingsStore();
|
var settings = new TestSettingsStore();
|
||||||
if (!remindersEnabled)
|
if (!remindersEnabled)
|
||||||
{
|
{
|
||||||
settings.Preload(SettingsKeys.RemindersEnabled, "false");
|
settings.Preload(SettingsKeys.RemindersEnabled, "false");
|
||||||
}
|
}
|
||||||
|
|
||||||
return (new CardsService(store, settings, new TestMlClient().Client, new TestFileStorage().Storage), store, settings);
|
return (new CardsService(store, settings.Store, new TestMlClient().Client, new TestFileStorage().Storage), store, settings);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Текущее время в epoch-мс (UTC) — для посева напоминаний в прошлом/будущем.
|
// Текущее время в epoch-мс (UTC) — для посева напоминаний в прошлом/будущем.
|
||||||
|
|||||||
@@ -24,8 +24,8 @@ public sealed class MlReviewServiceTests
|
|||||||
TestMlClient ml,
|
TestMlClient ml,
|
||||||
out CardsService cards)
|
out CardsService cards)
|
||||||
{
|
{
|
||||||
var settings = new FakeSettingsStore();
|
var settings = new TestSettingsStore();
|
||||||
cards = new CardsService(kanj, settings, ml.Client, new TestFileStorage().Storage);
|
cards = new CardsService(kanj, settings.Store, ml.Client, new TestFileStorage().Storage);
|
||||||
var processing = new PipelineProcessingService(pipeline.Store, ml.Client, 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.Client);
|
return new MlReviewService(pipeline.Store, kanj, cards, processing, ml.Client);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
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.Settings.Application.Models;
|
using Deal.Modules.Settings.Application.Models;
|
||||||
|
using Deal.Tests.Unit.Support;
|
||||||
using Deal.Tests.Unit.Modules.Cards;
|
using Deal.Tests.Unit.Modules.Cards;
|
||||||
using Deal.Tests.Unit.Modules.Settings;
|
using Deal.Tests.Unit.Modules.Settings;
|
||||||
|
|
||||||
@@ -47,7 +48,7 @@ public sealed class StorageTickServiceTests
|
|||||||
[Fact]
|
[Fact]
|
||||||
public async Task Tick_AutoArchiveSettingFalse_KeepsExpiredCardsInPlace()
|
public async Task Tick_AutoArchiveSettingFalse_KeepsExpiredCardsInPlace()
|
||||||
{
|
{
|
||||||
(StorageTickService service, FakeKanjStore store, FakeSettingsStore settings) = Create();
|
(StorageTickService service, FakeKanjStore store, TestSettingsStore settings) = Create();
|
||||||
settings.Preload(SettingsKeys.AutoArchive, "false");
|
settings.Preload(SettingsKeys.AutoArchive, "false");
|
||||||
store.SeedBoard(Board("b_1"));
|
store.SeedBoard(Board("b_1"));
|
||||||
store.SeedCard(Card("l_old", "b_1", ReceivedAtMsAgo(TimeSpan.FromDays(30))));
|
store.SeedCard(Card("l_old", "b_1", ReceivedAtMsAgo(TimeSpan.FromDays(30))));
|
||||||
@@ -62,7 +63,7 @@ public sealed class StorageTickServiceTests
|
|||||||
[Fact]
|
[Fact]
|
||||||
public async Task Tick_ArchiveAfterDays30_ArchivesOnlyCardsOlderThanConfiguredMonth()
|
public async Task Tick_ArchiveAfterDays30_ArchivesOnlyCardsOlderThanConfiguredMonth()
|
||||||
{
|
{
|
||||||
(StorageTickService service, FakeKanjStore store, FakeSettingsStore settings) = Create();
|
(StorageTickService service, FakeKanjStore store, TestSettingsStore settings) = Create();
|
||||||
settings.Preload(SettingsKeys.ArchiveAfterDays, "30");
|
settings.Preload(SettingsKeys.ArchiveAfterDays, "30");
|
||||||
store.SeedBoard(Board("b_1"));
|
store.SeedBoard(Board("b_1"));
|
||||||
store.SeedCard(Card("l_older_month", "b_1", ReceivedAtMsAgo(TimeSpan.FromDays(32))));
|
store.SeedCard(Card("l_older_month", "b_1", ReceivedAtMsAgo(TimeSpan.FromDays(32))));
|
||||||
@@ -78,7 +79,7 @@ public sealed class StorageTickServiceTests
|
|||||||
[Fact]
|
[Fact]
|
||||||
public async Task Tick_ArchiveAfterDays1_ArchivesTwoDayOldCard()
|
public async Task Tick_ArchiveAfterDays1_ArchivesTwoDayOldCard()
|
||||||
{
|
{
|
||||||
(StorageTickService service, FakeKanjStore store, FakeSettingsStore settings) = Create();
|
(StorageTickService service, FakeKanjStore store, TestSettingsStore settings) = Create();
|
||||||
settings.Preload(SettingsKeys.ArchiveAfterDays, "1");
|
settings.Preload(SettingsKeys.ArchiveAfterDays, "1");
|
||||||
store.SeedCard(Card("l_inbox_old", KanbanColumns.Inbox, ReceivedAtMsAgo(TimeSpan.FromDays(2))));
|
store.SeedCard(Card("l_inbox_old", KanbanColumns.Inbox, ReceivedAtMsAgo(TimeSpan.FromDays(2))));
|
||||||
|
|
||||||
@@ -129,7 +130,7 @@ public sealed class StorageTickServiceTests
|
|||||||
[Fact]
|
[Fact]
|
||||||
public async Task Tick_ArchiveClearDaysFromSettings_ControlsArchivePurgeBoundary()
|
public async Task Tick_ArchiveClearDaysFromSettings_ControlsArchivePurgeBoundary()
|
||||||
{
|
{
|
||||||
(StorageTickService service, FakeKanjStore store, FakeSettingsStore settings) = Create();
|
(StorageTickService service, FakeKanjStore store, TestSettingsStore settings) = Create();
|
||||||
settings.Preload(SettingsKeys.ArchiveClearDays, "30");
|
settings.Preload(SettingsKeys.ArchiveClearDays, "30");
|
||||||
DateTimeOffset now = DateTimeOffset.UtcNow;
|
DateTimeOffset now = DateTimeOffset.UtcNow;
|
||||||
store.SeedCard(Card("l_expired", KanbanColumns.Archive, ReceivedAtMsAgo(TimeSpan.FromDays(10))));
|
store.SeedCard(Card("l_expired", KanbanColumns.Archive, ReceivedAtMsAgo(TimeSpan.FromDays(10))));
|
||||||
@@ -165,7 +166,7 @@ public sealed class StorageTickServiceTests
|
|||||||
[Fact]
|
[Fact]
|
||||||
public async Task Tick_TrashClearDaysFromSettings_ControlsTrashPurgeBoundary()
|
public async Task Tick_TrashClearDaysFromSettings_ControlsTrashPurgeBoundary()
|
||||||
{
|
{
|
||||||
(StorageTickService service, FakeKanjStore store, FakeSettingsStore settings) = Create();
|
(StorageTickService service, FakeKanjStore store, TestSettingsStore settings) = Create();
|
||||||
settings.Preload(SettingsKeys.TrashClearDays, "3");
|
settings.Preload(SettingsKeys.TrashClearDays, "3");
|
||||||
store.SeedCard(Card("l_old", KanbanColumns.Trash, ReceivedAtMsAgo(TimeSpan.FromDays(5)))); // старше 3 дн.
|
store.SeedCard(Card("l_old", KanbanColumns.Trash, ReceivedAtMsAgo(TimeSpan.FromDays(5)))); // старше 3 дн.
|
||||||
store.SeedCard(Card("l_fresh", KanbanColumns.Trash, ReceivedAtMsAgo(TimeSpan.FromDays(1))));
|
store.SeedCard(Card("l_fresh", KanbanColumns.Trash, ReceivedAtMsAgo(TimeSpan.FromDays(1))));
|
||||||
@@ -194,11 +195,11 @@ public sealed class StorageTickServiceTests
|
|||||||
|
|
||||||
// ─── Хелперы ────────────────────────────────────────────────────────────
|
// ─── Хелперы ────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
private static (StorageTickService Service, FakeKanjStore Store, FakeSettingsStore Settings) Create()
|
private static (StorageTickService Service, FakeKanjStore Store, TestSettingsStore Settings) Create()
|
||||||
{
|
{
|
||||||
var store = new FakeKanjStore();
|
var store = new FakeKanjStore();
|
||||||
var settings = new FakeSettingsStore();
|
var settings = new TestSettingsStore();
|
||||||
return (new StorageTickService(store, settings), store, settings);
|
return (new StorageTickService(store, settings.Store), store, settings);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Доска-колонка минимально (id/имя достаточно для списка кандидатов фейка).
|
// Доска-колонка минимально (id/имя достаточно для списка кандидатов фейка).
|
||||||
|
|||||||
@@ -1,63 +0,0 @@
|
|||||||
using Deal.Modules.Settings.Application.Abstractions;
|
|
||||||
using Deal.Modules.Settings.Application.Models;
|
|
||||||
|
|
||||||
namespace Deal.Tests.Unit.Modules.Settings;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// In-memory реализация <see cref="ISettingsStore"/> для юнит-тестов SettingsService.
|
|
||||||
/// </summary>
|
|
||||||
public sealed class FakeSettingsStore : ISettingsStore
|
|
||||||
{
|
|
||||||
private readonly Dictionary<string, SettingValue> _rows = new(StringComparer.Ordinal);
|
|
||||||
|
|
||||||
public IReadOnlyCollection<string> Keys => _rows.Keys.ToList();
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Кладёт готовую строку
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="key">Ключ настройки.</param>
|
|
||||||
/// <param name="valueJson">Значение, сериализованное в JSON.</param>
|
|
||||||
public void Preload(string key, string valueJson)
|
|
||||||
{
|
|
||||||
_rows[key] = new SettingValue(key, valueJson, DateTimeOffset.UtcNow);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Возвращает JSON сохранённого значения или null, если ключа нет.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="key">Ключ настройки.</param>
|
|
||||||
/// <returns>JSON-строка значения или null.</returns>
|
|
||||||
public string? GetStoredJson(string key)
|
|
||||||
{
|
|
||||||
return _rows.TryGetValue(key, out SettingValue? row) ? row.ValueJson : null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public Task<SettingValue?> GetAsync(string key, CancellationToken ct)
|
|
||||||
{
|
|
||||||
return Task.FromResult(_rows.TryGetValue(key, out SettingValue? row) ? row : null);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public Task<IReadOnlyCollection<SettingValue>> GetAllAsync(CancellationToken ct)
|
|
||||||
{
|
|
||||||
return Task.FromResult<IReadOnlyCollection<SettingValue>>(_rows.Values.ToList());
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public Task SetAsync(
|
|
||||||
string key,
|
|
||||||
string valueJson,
|
|
||||||
CancellationToken ct)
|
|
||||||
{
|
|
||||||
_rows[key] = new SettingValue(key, valueJson, DateTimeOffset.UtcNow);
|
|
||||||
return Task.CompletedTask;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public Task RemoveAsync(string key, CancellationToken ct)
|
|
||||||
{
|
|
||||||
_rows.Remove(key);
|
|
||||||
return Task.CompletedTask;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -32,7 +32,7 @@ public sealed class AdminTickOrchestratorTests
|
|||||||
AdminTickOrchestrator Orchestrator,
|
AdminTickOrchestrator Orchestrator,
|
||||||
TestPipelineStore PipelineStore,
|
TestPipelineStore PipelineStore,
|
||||||
FakeKanjStore KanjStore,
|
FakeKanjStore KanjStore,
|
||||||
FakeSettingsStore Settings,
|
TestSettingsStore Settings,
|
||||||
TestAiClassifier AiClassifier,
|
TestAiClassifier AiClassifier,
|
||||||
PipelinePumpGate PumpGate,
|
PipelinePumpGate PumpGate,
|
||||||
SseSubscription Subscription);
|
SseSubscription Subscription);
|
||||||
@@ -202,7 +202,7 @@ public sealed class AdminTickOrchestratorTests
|
|||||||
// Возвращает: Оркестратор, фейки и подписку канала тенанта.
|
// Возвращает: Оркестратор, фейки и подписку канала тенанта.
|
||||||
private static Context CreateContext(bool withThrowingQueueRead = false, bool withThrowingReminderCheck = false)
|
private static Context CreateContext(bool withThrowingQueueRead = false, bool withThrowingReminderCheck = false)
|
||||||
{
|
{
|
||||||
var settings = new FakeSettingsStore();
|
var settings = new TestSettingsStore();
|
||||||
// При сбое чтения очереди подменяем хранилище целиком (тот же объект идёт во все сервисы и в контекст).
|
// При сбое чтения очереди подменяем хранилище целиком (тот же объект идёт во все сервисы и в контекст).
|
||||||
TestPipelineStore store = new();
|
TestPipelineStore store = new();
|
||||||
if (withThrowingQueueRead)
|
if (withThrowingQueueRead)
|
||||||
@@ -214,21 +214,21 @@ public sealed class AdminTickOrchestratorTests
|
|||||||
var kanjStore = new FakeKanjStore();
|
var kanjStore = new FakeKanjStore();
|
||||||
var mlClient = new TestMlClient();
|
var mlClient = new TestMlClient();
|
||||||
var aiClassifier = new TestAiClassifier();
|
var aiClassifier = new TestAiClassifier();
|
||||||
var rules = new IncomingRules(settings);
|
var rules = new IncomingRules(settings.Store);
|
||||||
var fieldsParser = new LocalFieldsParser(settings);
|
var fieldsParser = new LocalFieldsParser(settings.Store);
|
||||||
var ingest = new PipelineIngestService(store.Store);
|
var ingest = new PipelineIngestService(store.Store);
|
||||||
var processing = new PipelineProcessingService(store.Store, mlClient.Client, ingest);
|
var processing = new PipelineProcessingService(store.Store, mlClient.Client, ingest);
|
||||||
var composer = new CardComposer(kanjStore, settings);
|
var composer = new CardComposer(kanjStore, settings.Store);
|
||||||
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.Client, aiClassifier.Classifier, processing, writer, fieldsParser);
|
store.Store, settings.Store, 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.Store);
|
||||||
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.Client, new TestFileStorage().Storage);
|
var cardsService = new CardsService(reminderStore, settings.Store, 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);
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ public sealed class CardReclassifierTests
|
|||||||
private sealed record Context(
|
private sealed record Context(
|
||||||
CardReclassifier Reclassifier,
|
CardReclassifier Reclassifier,
|
||||||
FakeKanjStore Store,
|
FakeKanjStore Store,
|
||||||
FakeSettingsStore Settings,
|
TestSettingsStore Settings,
|
||||||
TestMlClient MlClient,
|
TestMlClient MlClient,
|
||||||
TestAiClassifier AiClassifier,
|
TestAiClassifier AiClassifier,
|
||||||
ReclassifyGate Gate);
|
ReclassifyGate Gate);
|
||||||
@@ -32,16 +32,16 @@ public sealed class CardReclassifierTests
|
|||||||
// Возвращает: Сервис и фейки (сценарий до-настраивает карточки/доски/настройки).
|
// Возвращает: Сервис и фейки (сценарий до-настраивает карточки/доски/настройки).
|
||||||
private static Context CreateContext()
|
private static Context CreateContext()
|
||||||
{
|
{
|
||||||
var settings = new FakeSettingsStore();
|
var settings = new TestSettingsStore();
|
||||||
var store = new FakeKanjStore();
|
var store = new FakeKanjStore();
|
||||||
var mlClient = new TestMlClient();
|
var mlClient = new TestMlClient();
|
||||||
var aiClassifier = new TestAiClassifier();
|
var aiClassifier = new TestAiClassifier();
|
||||||
var fieldsParser = new LocalFieldsParser(settings);
|
var fieldsParser = new LocalFieldsParser(settings.Store);
|
||||||
var composer = new CardComposer(store, settings);
|
var composer = new CardComposer(store, settings.Store);
|
||||||
var cardsService = new CardsService(store, settings, mlClient.Client, new TestFileStorage().Storage);
|
var cardsService = new CardsService(store, settings.Store, 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.Client, gate);
|
store, settings.Store, 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 TestMlClient().Client, storage.Storage), store, storage);
|
return (new CardsService(store, new TestSettingsStore().Store, 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, TestMlClient Ml) Create()
|
private static (CardsService Service, FakeKanjStore Store, TestSettingsStore Settings, TestMlClient Ml) Create()
|
||||||
{
|
{
|
||||||
var store = new FakeKanjStore();
|
var store = new FakeKanjStore();
|
||||||
var settings = new FakeSettingsStore();
|
var settings = new TestSettingsStore();
|
||||||
var ml = new TestMlClient();
|
var ml = new TestMlClient();
|
||||||
return (new CardsService(store, settings, ml.Client, new TestFileStorage().Storage), store, settings, ml);
|
return (new CardsService(store, settings.Store, ml.Client, new TestFileStorage().Storage), store, settings, ml);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Тело PATCH из пар «ключ → значение» (presence = наличие пары; null — явный JSON-null).
|
// Тело PATCH из пар «ключ → значение» (presence = наличие пары; null — явный JSON-null).
|
||||||
|
|||||||
@@ -340,7 +340,7 @@ public sealed class ContainersServiceTests
|
|||||||
[Fact]
|
[Fact]
|
||||||
public async Task GetColState_ReturnsStoredColumnsAsIs()
|
public async Task GetColState_ReturnsStoredColumnsAsIs()
|
||||||
{
|
{
|
||||||
(ContainersService service, _, FakeSettingsStore settings) = Create();
|
(ContainersService service, _, TestSettingsStore settings) = Create();
|
||||||
settings.Preload("colState", """{"inbox":{"collapsed":true},"trash":{"width":"lg"}}""");
|
settings.Preload("colState", """{"inbox":{"collapsed":true},"trash":{"width":"lg"}}""");
|
||||||
|
|
||||||
IReadOnlyDictionary<string, ColumnStateDto> state = await service.GetColStateAsync(CancellationToken.None);
|
IReadOnlyDictionary<string, ColumnStateDto> state = await service.GetColStateAsync(CancellationToken.None);
|
||||||
@@ -353,7 +353,7 @@ public sealed class ContainersServiceTests
|
|||||||
[Fact]
|
[Fact]
|
||||||
public async Task PatchColState_NewColumn_WritesStateAndReturnsIt()
|
public async Task PatchColState_NewColumn_WritesStateAndReturnsIt()
|
||||||
{
|
{
|
||||||
(ContainersService service, _, FakeSettingsStore settings) = Create();
|
(ContainersService service, _, TestSettingsStore settings) = Create();
|
||||||
|
|
||||||
ColumnStateDto result = await service.PatchColStateAsync(
|
ColumnStateDto result = await service.PatchColStateAsync(
|
||||||
"inbox", new ColumnStateDto(Collapsed: true, Width: null), CancellationToken.None);
|
"inbox", new ColumnStateDto(Collapsed: true, Width: null), CancellationToken.None);
|
||||||
@@ -368,7 +368,7 @@ public sealed class ContainersServiceTests
|
|||||||
[Fact]
|
[Fact]
|
||||||
public async Task PatchColState_ExistingColumn_MergesPatchIntoCurrent()
|
public async Task PatchColState_ExistingColumn_MergesPatchIntoCurrent()
|
||||||
{
|
{
|
||||||
(ContainersService service, _, FakeSettingsStore settings) = Create();
|
(ContainersService service, _, TestSettingsStore settings) = Create();
|
||||||
settings.Preload("colState", """{"inbox":{"collapsed":true}}""");
|
settings.Preload("colState", """{"inbox":{"collapsed":true}}""");
|
||||||
|
|
||||||
ColumnStateDto result = await service.PatchColStateAsync(
|
ColumnStateDto result = await service.PatchColStateAsync(
|
||||||
@@ -380,7 +380,7 @@ public sealed class ContainersServiceTests
|
|||||||
[Fact]
|
[Fact]
|
||||||
public async Task PatchColState_NullFields_KeepCurrentState()
|
public async Task PatchColState_NullFields_KeepCurrentState()
|
||||||
{
|
{
|
||||||
(ContainersService service, _, FakeSettingsStore settings) = Create();
|
(ContainersService service, _, TestSettingsStore settings) = Create();
|
||||||
settings.Preload("colState", """{"archive":{"collapsed":true,"width":"sm"}}""");
|
settings.Preload("colState", """{"archive":{"collapsed":true,"width":"sm"}}""");
|
||||||
|
|
||||||
ColumnStateDto result = await service.PatchColStateAsync(
|
ColumnStateDto result = await service.PatchColStateAsync(
|
||||||
@@ -392,7 +392,7 @@ public sealed class ContainersServiceTests
|
|||||||
[Fact]
|
[Fact]
|
||||||
public async Task PatchColState_EmptyPatch_KeepsOtherColumnsAndWritesEmptyEntry()
|
public async Task PatchColState_EmptyPatch_KeepsOtherColumnsAndWritesEmptyEntry()
|
||||||
{
|
{
|
||||||
(ContainersService service, _, FakeSettingsStore settings) = Create();
|
(ContainersService service, _, TestSettingsStore settings) = Create();
|
||||||
settings.Preload("colState", """{"archive":{"collapsed":true}}""");
|
settings.Preload("colState", """{"archive":{"collapsed":true}}""");
|
||||||
|
|
||||||
await service.PatchColStateAsync("inbox", new ColumnStateDto(Collapsed: null, Width: null), CancellationToken.None);
|
await service.PatchColStateAsync("inbox", new ColumnStateDto(Collapsed: null, Width: null), CancellationToken.None);
|
||||||
@@ -408,7 +408,7 @@ public sealed class ContainersServiceTests
|
|||||||
[Fact]
|
[Fact]
|
||||||
public async Task GetColState_CorruptJson_ReturnsEmpty()
|
public async Task GetColState_CorruptJson_ReturnsEmpty()
|
||||||
{
|
{
|
||||||
(ContainersService service, _, FakeSettingsStore settings) = Create();
|
(ContainersService service, _, TestSettingsStore settings) = Create();
|
||||||
settings.Preload("colState", "{oops");
|
settings.Preload("colState", "{oops");
|
||||||
|
|
||||||
IReadOnlyDictionary<string, ColumnStateDto> state = await service.GetColStateAsync(CancellationToken.None);
|
IReadOnlyDictionary<string, ColumnStateDto> state = await service.GetColStateAsync(CancellationToken.None);
|
||||||
@@ -418,11 +418,11 @@ public sealed class ContainersServiceTests
|
|||||||
|
|
||||||
// ─── Хелперы ──────────────────────────────────────────────────────────
|
// ─── Хелперы ──────────────────────────────────────────────────────────
|
||||||
|
|
||||||
private static (ContainersService Service, FakeKanjStore Store, FakeSettingsStore Settings) Create()
|
private static (ContainersService Service, FakeKanjStore Store, TestSettingsStore Settings) Create()
|
||||||
{
|
{
|
||||||
var store = new FakeKanjStore();
|
var store = new FakeKanjStore();
|
||||||
var settings = new FakeSettingsStore();
|
var settings = new TestSettingsStore();
|
||||||
return (new ContainersService(store, settings), store, settings);
|
return (new ContainersService(store, settings.Store), store, settings);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Патч только с нужными полями: остальные параметры null = «не менять».
|
// Патч только с нужными полями: остальные параметры null = «не менять».
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ using Deal.Modules.Kanban.Application.Models;
|
|||||||
using Deal.Modules.Kanban.Application.Services;
|
using Deal.Modules.Kanban.Application.Services;
|
||||||
using Deal.Modules.Settings.Application.Abstractions;
|
using Deal.Modules.Settings.Application.Abstractions;
|
||||||
using Deal.Modules.Settings.Application.Models;
|
using Deal.Modules.Settings.Application.Models;
|
||||||
|
using Deal.Tests.Unit.Support;
|
||||||
using Deal.Modules.Settings.Application.Services;
|
using Deal.Modules.Settings.Application.Services;
|
||||||
using Deal.Tests.Unit.Modules.Cards;
|
using Deal.Tests.Unit.Modules.Cards;
|
||||||
using Deal.Tests.Unit.Modules.Settings;
|
using Deal.Tests.Unit.Modules.Settings;
|
||||||
@@ -19,7 +20,7 @@ public sealed class ConversionRecomputerTests
|
|||||||
[Fact]
|
[Fact]
|
||||||
public async Task Recompute_MockRates_ConvertsUsdBudgetToRub()
|
public async Task Recompute_MockRates_ConvertsUsdBudgetToRub()
|
||||||
{
|
{
|
||||||
(ConversionRecomputer service, FakeKanjStore store, FakeSettingsStore settings) = Create();
|
(ConversionRecomputer service, FakeKanjStore store, TestSettingsStore settings) = Create();
|
||||||
settings.Preload(SettingsKeys.RatesCache, CacheJson(MockRates.Values));
|
settings.Preload(SettingsKeys.RatesCache, CacheJson(MockRates.Values));
|
||||||
store.SeedCard(Card("l_board", "b_1", from: 100, to: 100, cur: "USD"));
|
store.SeedCard(Card("l_board", "b_1", from: 100, to: 100, cur: "USD"));
|
||||||
store.SeedCard(Card("l_inbox", KanbanColumns.Inbox, from: 50, to: 200, cur: "USD"));
|
store.SeedCard(Card("l_inbox", KanbanColumns.Inbox, from: 50, to: 200, cur: "USD"));
|
||||||
@@ -34,7 +35,7 @@ public sealed class ConversionRecomputerTests
|
|||||||
[Fact]
|
[Fact]
|
||||||
public async Task Recompute_TargetCurrencyFromSettings_ConvertsToConfiguredCurrency()
|
public async Task Recompute_TargetCurrencyFromSettings_ConvertsToConfiguredCurrency()
|
||||||
{
|
{
|
||||||
(ConversionRecomputer service, FakeKanjStore store, FakeSettingsStore settings) = Create();
|
(ConversionRecomputer service, FakeKanjStore store, TestSettingsStore settings) = Create();
|
||||||
settings.Preload(SettingsKeys.RatesCache, CacheJson(MockRates.Values));
|
settings.Preload(SettingsKeys.RatesCache, CacheJson(MockRates.Values));
|
||||||
settings.Preload(SettingsKeys.TargetCurrency, "\"EUR\""); // PATCH пишет Upper (ApplyStringKey)
|
settings.Preload(SettingsKeys.TargetCurrency, "\"EUR\""); // PATCH пишет Upper (ApplyStringKey)
|
||||||
store.SeedCard(Card("l_board", "b_1", from: 100, to: 100, cur: "USD"));
|
store.SeedCard(Card("l_board", "b_1", from: 100, to: 100, cur: "USD"));
|
||||||
@@ -49,7 +50,7 @@ public sealed class ConversionRecomputerTests
|
|||||||
[Fact]
|
[Fact]
|
||||||
public async Task Recompute_NoTargetCurrencySetting_DefaultsToRub()
|
public async Task Recompute_NoTargetCurrencySetting_DefaultsToRub()
|
||||||
{
|
{
|
||||||
(ConversionRecomputer service, FakeKanjStore store, FakeSettingsStore settings) = Create();
|
(ConversionRecomputer service, FakeKanjStore store, TestSettingsStore settings) = Create();
|
||||||
settings.Preload(SettingsKeys.RatesCache, CacheJson(MockRates.Values));
|
settings.Preload(SettingsKeys.RatesCache, CacheJson(MockRates.Values));
|
||||||
store.SeedCard(Card("l_board", "b_1", from: 100, to: 100, cur: "USD"));
|
store.SeedCard(Card("l_board", "b_1", from: 100, to: 100, cur: "USD"));
|
||||||
|
|
||||||
@@ -62,7 +63,7 @@ public sealed class ConversionRecomputerTests
|
|||||||
[Fact]
|
[Fact]
|
||||||
public async Task Recompute_UsdtBudget_TreatedAsUsdRate()
|
public async Task Recompute_UsdtBudget_TreatedAsUsdRate()
|
||||||
{
|
{
|
||||||
(ConversionRecomputer service, FakeKanjStore store, FakeSettingsStore settings) = Create();
|
(ConversionRecomputer service, FakeKanjStore store, TestSettingsStore settings) = Create();
|
||||||
var rates = new Dictionary<string, double> { ["RUB"] = 1.0, ["USD"] = 100.0, ["USDT"] = 90.0 };
|
var rates = new Dictionary<string, double> { ["RUB"] = 1.0, ["USD"] = 100.0, ["USDT"] = 90.0 };
|
||||||
settings.Preload(SettingsKeys.RatesCache, CacheJson(rates));
|
settings.Preload(SettingsKeys.RatesCache, CacheJson(rates));
|
||||||
store.SeedCard(Card("l_board", "b_1", from: 100, to: 100, cur: "USDT"));
|
store.SeedCard(Card("l_board", "b_1", from: 100, to: 100, cur: "USDT"));
|
||||||
@@ -78,7 +79,7 @@ public sealed class ConversionRecomputerTests
|
|||||||
[Fact]
|
[Fact]
|
||||||
public async Task Recompute_BudgetWithoutUpperBound_UsesLowerBoundForConvTo()
|
public async Task Recompute_BudgetWithoutUpperBound_UsesLowerBoundForConvTo()
|
||||||
{
|
{
|
||||||
(ConversionRecomputer service, FakeKanjStore store, FakeSettingsStore settings) = Create();
|
(ConversionRecomputer service, FakeKanjStore store, TestSettingsStore settings) = Create();
|
||||||
settings.Preload(SettingsKeys.RatesCache, CacheJson(MockRates.Values));
|
settings.Preload(SettingsKeys.RatesCache, CacheJson(MockRates.Values));
|
||||||
store.SeedCard(Card("l_board", "b_1", from: 100, to: null, cur: "USD"));
|
store.SeedCard(Card("l_board", "b_1", from: 100, to: null, cur: "USD"));
|
||||||
|
|
||||||
@@ -93,7 +94,7 @@ public sealed class ConversionRecomputerTests
|
|||||||
[Fact]
|
[Fact]
|
||||||
public async Task Recompute_ConversionOff_ReturnsZeroAndKeepsCardsUntouched()
|
public async Task Recompute_ConversionOff_ReturnsZeroAndKeepsCardsUntouched()
|
||||||
{
|
{
|
||||||
(ConversionRecomputer service, FakeKanjStore store, FakeSettingsStore settings) = Create();
|
(ConversionRecomputer service, FakeKanjStore store, TestSettingsStore settings) = Create();
|
||||||
settings.Preload(SettingsKeys.ConversionOn, "false");
|
settings.Preload(SettingsKeys.ConversionOn, "false");
|
||||||
settings.Preload(SettingsKeys.RatesCache, CacheJson(MockRates.Values));
|
settings.Preload(SettingsKeys.RatesCache, CacheJson(MockRates.Values));
|
||||||
store.SeedCard(Card("l_board", "b_1", from: 100, to: 100, cur: "USD"));
|
store.SeedCard(Card("l_board", "b_1", from: 100, to: 100, cur: "USD"));
|
||||||
@@ -109,7 +110,7 @@ public sealed class ConversionRecomputerTests
|
|||||||
[Fact]
|
[Fact]
|
||||||
public async Task Recompute_ExcludesArchiveAndTrashCards()
|
public async Task Recompute_ExcludesArchiveAndTrashCards()
|
||||||
{
|
{
|
||||||
(ConversionRecomputer service, FakeKanjStore store, FakeSettingsStore settings) = Create();
|
(ConversionRecomputer service, FakeKanjStore store, TestSettingsStore settings) = Create();
|
||||||
settings.Preload(SettingsKeys.RatesCache, CacheJson(MockRates.Values));
|
settings.Preload(SettingsKeys.RatesCache, CacheJson(MockRates.Values));
|
||||||
store.SeedCard(Card("l_board", "b_1", from: 100, to: 100, cur: "USD"));
|
store.SeedCard(Card("l_board", "b_1", from: 100, to: 100, cur: "USD"));
|
||||||
store.SeedCard(Card("l_archive", KanbanColumns.Archive, from: 100, to: 100, cur: "USD"));
|
store.SeedCard(Card("l_archive", KanbanColumns.Archive, from: 100, to: 100, cur: "USD"));
|
||||||
@@ -128,7 +129,7 @@ public sealed class ConversionRecomputerTests
|
|||||||
[Fact]
|
[Fact]
|
||||||
public async Task Recompute_MissingCurrencyInRates_SkipsCardKeepingOldConversion()
|
public async Task Recompute_MissingCurrencyInRates_SkipsCardKeepingOldConversion()
|
||||||
{
|
{
|
||||||
(ConversionRecomputer service, FakeKanjStore store, FakeSettingsStore settings) = Create();
|
(ConversionRecomputer service, FakeKanjStore store, TestSettingsStore settings) = Create();
|
||||||
settings.Preload(SettingsKeys.RatesCache, CacheJson(new Dictionary<string, double> { ["RUB"] = 1.0, ["USD"] = 92.5 }));
|
settings.Preload(SettingsKeys.RatesCache, CacheJson(new Dictionary<string, double> { ["RUB"] = 1.0, ["USD"] = 92.5 }));
|
||||||
store.SeedCard(Card("l_board", "b_1", from: 100, to: 100, cur: "XXX") with
|
store.SeedCard(Card("l_board", "b_1", from: 100, to: 100, cur: "XXX") with
|
||||||
{
|
{
|
||||||
@@ -159,7 +160,7 @@ public sealed class ConversionRecomputerTests
|
|||||||
[Fact]
|
[Fact]
|
||||||
public async Task Recompute_CorruptedRatesCache_ReturnsZero()
|
public async Task Recompute_CorruptedRatesCache_ReturnsZero()
|
||||||
{
|
{
|
||||||
(ConversionRecomputer service, FakeKanjStore store, FakeSettingsStore settings) = Create();
|
(ConversionRecomputer service, FakeKanjStore store, TestSettingsStore settings) = Create();
|
||||||
settings.Preload(SettingsKeys.RatesCache, "{это не JSON");
|
settings.Preload(SettingsKeys.RatesCache, "{это не JSON");
|
||||||
store.SeedCard(Card("l_board", "b_1", from: 100, to: 100, cur: "USD"));
|
store.SeedCard(Card("l_board", "b_1", from: 100, to: 100, cur: "USD"));
|
||||||
|
|
||||||
@@ -174,7 +175,7 @@ public sealed class ConversionRecomputerTests
|
|||||||
[Fact]
|
[Fact]
|
||||||
public async Task OnRatesChangedAsync_PerformsFullRecompute()
|
public async Task OnRatesChangedAsync_PerformsFullRecompute()
|
||||||
{
|
{
|
||||||
(ConversionRecomputer service, FakeKanjStore store, FakeSettingsStore settings) = Create();
|
(ConversionRecomputer service, FakeKanjStore store, TestSettingsStore settings) = Create();
|
||||||
settings.Preload(SettingsKeys.RatesCache, CacheJson(MockRates.Values));
|
settings.Preload(SettingsKeys.RatesCache, CacheJson(MockRates.Values));
|
||||||
store.SeedCard(Card("l_board", "b_1", from: 100, to: 100, cur: "USD"));
|
store.SeedCard(Card("l_board", "b_1", from: 100, to: 100, cur: "USD"));
|
||||||
|
|
||||||
@@ -189,7 +190,7 @@ public sealed class ConversionRecomputerTests
|
|||||||
[Fact]
|
[Fact]
|
||||||
public async Task Recompute_SecondRun_RecomputesSameValues()
|
public async Task Recompute_SecondRun_RecomputesSameValues()
|
||||||
{
|
{
|
||||||
(ConversionRecomputer service, FakeKanjStore store, FakeSettingsStore settings) = Create();
|
(ConversionRecomputer service, FakeKanjStore store, TestSettingsStore settings) = Create();
|
||||||
settings.Preload(SettingsKeys.RatesCache, CacheJson(MockRates.Values));
|
settings.Preload(SettingsKeys.RatesCache, CacheJson(MockRates.Values));
|
||||||
store.SeedCard(Card("l_board", "b_1", from: 100, to: 100, cur: "USD"));
|
store.SeedCard(Card("l_board", "b_1", from: 100, to: 100, cur: "USD"));
|
||||||
|
|
||||||
@@ -203,11 +204,11 @@ public sealed class ConversionRecomputerTests
|
|||||||
|
|
||||||
// ─── Хелперы ────────────────────────────────────────────────────────────
|
// ─── Хелперы ────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
private static (ConversionRecomputer Service, FakeKanjStore Store, FakeSettingsStore Settings) Create()
|
private static (ConversionRecomputer Service, FakeKanjStore Store, TestSettingsStore Settings) Create()
|
||||||
{
|
{
|
||||||
var store = new FakeKanjStore();
|
var store = new FakeKanjStore();
|
||||||
var settings = new FakeSettingsStore();
|
var settings = new TestSettingsStore();
|
||||||
return (new ConversionRecomputer(settings, store), store, settings);
|
return (new ConversionRecomputer(settings.Store, store), store, settings);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Карточка с бюджетом в колонке (BudgetCur задан → Budget не null, как маппинг адаптера).
|
// Карточка с бюджетом в колонке (BudgetCur задан → Budget не null, как маппинг адаптера).
|
||||||
|
|||||||
@@ -52,7 +52,7 @@ public sealed class GrpcAiClassifierTests
|
|||||||
Reason = "реклама",
|
Reason = "реклама",
|
||||||
Usage = new Usage { Prompt = 500, Completion = 40, Total = 540 },
|
Usage = new Usage { Prompt = 500, Completion = 40, Total = 540 },
|
||||||
};
|
};
|
||||||
(FakeSettingsStore settings, ISecretCipher cipher, FakeKanjStore kanjStore) = Context(port);
|
(TestSettingsStore settings, ISecretCipher cipher, FakeKanjStore kanjStore) = Context(port);
|
||||||
settings.Preload(SettingsKeys.AiFilterPrompt, Json("Фильтруй. {domain} | {keywords}."));
|
settings.Preload(SettingsKeys.AiFilterPrompt, Json("Фильтруй. {domain} | {keywords}."));
|
||||||
settings.Preload(SettingsKeys.DomainDescription, Json(TestDomain));
|
settings.Preload(SettingsKeys.DomainDescription, Json(TestDomain));
|
||||||
settings.Preload(SettingsKeys.DomainKeywords, Json(TestKeywords));
|
settings.Preload(SettingsKeys.DomainKeywords, Json(TestKeywords));
|
||||||
@@ -88,7 +88,7 @@ public sealed class GrpcAiClassifierTests
|
|||||||
await AiGrpcTestHost.RunAsync(AiGrpcTestHost.DefaultToken, new RecordingAiService(), async (port, service) =>
|
await AiGrpcTestHost.RunAsync(AiGrpcTestHost.DefaultToken, new RecordingAiService(), async (port, service) =>
|
||||||
{
|
{
|
||||||
service.FilterUnavailable = true;
|
service.FilterUnavailable = true;
|
||||||
(FakeSettingsStore settings, ISecretCipher cipher, FakeKanjStore kanjStore) = Context(port);
|
(TestSettingsStore settings, ISecretCipher cipher, FakeKanjStore kanjStore) = Context(port);
|
||||||
IAiClassifier classifier = CreateClassifier(port, settings, cipher, kanjStore);
|
IAiClassifier classifier = CreateClassifier(port, settings, cipher, kanjStore);
|
||||||
|
|
||||||
await Assert.ThrowsAsync<AiUnavailableException>(
|
await Assert.ThrowsAsync<AiUnavailableException>(
|
||||||
@@ -102,7 +102,7 @@ public sealed class GrpcAiClassifierTests
|
|||||||
await AiGrpcTestHost.RunAsync(AiGrpcTestHost.DefaultToken, new RecordingAiService(), async (port, service) =>
|
await AiGrpcTestHost.RunAsync(AiGrpcTestHost.DefaultToken, new RecordingAiService(), async (port, service) =>
|
||||||
{
|
{
|
||||||
string text = new string('а', 5000);
|
string text = new string('а', 5000);
|
||||||
(FakeSettingsStore settings, ISecretCipher cipher, FakeKanjStore kanjStore) = Context(port);
|
(TestSettingsStore settings, ISecretCipher cipher, FakeKanjStore kanjStore) = Context(port);
|
||||||
IAiClassifier classifier = CreateClassifier(port, settings, cipher, kanjStore);
|
IAiClassifier classifier = CreateClassifier(port, settings, cipher, kanjStore);
|
||||||
|
|
||||||
await classifier.FilterAsync(text, CancellationToken.None);
|
await classifier.FilterAsync(text, CancellationToken.None);
|
||||||
@@ -131,7 +131,7 @@ public sealed class GrpcAiClassifierTests
|
|||||||
""",
|
""",
|
||||||
Usage = new Usage { Prompt = 3000, Completion = 700, Total = 3700 },
|
Usage = new Usage { Prompt = 3000, Completion = 700, Total = 3700 },
|
||||||
};
|
};
|
||||||
(FakeSettingsStore settings, ISecretCipher cipher, FakeKanjStore kanjStore) = Context(port);
|
(TestSettingsStore settings, ISecretCipher cipher, FakeKanjStore kanjStore) = Context(port);
|
||||||
settings.Preload(SettingsKeys.AiPrompt, Json("Разбор: {domain}."));
|
settings.Preload(SettingsKeys.AiPrompt, Json("Разбор: {domain}."));
|
||||||
settings.Preload(SettingsKeys.CardPrompt, Json("Верни блок «О заявке»."));
|
settings.Preload(SettingsKeys.CardPrompt, Json("Верни блок «О заявке»."));
|
||||||
settings.Preload(SettingsKeys.DomainDescription, Json(TestDomain));
|
settings.Preload(SettingsKeys.DomainDescription, Json(TestDomain));
|
||||||
@@ -191,7 +191,7 @@ public sealed class GrpcAiClassifierTests
|
|||||||
Ok = false,
|
Ok = false,
|
||||||
Usage = new Usage { Prompt = 900, Completion = 0, Total = 900 },
|
Usage = new Usage { Prompt = 900, Completion = 0, Total = 900 },
|
||||||
};
|
};
|
||||||
(FakeSettingsStore settings, ISecretCipher cipher, FakeKanjStore kanjStore) = Context(port);
|
(TestSettingsStore settings, ISecretCipher cipher, FakeKanjStore kanjStore) = Context(port);
|
||||||
|
|
||||||
TestTenantLimitStore limits = new();
|
TestTenantLimitStore limits = new();
|
||||||
IAiClassifier classifier = CreateClassifier(port, settings, cipher, kanjStore, limits);
|
IAiClassifier classifier = CreateClassifier(port, settings, cipher, kanjStore, limits);
|
||||||
@@ -212,7 +212,7 @@ public sealed class GrpcAiClassifierTests
|
|||||||
await AiGrpcTestHost.RunAsync(AiGrpcTestHost.DefaultToken, new RecordingAiService(), async (port, service) =>
|
await AiGrpcTestHost.RunAsync(AiGrpcTestHost.DefaultToken, new RecordingAiService(), async (port, service) =>
|
||||||
{
|
{
|
||||||
service.ClassifyUnavailable = true;
|
service.ClassifyUnavailable = true;
|
||||||
(FakeSettingsStore settings, ISecretCipher cipher, FakeKanjStore kanjStore) = Context(port);
|
(TestSettingsStore settings, ISecretCipher cipher, FakeKanjStore kanjStore) = Context(port);
|
||||||
IAiClassifier classifier = CreateClassifier(port, settings, cipher, kanjStore);
|
IAiClassifier classifier = CreateClassifier(port, settings, cipher, kanjStore);
|
||||||
|
|
||||||
await Assert.ThrowsAsync<AiUnavailableException>(
|
await Assert.ThrowsAsync<AiUnavailableException>(
|
||||||
@@ -226,7 +226,7 @@ public sealed class GrpcAiClassifierTests
|
|||||||
await AiGrpcTestHost.RunAsync(AiGrpcTestHost.DefaultToken, new RecordingAiService(), async (port, service) =>
|
await AiGrpcTestHost.RunAsync(AiGrpcTestHost.DefaultToken, new RecordingAiService(), async (port, service) =>
|
||||||
{
|
{
|
||||||
service.ClassifyReply = new ClassifyReply { Ok = true, Json = """{"title":"Заголовок","stack":[],"is_spam":false}""" };
|
service.ClassifyReply = new ClassifyReply { Ok = true, Json = """{"title":"Заголовок","stack":[],"is_spam":false}""" };
|
||||||
(FakeSettingsStore settings, ISecretCipher cipher, FakeKanjStore kanjStore) = Context(port);
|
(TestSettingsStore settings, ISecretCipher cipher, FakeKanjStore kanjStore) = Context(port);
|
||||||
settings.Preload(SettingsKeys.AiProvider, Json("anthropic"));
|
settings.Preload(SettingsKeys.AiProvider, Json("anthropic"));
|
||||||
settings.Preload(
|
settings.Preload(
|
||||||
SettingsKeys.AiConfigs,
|
SettingsKeys.AiConfigs,
|
||||||
@@ -263,7 +263,7 @@ public sealed class GrpcAiClassifierTests
|
|||||||
{
|
{
|
||||||
service.ClassifyReply = new ClassifyReply { Ok = true, Json = """{"title":"Т","stack":[],"is_spam":false}""" };
|
service.ClassifyReply = new ClassifyReply { Ok = true, Json = """{"title":"Т","stack":[],"is_spam":false}""" };
|
||||||
string text = new string('б', 6000);
|
string text = new string('б', 6000);
|
||||||
(FakeSettingsStore settings, ISecretCipher cipher, FakeKanjStore kanjStore) = Context(port);
|
(TestSettingsStore settings, ISecretCipher cipher, FakeKanjStore kanjStore) = Context(port);
|
||||||
IAiClassifier classifier = CreateClassifier(port, settings, cipher, kanjStore);
|
IAiClassifier classifier = CreateClassifier(port, settings, cipher, kanjStore);
|
||||||
|
|
||||||
await classifier.ClassifyAsync(text, CancellationToken.None);
|
await classifier.ClassifyAsync(text, CancellationToken.None);
|
||||||
@@ -284,7 +284,7 @@ public sealed class GrpcAiClassifierTests
|
|||||||
// Возвращает: Экземпляр GrpcAiClassifier.
|
// Возвращает: Экземпляр GrpcAiClassifier.
|
||||||
private static GrpcAiClassifier CreateClassifier(
|
private static GrpcAiClassifier CreateClassifier(
|
||||||
int port,
|
int port,
|
||||||
FakeSettingsStore settings,
|
TestSettingsStore settings,
|
||||||
ISecretCipher cipher,
|
ISecretCipher cipher,
|
||||||
FakeKanjStore kanjStore,
|
FakeKanjStore kanjStore,
|
||||||
TestTenantLimitStore? limits = null)
|
TestTenantLimitStore? limits = null)
|
||||||
@@ -296,17 +296,17 @@ public sealed class GrpcAiClassifierTests
|
|||||||
return new GrpcAiClassifier(
|
return new GrpcAiClassifier(
|
||||||
tenantContext,
|
tenantContext,
|
||||||
connection,
|
connection,
|
||||||
new AiProviderConfigBuilder(settings, cipher),
|
new AiProviderConfigBuilder(settings.Store, cipher),
|
||||||
new AiClassifyContextBuilder(settings, kanjStore),
|
new AiClassifyContextBuilder(settings.Store, kanjStore),
|
||||||
new TokenUsageRecorder(settings, limits.Store, tenantContext, new TokenUsageEventService(new TestTokenUsageEventStore().Store)),
|
new TokenUsageRecorder(settings.Store, limits.Store, tenantContext, new TokenUsageEventService(new TestTokenUsageEventStore().Store)),
|
||||||
NullLogger<GrpcAiClassifier>.Instance);
|
NullLogger<GrpcAiClassifier>.Instance);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Создаёт контекст сценария (пустые фейки; сценарий переопределяет настройки/доски).
|
// Создаёт контекст сценария (пустые фейки; сценарий переопределяет настройки/доски).
|
||||||
// port: Порт хоста (не используется контекстом — единый вид хелперов).
|
// port: Порт хоста (не используется контекстом — единый вид хелперов).
|
||||||
// Возвращает: Кортеж фейков (настройки, шифр, канбан).
|
// Возвращает: Кортеж фейков (настройки, шифр, канбан).
|
||||||
private static (FakeSettingsStore Settings, ISecretCipher Cipher, FakeKanjStore Kanj) Context(int port)
|
private static (TestSettingsStore Settings, ISecretCipher Cipher, FakeKanjStore Kanj) Context(int port)
|
||||||
=> (new FakeSettingsStore(), TestCiphers.New(), new FakeKanjStore());
|
=> (new TestSettingsStore(), TestCiphers.New(), new FakeKanjStore());
|
||||||
|
|
||||||
// Сериализует значение настройки в JSON-строку (как пишет SettingsStore).
|
// Сериализует значение настройки в JSON-строку (как пишет SettingsStore).
|
||||||
// value: Значение (строка/список).
|
// value: Значение (строка/список).
|
||||||
@@ -314,7 +314,7 @@ public sealed class GrpcAiClassifierTests
|
|||||||
private static string Json(object value) => JsonSerializer.Serialize(value);
|
private static string Json(object value) => JsonSerializer.Serialize(value);
|
||||||
|
|
||||||
private static async Task AssertUsageAsync(
|
private static async Task AssertUsageAsync(
|
||||||
FakeSettingsStore settings,
|
TestSettingsStore settings,
|
||||||
uint prompt,
|
uint prompt,
|
||||||
uint completion,
|
uint completion,
|
||||||
uint total)
|
uint total)
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
using Deal.Modules.Settings.Application.Models;
|
using Deal.Modules.Settings.Application.Models;
|
||||||
|
using Deal.Tests.Unit.Support;
|
||||||
using Deal.Tests.Unit.Modules.Settings;
|
using Deal.Tests.Unit.Modules.Settings;
|
||||||
|
|
||||||
namespace Deal.Tests.Unit.Support;
|
namespace Deal.Tests.Unit.Support;
|
||||||
@@ -12,7 +13,7 @@ public sealed class IncomingRulesTests
|
|||||||
// Создаёт правила поверх in-memory KV-хранилища.
|
// Создаёт правила поверх in-memory KV-хранилища.
|
||||||
// store: Хранилище настроек тенанта.
|
// store: Хранилище настроек тенанта.
|
||||||
// Возвращает: Экземпляр IncomingRules.
|
// Возвращает: Экземпляр IncomingRules.
|
||||||
private static IncomingRules CreateRules(FakeSettingsStore store) => new(store);
|
private static IncomingRules CreateRules(TestSettingsStore store) => new(store.Store);
|
||||||
|
|
||||||
// Сериализует значение в JSON-строку строки settings (как пишет SettingsStore).
|
// Сериализует значение в JSON-строку строки settings (как пишет SettingsStore).
|
||||||
// value: Значение (строка/число/булево/список).
|
// value: Значение (строка/число/булево/список).
|
||||||
@@ -23,7 +24,7 @@ public sealed class IncomingRulesTests
|
|||||||
[Fact]
|
[Fact]
|
||||||
public async Task CheckAsync_ShortTextBelowDefaultMinLen_FailsWithKindLength()
|
public async Task CheckAsync_ShortTextBelowDefaultMinLen_FailsWithKindLength()
|
||||||
{
|
{
|
||||||
IncomingRulesResult result = await CreateRules(new FakeSettingsStore())
|
IncomingRulesResult result = await CreateRules(new TestSettingsStore())
|
||||||
.CheckAsync("Короткий текст", CancellationToken.None);
|
.CheckAsync("Короткий текст", CancellationToken.None);
|
||||||
|
|
||||||
Assert.False(result.Pass);
|
Assert.False(result.Pass);
|
||||||
@@ -37,7 +38,7 @@ public sealed class IncomingRulesTests
|
|||||||
public async Task CheckAsync_EmptyText_IsLengthFail()
|
public async Task CheckAsync_EmptyText_IsLengthFail()
|
||||||
{
|
{
|
||||||
// (text or "").strip() — пустой/пробельный текст всегда «короче minLen».
|
// (text or "").strip() — пустой/пробельный текст всегда «короче minLen».
|
||||||
IncomingRulesResult result = await CreateRules(new FakeSettingsStore())
|
IncomingRulesResult result = await CreateRules(new TestSettingsStore())
|
||||||
.CheckAsync(" ", CancellationToken.None);
|
.CheckAsync(" ", CancellationToken.None);
|
||||||
|
|
||||||
Assert.False(result.Pass);
|
Assert.False(result.Pass);
|
||||||
@@ -47,7 +48,7 @@ public sealed class IncomingRulesTests
|
|||||||
[Fact]
|
[Fact]
|
||||||
public async Task CheckAsync_StoredMinLenShorter_AllowsTextBetweenDefaultsAndOverride()
|
public async Task CheckAsync_StoredMinLenShorter_AllowsTextBetweenDefaultsAndOverride()
|
||||||
{
|
{
|
||||||
var store = new FakeSettingsStore();
|
var store = new TestSettingsStore();
|
||||||
store.Preload(SettingsKeys.MinLen, Json(10));
|
store.Preload(SettingsKeys.MinLen, Json(10));
|
||||||
IncomingRules rules = CreateRules(store);
|
IncomingRules rules = CreateRules(store);
|
||||||
|
|
||||||
@@ -61,7 +62,7 @@ public sealed class IncomingRulesTests
|
|||||||
[Fact]
|
[Fact]
|
||||||
public async Task CheckAsync_LongCleanTextWithDefaults_Passes()
|
public async Task CheckAsync_LongCleanTextWithDefaults_Passes()
|
||||||
{
|
{
|
||||||
IncomingRulesResult result = await CreateRules(new FakeSettingsStore()).CheckAsync(
|
IncomingRulesResult result = await CreateRules(new TestSettingsStore()).CheckAsync(
|
||||||
"Заработок на крипте 300% в месяц! Подпишись на канал и получи бесплатный курс по трейдингу",
|
"Заработок на крипте 300% в месяц! Подпишись на канал и получи бесплатный курс по трейдингу",
|
||||||
CancellationToken.None);
|
CancellationToken.None);
|
||||||
|
|
||||||
@@ -75,7 +76,7 @@ public sealed class IncomingRulesTests
|
|||||||
[Fact]
|
[Fact]
|
||||||
public async Task CheckAsync_StopPhraseHit_ReturnsPhraseAsKwAndReason()
|
public async Task CheckAsync_StopPhraseHit_ReturnsPhraseAsKwAndReason()
|
||||||
{
|
{
|
||||||
var store = new FakeSettingsStore();
|
var store = new TestSettingsStore();
|
||||||
store.Preload(SettingsKeys.StopPhrases, Json(new[] { "взаимный пиар" }));
|
store.Preload(SettingsKeys.StopPhrases, Json(new[] { "взаимный пиар" }));
|
||||||
IncomingRules rules = CreateRules(store);
|
IncomingRules rules = CreateRules(store);
|
||||||
|
|
||||||
@@ -92,7 +93,7 @@ public sealed class IncomingRulesTests
|
|||||||
[Fact]
|
[Fact]
|
||||||
public async Task CheckAsync_StopPhrase_CaseInsensitiveMatch_KwKeepsStoredCase()
|
public async Task CheckAsync_StopPhrase_CaseInsensitiveMatch_KwKeepsStoredCase()
|
||||||
{
|
{
|
||||||
var store = new FakeSettingsStore();
|
var store = new TestSettingsStore();
|
||||||
store.Preload(SettingsKeys.StopPhrases, Json(new[] { "Взаимный Пиар" }));
|
store.Preload(SettingsKeys.StopPhrases, Json(new[] { "Взаимный Пиар" }));
|
||||||
IncomingRules rules = CreateRules(store);
|
IncomingRules rules = CreateRules(store);
|
||||||
|
|
||||||
@@ -108,7 +109,7 @@ public sealed class IncomingRulesTests
|
|||||||
[Fact]
|
[Fact]
|
||||||
public async Task CheckAsync_ResumeMarker_BlockResumesOn_BlocksWithMarker()
|
public async Task CheckAsync_ResumeMarker_BlockResumesOn_BlocksWithMarker()
|
||||||
{
|
{
|
||||||
var store = new FakeSettingsStore();
|
var store = new TestSettingsStore();
|
||||||
store.Preload(SettingsKeys.StopPhrases, Json(Array.Empty<string>()));
|
store.Preload(SettingsKeys.StopPhrases, Json(Array.Empty<string>()));
|
||||||
IncomingRules rules = CreateRules(store);
|
IncomingRules rules = CreateRules(store);
|
||||||
|
|
||||||
@@ -125,7 +126,7 @@ public sealed class IncomingRulesTests
|
|||||||
[Fact]
|
[Fact]
|
||||||
public async Task CheckAsync_ResumeText_BlockResumesOff_Passes()
|
public async Task CheckAsync_ResumeText_BlockResumesOff_Passes()
|
||||||
{
|
{
|
||||||
var store = new FakeSettingsStore();
|
var store = new TestSettingsStore();
|
||||||
store.Preload(SettingsKeys.StopPhrases, Json(Array.Empty<string>()));
|
store.Preload(SettingsKeys.StopPhrases, Json(Array.Empty<string>()));
|
||||||
store.Preload(SettingsKeys.BlockResumes, Json(false));
|
store.Preload(SettingsKeys.BlockResumes, Json(false));
|
||||||
IncomingRules rules = CreateRules(store);
|
IncomingRules rules = CreateRules(store);
|
||||||
@@ -141,7 +142,7 @@ public sealed class IncomingRulesTests
|
|||||||
[Fact]
|
[Fact]
|
||||||
public async Task CheckAsync_ResumeWordWithoutHireMarkerBefore_IsCut()
|
public async Task CheckAsync_ResumeWordWithoutHireMarkerBefore_IsCut()
|
||||||
{
|
{
|
||||||
var store = new FakeSettingsStore();
|
var store = new TestSettingsStore();
|
||||||
store.Preload(SettingsKeys.StopPhrases, Json(Array.Empty<string>()));
|
store.Preload(SettingsKeys.StopPhrases, Json(Array.Empty<string>()));
|
||||||
IncomingRules rules = CreateRules(store);
|
IncomingRules rules = CreateRules(store);
|
||||||
|
|
||||||
@@ -158,7 +159,7 @@ public sealed class IncomingRulesTests
|
|||||||
[Fact]
|
[Fact]
|
||||||
public async Task CheckAsync_VacancyWithResumeWordAfterHireMarker_GuardAllowsPass()
|
public async Task CheckAsync_VacancyWithResumeWordAfterHireMarker_GuardAllowsPass()
|
||||||
{
|
{
|
||||||
var store = new FakeSettingsStore();
|
var store = new TestSettingsStore();
|
||||||
store.Preload(SettingsKeys.StopPhrases, Json(Array.Empty<string>()));
|
store.Preload(SettingsKeys.StopPhrases, Json(Array.Empty<string>()));
|
||||||
IncomingRules rules = CreateRules(store);
|
IncomingRules rules = CreateRules(store);
|
||||||
|
|
||||||
@@ -175,7 +176,7 @@ public sealed class IncomingRulesTests
|
|||||||
[Fact]
|
[Fact]
|
||||||
public async Task CheckAsync_WantedTypeFreelance_VacancyLikeText_BlockedAsType()
|
public async Task CheckAsync_WantedTypeFreelance_VacancyLikeText_BlockedAsType()
|
||||||
{
|
{
|
||||||
var store = new FakeSettingsStore();
|
var store = new TestSettingsStore();
|
||||||
store.Preload(SettingsKeys.StopPhrases, Json(Array.Empty<string>()));
|
store.Preload(SettingsKeys.StopPhrases, Json(Array.Empty<string>()));
|
||||||
store.Preload(SettingsKeys.WantedType, Json("freelance"));
|
store.Preload(SettingsKeys.WantedType, Json("freelance"));
|
||||||
IncomingRules rules = CreateRules(store);
|
IncomingRules rules = CreateRules(store);
|
||||||
@@ -194,7 +195,7 @@ public sealed class IncomingRulesTests
|
|||||||
[Fact]
|
[Fact]
|
||||||
public async Task CheckAsync_WantedTypeVacancy_OrderLikeText_BlockedAsType()
|
public async Task CheckAsync_WantedTypeVacancy_OrderLikeText_BlockedAsType()
|
||||||
{
|
{
|
||||||
var store = new FakeSettingsStore();
|
var store = new TestSettingsStore();
|
||||||
store.Preload(SettingsKeys.StopPhrases, Json(Array.Empty<string>()));
|
store.Preload(SettingsKeys.StopPhrases, Json(Array.Empty<string>()));
|
||||||
store.Preload(SettingsKeys.WantedType, Json("vacancy"));
|
store.Preload(SettingsKeys.WantedType, Json("vacancy"));
|
||||||
IncomingRules rules = CreateRules(store);
|
IncomingRules rules = CreateRules(store);
|
||||||
@@ -211,7 +212,7 @@ public sealed class IncomingRulesTests
|
|||||||
[Fact]
|
[Fact]
|
||||||
public async Task CheckAsync_WantedTypeFreelance_OrderText_Passes()
|
public async Task CheckAsync_WantedTypeFreelance_OrderText_Passes()
|
||||||
{
|
{
|
||||||
var store = new FakeSettingsStore();
|
var store = new TestSettingsStore();
|
||||||
store.Preload(SettingsKeys.StopPhrases, Json(Array.Empty<string>()));
|
store.Preload(SettingsKeys.StopPhrases, Json(Array.Empty<string>()));
|
||||||
store.Preload(SettingsKeys.WantedType, Json("freelance"));
|
store.Preload(SettingsKeys.WantedType, Json("freelance"));
|
||||||
IncomingRules rules = CreateRules(store);
|
IncomingRules rules = CreateRules(store);
|
||||||
@@ -229,7 +230,7 @@ public sealed class IncomingRulesTests
|
|||||||
public async Task CheckAsync_WantedTypeBoth_VacancyText_Passes()
|
public async Task CheckAsync_WantedTypeBoth_VacancyText_Passes()
|
||||||
{
|
{
|
||||||
// Дефолт wantedType=both: и вакансии, и заказы — вакансионный текст не отсекается.
|
// Дефолт wantedType=both: и вакансии, и заказы — вакансионный текст не отсекается.
|
||||||
var store = new FakeSettingsStore();
|
var store = new TestSettingsStore();
|
||||||
store.Preload(SettingsKeys.StopPhrases, Json(Array.Empty<string>()));
|
store.Preload(SettingsKeys.StopPhrases, Json(Array.Empty<string>()));
|
||||||
IncomingRules rules = CreateRules(store);
|
IncomingRules rules = CreateRules(store);
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ 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;
|
||||||
using Deal.Modules.Settings.Application.Models;
|
using Deal.Modules.Settings.Application.Models;
|
||||||
|
using Deal.Tests.Unit.Support;
|
||||||
using Deal.Tests.Unit.Modules.Cards;
|
using Deal.Tests.Unit.Modules.Cards;
|
||||||
using Deal.Tests.Unit.Modules.Settings;
|
using Deal.Tests.Unit.Modules.Settings;
|
||||||
|
|
||||||
@@ -17,11 +18,11 @@ public sealed class LocalColumnSuggesterTests
|
|||||||
{
|
{
|
||||||
// Создаёт контекст теста: in-memory хранилища + адаптер поверх сервиса досок модуля.
|
// Создаёт контекст теста: in-memory хранилища + адаптер поверх сервиса досок модуля.
|
||||||
// Возвращает: Кортеж (хранилище канбана, KV-настройки, адаптер).
|
// Возвращает: Кортеж (хранилище канбана, KV-настройки, адаптер).
|
||||||
private static (FakeKanjStore Store, FakeSettingsStore Settings, LocalColumnSuggester Suggester) CreateContext()
|
private static (FakeKanjStore Store, TestSettingsStore Settings, LocalColumnSuggester Suggester) CreateContext()
|
||||||
{
|
{
|
||||||
var store = new FakeKanjStore();
|
var store = new FakeKanjStore();
|
||||||
var settings = new FakeSettingsStore();
|
var settings = new TestSettingsStore();
|
||||||
var suggester = new LocalColumnSuggester(store, settings, new ContainersService(store, settings));
|
var suggester = new LocalColumnSuggester(store, settings.Store, new ContainersService(store, settings.Store));
|
||||||
return (store, settings, suggester);
|
return (store, settings, suggester);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -72,7 +73,7 @@ public sealed class LocalColumnSuggesterTests
|
|||||||
[Fact]
|
[Fact]
|
||||||
public async Task SuggestColumns_CooldownActive_ReturnsCooldownReason()
|
public async Task SuggestColumns_CooldownActive_ReturnsCooldownReason()
|
||||||
{
|
{
|
||||||
(FakeKanjStore store, FakeSettingsStore settings, LocalColumnSuggester suggester) = CreateContext();
|
(FakeKanjStore store, TestSettingsStore settings, LocalColumnSuggester suggester) = CreateContext();
|
||||||
settings.Preload(SettingsKeys.LastSuggestAt,
|
settings.Preload(SettingsKeys.LastSuggestAt,
|
||||||
DateTimeOffset.UtcNow.ToUnixTimeSeconds().ToString(CultureInfo.InvariantCulture));
|
DateTimeOffset.UtcNow.ToUnixTimeSeconds().ToString(CultureInfo.InvariantCulture));
|
||||||
SeedInbox("l_1", "нужен python", store);
|
SeedInbox("l_1", "нужен python", store);
|
||||||
@@ -128,7 +129,7 @@ public sealed class LocalColumnSuggesterTests
|
|||||||
[Fact]
|
[Fact]
|
||||||
public async Task SuggestColumns_Success_CreatesSuggestedBoardsAndPlacesCards()
|
public async Task SuggestColumns_Success_CreatesSuggestedBoardsAndPlacesCards()
|
||||||
{
|
{
|
||||||
(FakeKanjStore store, FakeSettingsStore settings, LocalColumnSuggester suggester) = CreateContext();
|
(FakeKanjStore store, TestSettingsStore settings, LocalColumnSuggester suggester) = CreateContext();
|
||||||
SeedInbox("l_p1", "нужен python", store);
|
SeedInbox("l_p1", "нужен python", store);
|
||||||
SeedInbox("l_p2", "нужен python", store);
|
SeedInbox("l_p2", "нужен python", store);
|
||||||
SeedInbox("l_p3", "нужен python", store);
|
SeedInbox("l_p3", "нужен python", store);
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ using Deal.Contracts.Integrations.Models;
|
|||||||
using Deal.Infrastructure.Integrations.Services;
|
using Deal.Infrastructure.Integrations.Services;
|
||||||
using Deal.Modules.Kanban.Application.Models;
|
using Deal.Modules.Kanban.Application.Models;
|
||||||
using Deal.Modules.Settings.Application.Models;
|
using Deal.Modules.Settings.Application.Models;
|
||||||
|
using Deal.Tests.Unit.Support;
|
||||||
using Deal.Tests.Unit.Modules.Kanban;
|
using Deal.Tests.Unit.Modules.Kanban;
|
||||||
using Deal.Tests.Unit.Modules.Settings;
|
using Deal.Tests.Unit.Modules.Settings;
|
||||||
|
|
||||||
@@ -27,14 +28,14 @@ public sealed class LocalMlClientTests
|
|||||||
// store: Хранилище настроек тенанта.
|
// store: Хранилище настроек тенанта.
|
||||||
// learning: Хранилище обучения (по умолчанию пустое: счётчики 0).
|
// learning: Хранилище обучения (по умолчанию пустое: счётчики 0).
|
||||||
// Возвращает: Экземпляр LocalMlClient.
|
// Возвращает: Экземпляр LocalMlClient.
|
||||||
private static LocalMlClient CreateClient(FakeSettingsStore store, FakeMlLearningStore? learning = null)
|
private static LocalMlClient CreateClient(TestSettingsStore store, FakeMlLearningStore? learning = null)
|
||||||
=> new(store, learning ?? new FakeMlLearningStore());
|
=> new(store.Store, learning ?? new FakeMlLearningStore());
|
||||||
|
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task StatusAsync_NoSettings_ReturnsDeterministicNotReadyStatus()
|
public async Task StatusAsync_NoSettings_ReturnsDeterministicNotReadyStatus()
|
||||||
{
|
{
|
||||||
LocalMlClient client = CreateClient(new FakeSettingsStore());
|
LocalMlClient client = CreateClient(new TestSettingsStore());
|
||||||
|
|
||||||
MlStatusResponseDto status = await client.StatusAsync(CancellationToken.None);
|
MlStatusResponseDto status = await client.StatusAsync(CancellationToken.None);
|
||||||
|
|
||||||
@@ -63,7 +64,7 @@ public sealed class LocalMlClientTests
|
|||||||
[Fact]
|
[Fact]
|
||||||
public async Task StatusAsync_StoredCounters_AreReflectedInStats()
|
public async Task StatusAsync_StoredCounters_AreReflectedInStats()
|
||||||
{
|
{
|
||||||
var store = new FakeSettingsStore();
|
var store = new TestSettingsStore();
|
||||||
store.Preload(SettingsKeys.MlDecisions, "7");
|
store.Preload(SettingsKeys.MlDecisions, "7");
|
||||||
store.Preload(SettingsKeys.AiDecisions, "3");
|
store.Preload(SettingsKeys.AiDecisions, "3");
|
||||||
LocalMlClient client = CreateClient(store);
|
LocalMlClient client = CreateClient(store);
|
||||||
@@ -78,7 +79,7 @@ public sealed class LocalMlClientTests
|
|||||||
[Fact]
|
[Fact]
|
||||||
public async Task StatusAsync_StoredMlEnabledFalse_Disables()
|
public async Task StatusAsync_StoredMlEnabledFalse_Disables()
|
||||||
{
|
{
|
||||||
var store = new FakeSettingsStore();
|
var store = new TestSettingsStore();
|
||||||
store.Preload(SettingsKeys.MlEnabled, "false");
|
store.Preload(SettingsKeys.MlEnabled, "false");
|
||||||
|
|
||||||
MlStatusResponseDto status = await CreateClient(store).StatusAsync(CancellationToken.None);
|
MlStatusResponseDto status = await CreateClient(store).StatusAsync(CancellationToken.None);
|
||||||
@@ -89,7 +90,7 @@ public sealed class LocalMlClientTests
|
|||||||
[Fact]
|
[Fact]
|
||||||
public async Task StatusAsync_StoredMlEnabledTrue_Enables()
|
public async Task StatusAsync_StoredMlEnabledTrue_Enables()
|
||||||
{
|
{
|
||||||
var store = new FakeSettingsStore();
|
var store = new TestSettingsStore();
|
||||||
store.Preload(SettingsKeys.MlEnabled, "true");
|
store.Preload(SettingsKeys.MlEnabled, "true");
|
||||||
|
|
||||||
MlStatusResponseDto status = await CreateClient(store).StatusAsync(CancellationToken.None);
|
MlStatusResponseDto status = await CreateClient(store).StatusAsync(CancellationToken.None);
|
||||||
@@ -102,7 +103,7 @@ public sealed class LocalMlClientTests
|
|||||||
[InlineData("не json {{")]
|
[InlineData("не json {{")]
|
||||||
public async Task StatusAsync_NonBoolOrDamagedMlEnabled_DegradesToDefault(string valueJson)
|
public async Task StatusAsync_NonBoolOrDamagedMlEnabled_DegradesToDefault(string valueJson)
|
||||||
{
|
{
|
||||||
var store = new FakeSettingsStore();
|
var store = new TestSettingsStore();
|
||||||
store.Preload(SettingsKeys.MlEnabled, valueJson);
|
store.Preload(SettingsKeys.MlEnabled, valueJson);
|
||||||
|
|
||||||
MlStatusResponseDto status = await CreateClient(store).StatusAsync(CancellationToken.None);
|
MlStatusResponseDto status = await CreateClient(store).StatusAsync(CancellationToken.None);
|
||||||
@@ -113,7 +114,7 @@ public sealed class LocalMlClientTests
|
|||||||
[Fact]
|
[Fact]
|
||||||
public async Task StatusAsync_DamagedCounter_DegradesToZero()
|
public async Task StatusAsync_DamagedCounter_DegradesToZero()
|
||||||
{
|
{
|
||||||
var store = new FakeSettingsStore();
|
var store = new TestSettingsStore();
|
||||||
store.Preload(SettingsKeys.MlDecisions, "не число");
|
store.Preload(SettingsKeys.MlDecisions, "не число");
|
||||||
|
|
||||||
MlStatusResponseDto status = await CreateClient(store).StatusAsync(CancellationToken.None);
|
MlStatusResponseDto status = await CreateClient(store).StatusAsync(CancellationToken.None);
|
||||||
@@ -124,7 +125,7 @@ public sealed class LocalMlClientTests
|
|||||||
[Fact]
|
[Fact]
|
||||||
public async Task StatusAsync_ReportsLearningAndOutboxFromTables()
|
public async Task StatusAsync_ReportsLearningAndOutboxFromTables()
|
||||||
{
|
{
|
||||||
var store = new FakeSettingsStore();
|
var store = new TestSettingsStore();
|
||||||
var learning = new FakeMlLearningStore { LearningCount = 3 }; // журнал CardMoves: 3 записи
|
var learning = new FakeMlLearningStore { LearningCount = 3 }; // журнал CardMoves: 3 записи
|
||||||
LocalMlClient client = CreateClient(store, learning);
|
LocalMlClient client = CreateClient(store, learning);
|
||||||
await client.PushAsync("Разработчик Python, готов к проекту", "b_alpha", 1.0, CancellationToken.None);
|
await client.PushAsync("Разработчик Python, готов к проекту", "b_alpha", 1.0, CancellationToken.None);
|
||||||
@@ -142,7 +143,7 @@ public sealed class LocalMlClientTests
|
|||||||
[Fact]
|
[Fact]
|
||||||
public async Task PredictAsync_AnyText_ReturnsFixedNotReadyShape()
|
public async Task PredictAsync_AnyText_ReturnsFixedNotReadyShape()
|
||||||
{
|
{
|
||||||
LocalMlClient client = CreateClient(new FakeSettingsStore());
|
LocalMlClient client = CreateClient(new TestSettingsStore());
|
||||||
|
|
||||||
MlPredictResultDto result = await client.PredictAsync(
|
MlPredictResultDto result = await client.PredictAsync(
|
||||||
"Python backend на fastapi, бот в телеграм", CancellationToken.None);
|
"Python backend на fastapi, бот в телеграм", CancellationToken.None);
|
||||||
@@ -162,7 +163,7 @@ public sealed class LocalMlClientTests
|
|||||||
public async Task PushAsync_WritesOutboxRow_WithMleIdAndTrimmedFields()
|
public async Task PushAsync_WritesOutboxRow_WithMleIdAndTrimmedFields()
|
||||||
{
|
{
|
||||||
var learning = new FakeMlLearningStore();
|
var learning = new FakeMlLearningStore();
|
||||||
LocalMlClient client = CreateClient(new FakeSettingsStore(), learning);
|
LocalMlClient client = CreateClient(new TestSettingsStore(), learning);
|
||||||
|
|
||||||
await client.PushAsync(" Python-разработчик, готов к работе ", " b_alpha ", 1.0, CancellationToken.None);
|
await client.PushAsync(" Python-разработчик, готов к работе ", " b_alpha ", 1.0, CancellationToken.None);
|
||||||
|
|
||||||
@@ -182,7 +183,7 @@ public sealed class LocalMlClientTests
|
|||||||
public async Task PushAsync_EmptyTextOrLabelAfterTrim_IsNoOp(string text, string label)
|
public async Task PushAsync_EmptyTextOrLabelAfterTrim_IsNoOp(string text, string label)
|
||||||
{
|
{
|
||||||
var learning = new FakeMlLearningStore();
|
var learning = new FakeMlLearningStore();
|
||||||
LocalMlClient client = CreateClient(new FakeSettingsStore(), learning);
|
LocalMlClient client = CreateClient(new TestSettingsStore(), learning);
|
||||||
|
|
||||||
await client.PushAsync(text, label, 1.0, CancellationToken.None);
|
await client.PushAsync(text, label, 1.0, CancellationToken.None);
|
||||||
|
|
||||||
@@ -193,7 +194,7 @@ public sealed class LocalMlClientTests
|
|||||||
public async Task PushAsync_TextOver6000_IsTruncatedToMaxLength()
|
public async Task PushAsync_TextOver6000_IsTruncatedToMaxLength()
|
||||||
{
|
{
|
||||||
var learning = new FakeMlLearningStore();
|
var learning = new FakeMlLearningStore();
|
||||||
LocalMlClient client = CreateClient(new FakeSettingsStore(), learning);
|
LocalMlClient client = CreateClient(new TestSettingsStore(), learning);
|
||||||
string longText = new string('а', MaxLearningTextLength + 1000);
|
string longText = new string('а', MaxLearningTextLength + 1000);
|
||||||
|
|
||||||
await client.PushAsync(longText, "spam", 1.0, CancellationToken.None);
|
await client.PushAsync(longText, "spam", 1.0, CancellationToken.None);
|
||||||
@@ -207,7 +208,7 @@ public sealed class LocalMlClientTests
|
|||||||
public async Task PushAsync_TruncationDoesNotSplitSurrogatePair()
|
public async Task PushAsync_TruncationDoesNotSplitSurrogatePair()
|
||||||
{
|
{
|
||||||
var learning = new FakeMlLearningStore();
|
var learning = new FakeMlLearningStore();
|
||||||
LocalMlClient client = CreateClient(new FakeSettingsStore(), learning);
|
LocalMlClient client = CreateClient(new TestSettingsStore(), learning);
|
||||||
// Длина 6001 в UTF-16: 5999 'x' + «🙂» (суррогатная пара) — срез .NET на 6000 попадает в пару.
|
// Длина 6001 в UTF-16: 5999 'x' + «🙂» (суррогатная пара) — срез .NET на 6000 попадает в пару.
|
||||||
string text = new string('x', MaxLearningTextLength - 1) + "🙂";
|
string text = new string('x', MaxLearningTextLength - 1) + "🙂";
|
||||||
|
|
||||||
@@ -223,7 +224,7 @@ public sealed class LocalMlClientTests
|
|||||||
public async Task PushAsync_NegativeDelta_IsStoredAsIs()
|
public async Task PushAsync_NegativeDelta_IsStoredAsIs()
|
||||||
{
|
{
|
||||||
var learning = new FakeMlLearningStore();
|
var learning = new FakeMlLearningStore();
|
||||||
LocalMlClient client = CreateClient(new FakeSettingsStore(), learning);
|
LocalMlClient client = CreateClient(new TestSettingsStore(), learning);
|
||||||
|
|
||||||
await client.PushAsync("Это не спам — вернул из корзины", "spam", -1.0, CancellationToken.None);
|
await client.PushAsync("Это не спам — вернул из корзины", "spam", -1.0, CancellationToken.None);
|
||||||
|
|
||||||
@@ -236,7 +237,7 @@ public sealed class LocalMlClientTests
|
|||||||
[Fact]
|
[Fact]
|
||||||
public async Task ResetAsync_ReturnsOk()
|
public async Task ResetAsync_ReturnsOk()
|
||||||
{
|
{
|
||||||
MlResetResultDto result = await CreateClient(new FakeSettingsStore()).ResetAsync(CancellationToken.None);
|
MlResetResultDto result = await CreateClient(new TestSettingsStore()).ResetAsync(CancellationToken.None);
|
||||||
|
|
||||||
Assert.True(result.Ok);
|
Assert.True(result.Ok);
|
||||||
Assert.Null(result.Error);
|
Assert.Null(result.Error);
|
||||||
@@ -245,7 +246,7 @@ public sealed class LocalMlClientTests
|
|||||||
[Fact]
|
[Fact]
|
||||||
public async Task ResetAsync_DoesNotTouchSettingsRows()
|
public async Task ResetAsync_DoesNotTouchSettingsRows()
|
||||||
{
|
{
|
||||||
var store = new FakeSettingsStore();
|
var store = new TestSettingsStore();
|
||||||
store.Preload(SettingsKeys.MlDecisions, "7");
|
store.Preload(SettingsKeys.MlDecisions, "7");
|
||||||
LocalMlClient client = CreateClient(store);
|
LocalMlClient client = CreateClient(store);
|
||||||
|
|
||||||
@@ -259,7 +260,7 @@ public sealed class LocalMlClientTests
|
|||||||
[Fact]
|
[Fact]
|
||||||
public async Task ResetAsync_ClearsOutbox_KeepsLearningJournalAndKvCounters()
|
public async Task ResetAsync_ClearsOutbox_KeepsLearningJournalAndKvCounters()
|
||||||
{
|
{
|
||||||
var store = new FakeSettingsStore();
|
var store = new TestSettingsStore();
|
||||||
store.Preload(SettingsKeys.MlDecisions, "7");
|
store.Preload(SettingsKeys.MlDecisions, "7");
|
||||||
var learning = new FakeMlLearningStore { LearningCount = 4 }; // журнал CardMoves: 4 записи
|
var learning = new FakeMlLearningStore { LearningCount = 4 }; // журнал CardMoves: 4 записи
|
||||||
LocalMlClient client = CreateClient(store, learning);
|
LocalMlClient client = CreateClient(store, learning);
|
||||||
@@ -278,7 +279,7 @@ public sealed class LocalMlClientTests
|
|||||||
[Fact]
|
[Fact]
|
||||||
public async Task MlStatusResponseDto_SerializesToPrototypeWireFormat()
|
public async Task MlStatusResponseDto_SerializesToPrototypeWireFormat()
|
||||||
{
|
{
|
||||||
var store = new FakeSettingsStore();
|
var store = new TestSettingsStore();
|
||||||
store.Preload(SettingsKeys.MlEnabled, "false");
|
store.Preload(SettingsKeys.MlEnabled, "false");
|
||||||
store.Preload(SettingsKeys.MlDecisions, "7");
|
store.Preload(SettingsKeys.MlDecisions, "7");
|
||||||
store.Preload(SettingsKeys.AiDecisions, "3");
|
store.Preload(SettingsKeys.AiDecisions, "3");
|
||||||
@@ -316,7 +317,7 @@ public sealed class LocalMlClientTests
|
|||||||
[Fact]
|
[Fact]
|
||||||
public async Task MlPredictResultDto_SerializesToPrototypeWireFormat()
|
public async Task MlPredictResultDto_SerializesToPrototypeWireFormat()
|
||||||
{
|
{
|
||||||
MlPredictResultDto dto = await CreateClient(new FakeSettingsStore()).PredictAsync("текст", CancellationToken.None);
|
MlPredictResultDto dto = await CreateClient(new TestSettingsStore()).PredictAsync("текст", CancellationToken.None);
|
||||||
|
|
||||||
string json = JsonSerializer.Serialize(dto, CamelCaseOptions);
|
string json = JsonSerializer.Serialize(dto, CamelCaseOptions);
|
||||||
|
|
||||||
@@ -329,7 +330,7 @@ public sealed class LocalMlClientTests
|
|||||||
[Fact]
|
[Fact]
|
||||||
public async Task MlResetResultDto_Success_SerializesWithoutErrorKey()
|
public async Task MlResetResultDto_Success_SerializesWithoutErrorKey()
|
||||||
{
|
{
|
||||||
MlResetResultDto dto = await CreateClient(new FakeSettingsStore()).ResetAsync(CancellationToken.None);
|
MlResetResultDto dto = await CreateClient(new TestSettingsStore()).ResetAsync(CancellationToken.None);
|
||||||
|
|
||||||
string json = JsonSerializer.Serialize(dto, CamelCaseOptions);
|
string json = JsonSerializer.Serialize(dto, CamelCaseOptions);
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ 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;
|
||||||
using Deal.Modules.Settings.Application.Models;
|
using Deal.Modules.Settings.Application.Models;
|
||||||
|
using Deal.Tests.Unit.Support;
|
||||||
using Deal.Tests.Unit.Modules.Settings;
|
using Deal.Tests.Unit.Modules.Settings;
|
||||||
|
|
||||||
namespace Deal.Tests.Unit.Support;
|
namespace Deal.Tests.Unit.Support;
|
||||||
@@ -428,10 +429,10 @@ public sealed class MessageParseCoreTests
|
|||||||
public async Task ParseAsync_ReadsMarkerOverridesFromSettings()
|
public async Task ParseAsync_ReadsMarkerOverridesFromSettings()
|
||||||
{
|
{
|
||||||
// Кастомные hireMarkers из хранилища перекрывают дефолты: «вакансия» больше не маркер, свой — маркер.
|
// Кастомные hireMarkers из хранилища перекрывают дефолты: «вакансия» больше не маркер, свой — маркер.
|
||||||
var store = new FakeSettingsStore();
|
var store = new TestSettingsStore();
|
||||||
store.Preload(SettingsKeys.HireMarkers, JsonSerializer.Serialize(new[] { "нанимаем джунов" }));
|
store.Preload(SettingsKeys.HireMarkers, JsonSerializer.Serialize(new[] { "нанимаем джунов" }));
|
||||||
|
|
||||||
LocalFieldsParser parser = new(store);
|
LocalFieldsParser parser = new(store.Store);
|
||||||
LocalParsedFields custom = await parser.ParseAsync("Срочно нанимаем джунов на проект", default);
|
LocalParsedFields custom = await parser.ParseAsync("Срочно нанимаем джунов на проект", default);
|
||||||
LocalParsedFields withoutMarker = await parser.ParseAsync("Вакансия Java-разработчика на проект", default);
|
LocalParsedFields withoutMarker = await parser.ParseAsync("Вакансия Java-разработчика на проект", default);
|
||||||
|
|
||||||
@@ -442,7 +443,7 @@ public sealed class MessageParseCoreTests
|
|||||||
[Fact]
|
[Fact]
|
||||||
public async Task ParseAsync_NoOverrides_UsesDefaults()
|
public async Task ParseAsync_NoOverrides_UsesDefaults()
|
||||||
{
|
{
|
||||||
LocalFieldsParser parser = new(new FakeSettingsStore());
|
LocalFieldsParser parser = new(new TestSettingsStore().Store);
|
||||||
|
|
||||||
LocalParsedFields result = await parser.ParseAsync("Вакансия в команду на проект", default);
|
LocalParsedFields result = await parser.ParseAsync("Вакансия в команду на проект", default);
|
||||||
|
|
||||||
|
|||||||
@@ -155,8 +155,8 @@ public sealed class PipelineWorkerSchedulerTests
|
|||||||
var pipelineB = new FakePipelineStore();
|
var pipelineB = new FakePipelineStore();
|
||||||
var kanjA = new FakeKanjStore();
|
var kanjA = new FakeKanjStore();
|
||||||
var kanjB = new FakeKanjStore();
|
var kanjB = new FakeKanjStore();
|
||||||
var settingsA = new FakeSettingsStore();
|
var settingsA = new TestSettingsStore();
|
||||||
var settingsB = new FakeSettingsStore();
|
var settingsB = new TestSettingsStore();
|
||||||
var pumpGate = new PipelinePumpGate();
|
var pumpGate = new PipelinePumpGate();
|
||||||
var aiClassifier = new TestAiClassifier
|
var aiClassifier = new TestAiClassifier
|
||||||
{
|
{
|
||||||
@@ -173,7 +173,7 @@ public sealed class PipelineWorkerSchedulerTests
|
|||||||
// Тенант-scoped адаптеры: фейк выбирает хранилище по ITenantContext, который цикл заполняет SetTenant
|
// Тенант-scoped адаптеры: фейк выбирает хранилище по ITenantContext, который цикл заполняет SetTenant
|
||||||
// (эталон StorageTickSchedulerTests/ConnectionStringProvider.ForTenant).
|
// (эталон StorageTickSchedulerTests/ConnectionStringProvider.ForTenant).
|
||||||
services.AddScoped<ICardStore>(provider => TenantOf(provider) == TenantA ? kanjA : kanjB);
|
services.AddScoped<ICardStore>(provider => TenantOf(provider) == TenantA ? kanjA : kanjB);
|
||||||
services.AddScoped<ISettingsStore>(provider => TenantOf(provider) == TenantA ? settingsA : settingsB);
|
services.AddScoped<ISettingsStore>(provider => TenantOf(provider) == TenantA ? settingsA.Store : settingsB.Store);
|
||||||
services.AddScoped<IPipelineStore>(provider => TenantOf(provider) == TenantA ? pipelineA : pipelineB);
|
services.AddScoped<IPipelineStore>(provider => TenantOf(provider) == TenantA ? pipelineA : pipelineB);
|
||||||
// Реальные сервисы модуля Pipeline — как AddPipelineModule в Program.cs: цикл резолвит их в tenant-scope.
|
// Реальные сервисы модуля Pipeline — как AddPipelineModule в Program.cs: цикл резолвит их в tenant-scope.
|
||||||
services.AddScoped<IncomingRules>();
|
services.AddScoped<IncomingRules>();
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ public sealed class PipelineWorkerServiceTests
|
|||||||
PipelineWorkerService Worker,
|
PipelineWorkerService Worker,
|
||||||
TestPipelineStore PipelineStore,
|
TestPipelineStore PipelineStore,
|
||||||
FakeKanjStore KanjStore,
|
FakeKanjStore KanjStore,
|
||||||
FakeSettingsStore Settings,
|
TestSettingsStore Settings,
|
||||||
TestMlClient MlClient,
|
TestMlClient MlClient,
|
||||||
TestAiClassifier AiClassifier);
|
TestAiClassifier AiClassifier);
|
||||||
|
|
||||||
@@ -36,18 +36,18 @@ public sealed class PipelineWorkerServiceTests
|
|||||||
// Возвращает: Воркер и фейки (сценарий до-настраивает строки очереди/ответы портов).
|
// Возвращает: Воркер и фейки (сценарий до-настраивает строки очереди/ответы портов).
|
||||||
private static Context CreateContext(TestPipelineStore? pipelineStore = null)
|
private static Context CreateContext(TestPipelineStore? pipelineStore = null)
|
||||||
{
|
{
|
||||||
var settings = new FakeSettingsStore();
|
var settings = new TestSettingsStore();
|
||||||
var store = pipelineStore ?? new TestPipelineStore();
|
var store = pipelineStore ?? new TestPipelineStore();
|
||||||
var kanjStore = new FakeKanjStore();
|
var kanjStore = new FakeKanjStore();
|
||||||
var mlClient = new TestMlClient { 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.Store);
|
||||||
var fieldsParser = new LocalFieldsParser(settings);
|
var fieldsParser = new LocalFieldsParser(settings.Store);
|
||||||
var processing = new PipelineProcessingService(store.Store, mlClient.Client, 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.Store);
|
||||||
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.Client, aiClassifier.Classifier, processing, writer, fieldsParser);
|
store.Store, settings.Store, 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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
using Deal.Modules.Settings.Application.Abstractions;
|
using Deal.Modules.Settings.Application.Abstractions;
|
||||||
using Deal.Modules.Settings.Application.Models;
|
using Deal.Modules.Settings.Application.Models;
|
||||||
|
using Deal.Tests.Unit.Support;
|
||||||
using Deal.Modules.Settings.Application.Services;
|
using Deal.Modules.Settings.Application.Services;
|
||||||
using Deal.Tests.Unit.Modules.Settings;
|
using Deal.Tests.Unit.Modules.Settings;
|
||||||
using NSubstitute;
|
using NSubstitute;
|
||||||
@@ -14,7 +15,7 @@ public sealed class RatesServiceTests
|
|||||||
{
|
{
|
||||||
private const long FixedTimestampMs = 1_752_000_000_000;
|
private const long FixedTimestampMs = 1_752_000_000_000;
|
||||||
|
|
||||||
private readonly FakeSettingsStore _store = new();
|
private readonly TestSettingsStore _store = new();
|
||||||
|
|
||||||
// ─── GetAsync: кэш и дефолт ─────────────────────────────────────────────
|
// ─── GetAsync: кэш и дефолт ─────────────────────────────────────────────
|
||||||
|
|
||||||
@@ -418,7 +419,7 @@ public sealed class RatesServiceTests
|
|||||||
|
|
||||||
private RatesService CreateService(IRatesSource source, IReadOnlyList<IRatesChangedListener> listeners)
|
private RatesService CreateService(IRatesSource source, IReadOnlyList<IRatesChangedListener> listeners)
|
||||||
{
|
{
|
||||||
return new RatesService(_store, source, listeners);
|
return new RatesService(_store.Store, source, listeners);
|
||||||
}
|
}
|
||||||
|
|
||||||
// JSON значения ratesCache с одним курсом USD и заданным временем/источником.
|
// JSON значения ratesCache с одним курсом USD и заданным временем/источником.
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
using Deal.Modules.Settings.Application.Abstractions;
|
using Deal.Modules.Settings.Application.Abstractions;
|
||||||
using Deal.Modules.Settings.Application.Models;
|
using Deal.Modules.Settings.Application.Models;
|
||||||
|
using Deal.Tests.Unit.Support;
|
||||||
using Deal.Modules.Settings.Application.Services;
|
using Deal.Modules.Settings.Application.Services;
|
||||||
using Deal.Tests.Unit.Modules.Settings;
|
using Deal.Tests.Unit.Modules.Settings;
|
||||||
using NSubstitute;
|
using NSubstitute;
|
||||||
@@ -12,13 +13,13 @@ namespace Deal.Tests.Unit.Support;
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public sealed class SettingsServiceTests
|
public sealed class SettingsServiceTests
|
||||||
{
|
{
|
||||||
private readonly FakeSettingsStore _store = new();
|
private readonly TestSettingsStore _store = new();
|
||||||
private readonly ISecretCipher _cipher = TestCiphers.New();
|
private readonly ISecretCipher _cipher = TestCiphers.New();
|
||||||
private readonly SettingsService _service;
|
private readonly SettingsService _service;
|
||||||
|
|
||||||
public SettingsServiceTests()
|
public SettingsServiceTests()
|
||||||
{
|
{
|
||||||
_service = new SettingsService(_store, _cipher, Array.Empty<IRatesChangedListener>());
|
_service = new SettingsService(_store.Store, _cipher, Array.Empty<IRatesChangedListener>());
|
||||||
}
|
}
|
||||||
|
|
||||||
// ─── GET: снимок ─────────────────────────────────────────────────────────
|
// ─── GET: снимок ─────────────────────────────────────────────────────────
|
||||||
@@ -592,7 +593,7 @@ public sealed class SettingsServiceTests
|
|||||||
public async Task ApplyPatchAsync_TargetCurrencyInBody_NotifiesListenersAfterSave()
|
public async Task ApplyPatchAsync_TargetCurrencyInBody_NotifiesListenersAfterSave()
|
||||||
{
|
{
|
||||||
var listener = Substitute.For<IRatesChangedListener>();
|
var listener = Substitute.For<IRatesChangedListener>();
|
||||||
SettingsService local = new(_store, _cipher, new[] { listener });
|
SettingsService local = new(_store.Store, _cipher, new[] { listener });
|
||||||
|
|
||||||
PublicSettingsDto snapshot = await local.ApplyPatchAsync(JsonBody(new { targetCurrency = "usd" }), CancellationToken.None);
|
PublicSettingsDto snapshot = await local.ApplyPatchAsync(JsonBody(new { targetCurrency = "usd" }), CancellationToken.None);
|
||||||
|
|
||||||
@@ -605,7 +606,7 @@ public sealed class SettingsServiceTests
|
|||||||
public async Task ApplyPatchAsync_ConversionOnInBody_NotifiesListeners()
|
public async Task ApplyPatchAsync_ConversionOnInBody_NotifiesListeners()
|
||||||
{
|
{
|
||||||
var listener = Substitute.For<IRatesChangedListener>();
|
var listener = Substitute.For<IRatesChangedListener>();
|
||||||
SettingsService local = new(_store, _cipher, new[] { listener });
|
SettingsService local = new(_store.Store, _cipher, new[] { listener });
|
||||||
|
|
||||||
await local.ApplyPatchAsync(JsonBody(new { conversionOn = false }), CancellationToken.None);
|
await local.ApplyPatchAsync(JsonBody(new { conversionOn = false }), CancellationToken.None);
|
||||||
|
|
||||||
@@ -617,7 +618,7 @@ public sealed class SettingsServiceTests
|
|||||||
public async Task ApplyPatchAsync_UnrelatedKeys_DoesNotNotifyListeners()
|
public async Task ApplyPatchAsync_UnrelatedKeys_DoesNotNotifyListeners()
|
||||||
{
|
{
|
||||||
var listener = Substitute.For<IRatesChangedListener>();
|
var listener = Substitute.For<IRatesChangedListener>();
|
||||||
SettingsService local = new(_store, _cipher, new[] { listener });
|
SettingsService local = new(_store.Store, _cipher, new[] { listener });
|
||||||
|
|
||||||
await local.ApplyPatchAsync(JsonBody(new { minLen = 100, rateSource = "mock" }), CancellationToken.None);
|
await local.ApplyPatchAsync(JsonBody(new { minLen = 100, rateSource = "mock" }), CancellationToken.None);
|
||||||
|
|
||||||
@@ -628,7 +629,7 @@ public sealed class SettingsServiceTests
|
|||||||
public async Task ApplyPatchAsync_TargetCurrencyJsonNull_DoesNotNotifyListeners()
|
public async Task ApplyPatchAsync_TargetCurrencyJsonNull_DoesNotNotifyListeners()
|
||||||
{
|
{
|
||||||
var listener = Substitute.For<IRatesChangedListener>();
|
var listener = Substitute.For<IRatesChangedListener>();
|
||||||
SettingsService local = new(_store, _cipher, new[] { listener });
|
SettingsService local = new(_store.Store, _cipher, new[] { listener });
|
||||||
|
|
||||||
await local.ApplyPatchAsync(JsonBody(new Dictionary<string, object?> { ["targetCurrency"] = null }), CancellationToken.None);
|
await local.ApplyPatchAsync(JsonBody(new Dictionary<string, object?> { ["targetCurrency"] = null }), CancellationToken.None);
|
||||||
|
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ using Deal.Modules.Pipeline.Application.Models;
|
|||||||
using Deal.Modules.Pipeline.Application.Services;
|
using Deal.Modules.Pipeline.Application.Services;
|
||||||
using Deal.Modules.Settings.Application.Abstractions;
|
using Deal.Modules.Settings.Application.Abstractions;
|
||||||
using Deal.Modules.Settings.Application.Models;
|
using Deal.Modules.Settings.Application.Models;
|
||||||
|
using Deal.Tests.Unit.Support;
|
||||||
using Deal.Modules.Tenants.Application.Abstractions;
|
using Deal.Modules.Tenants.Application.Abstractions;
|
||||||
using Deal.Modules.Tenants.Application.Models;
|
using Deal.Modules.Tenants.Application.Models;
|
||||||
using Deal.SharedKernel.Tenants.Abstractions;
|
using Deal.SharedKernel.Tenants.Abstractions;
|
||||||
@@ -44,7 +45,7 @@ public sealed class StorageTickSchedulerTests
|
|||||||
{
|
{
|
||||||
FakeKanjStore storeA = StoreWithExpiredInboxCard("l_a_old");
|
FakeKanjStore storeA = StoreWithExpiredInboxCard("l_a_old");
|
||||||
FakeKanjStore storeB = StoreWithExpiredInboxCard("l_b_old");
|
FakeKanjStore storeB = StoreWithExpiredInboxCard("l_b_old");
|
||||||
var settings = new FakeSettingsStore();
|
var settings = new TestSettingsStore();
|
||||||
var tenants = new TestTenantRepository(Tenant(TenantA), Tenant(TenantB));
|
var tenants = new TestTenantRepository(Tenant(TenantA), Tenant(TenantB));
|
||||||
var tenantContext = new TenantContext();
|
var tenantContext = new TenantContext();
|
||||||
await using ServiceProvider provider = BuildProvider(tenants, tenantContext, storeA, storeB, settings);
|
await using ServiceProvider provider = BuildProvider(tenants, tenantContext, storeA, storeB, settings);
|
||||||
@@ -73,7 +74,7 @@ public sealed class StorageTickSchedulerTests
|
|||||||
FakeKanjStore storeA = StoreWithExpiredInboxCard("l_a_old");
|
FakeKanjStore storeA = StoreWithExpiredInboxCard("l_a_old");
|
||||||
var storeB = new FakeKanjStore();
|
var storeB = new FakeKanjStore();
|
||||||
storeB.SeedCard(Card("l_b_fresh", KanbanColumns.Inbox, ReceivedAtMsAgo(TimeSpan.FromHours(1))));
|
storeB.SeedCard(Card("l_b_fresh", KanbanColumns.Inbox, ReceivedAtMsAgo(TimeSpan.FromHours(1))));
|
||||||
var settings = new FakeSettingsStore();
|
var settings = new TestSettingsStore();
|
||||||
var tenantContext = new TenantContext();
|
var tenantContext = new TenantContext();
|
||||||
await using ServiceProvider provider = BuildProvider(
|
await using ServiceProvider provider = BuildProvider(
|
||||||
new TestTenantRepository(Tenant(TenantA), Tenant(TenantB)),
|
new TestTenantRepository(Tenant(TenantA), Tenant(TenantB)),
|
||||||
@@ -104,7 +105,7 @@ public sealed class StorageTickSchedulerTests
|
|||||||
var settingsByTenant = new Dictionary<Guid, ISettingsStore>
|
var settingsByTenant = new Dictionary<Guid, ISettingsStore>
|
||||||
{
|
{
|
||||||
[TenantA] = new ThrowingSettingsStore(),
|
[TenantA] = new ThrowingSettingsStore(),
|
||||||
[TenantB] = new FakeSettingsStore(),
|
[TenantB] = new TestSettingsStore().Store,
|
||||||
};
|
};
|
||||||
var storesByTenant = new Dictionary<Guid, FakeKanjStore>
|
var storesByTenant = new Dictionary<Guid, FakeKanjStore>
|
||||||
{
|
{
|
||||||
@@ -151,7 +152,7 @@ public sealed class StorageTickSchedulerTests
|
|||||||
public async Task RunCycle_PurgesExpiredRejectedRowsAndPublishesRejectedPurgeToast()
|
public async Task RunCycle_PurgesExpiredRejectedRowsAndPublishesRejectedPurgeToast()
|
||||||
{
|
{
|
||||||
var kanjStore = new FakeKanjStore();
|
var kanjStore = new FakeKanjStore();
|
||||||
var settings = new FakeSettingsStore();
|
var settings = new TestSettingsStore();
|
||||||
var tenantContext = new TenantContext();
|
var tenantContext = new TenantContext();
|
||||||
var pipelineStoreA = new TestPipelineStore();
|
var pipelineStoreA = new TestPipelineStore();
|
||||||
long nowMs = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
|
long nowMs = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
|
||||||
@@ -161,7 +162,7 @@ public sealed class StorageTickSchedulerTests
|
|||||||
new TestTenantRepository(Tenant(TenantA), Tenant(TenantB)).Repository,
|
new TestTenantRepository(Tenant(TenantA), Tenant(TenantB)).Repository,
|
||||||
tenantContext,
|
tenantContext,
|
||||||
new Dictionary<Guid, FakeKanjStore> { [TenantA] = kanjStore, [TenantB] = new FakeKanjStore() },
|
new Dictionary<Guid, FakeKanjStore> { [TenantA] = kanjStore, [TenantB] = new FakeKanjStore() },
|
||||||
new Dictionary<Guid, ISettingsStore> { [TenantA] = settings, [TenantB] = settings },
|
new Dictionary<Guid, ISettingsStore> { [TenantA] = settings.Store, [TenantB] = settings.Store },
|
||||||
new Dictionary<Guid, TestPipelineStore> { [TenantA] = pipelineStoreA, [TenantB] = new TestPipelineStore() });
|
new Dictionary<Guid, TestPipelineStore> { [TenantA] = pipelineStoreA, [TenantB] = new TestPipelineStore() });
|
||||||
|
|
||||||
SseBroker broker = provider.GetRequiredService<SseBroker>();
|
SseBroker broker = provider.GetRequiredService<SseBroker>();
|
||||||
@@ -191,7 +192,7 @@ public sealed class StorageTickSchedulerTests
|
|||||||
new TestTenantRepository(Tenant(TenantA), Tenant(TenantB)).Repository,
|
new TestTenantRepository(Tenant(TenantA), Tenant(TenantB)).Repository,
|
||||||
tenantContext,
|
tenantContext,
|
||||||
new Dictionary<Guid, FakeKanjStore> { [TenantA] = cardStoreA, [TenantB] = cardStoreB },
|
new Dictionary<Guid, FakeKanjStore> { [TenantA] = cardStoreA, [TenantB] = cardStoreB },
|
||||||
new Dictionary<Guid, ISettingsStore> { [TenantA] = new FakeSettingsStore(), [TenantB] = new FakeSettingsStore() },
|
new Dictionary<Guid, ISettingsStore> { [TenantA] = new TestSettingsStore().Store, [TenantB] = new TestSettingsStore().Store },
|
||||||
new Dictionary<Guid, TestPipelineStore> { [TenantA] = new(), [TenantB] = new() });
|
new Dictionary<Guid, TestPipelineStore> { [TenantA] = new(), [TenantB] = new() });
|
||||||
|
|
||||||
SseBroker broker = provider.GetRequiredService<SseBroker>();
|
SseBroker broker = provider.GetRequiredService<SseBroker>();
|
||||||
@@ -219,14 +220,14 @@ public sealed class StorageTickSchedulerTests
|
|||||||
{
|
{
|
||||||
var cardStoreA = new FakeKanjStore();
|
var cardStoreA = new FakeKanjStore();
|
||||||
cardStoreA.SeedCard(HoldCard("c_past", title: "Старое", reminderAtMs: NowMs() - 60_000));
|
cardStoreA.SeedCard(HoldCard("c_past", title: "Старое", reminderAtMs: NowMs() - 60_000));
|
||||||
var settingsA = new FakeSettingsStore();
|
var settingsA = new TestSettingsStore();
|
||||||
settingsA.Preload(SettingsKeys.RemindersEnabled, "false");
|
settingsA.Preload(SettingsKeys.RemindersEnabled, "false");
|
||||||
var tenantContext = new TenantContext();
|
var tenantContext = new TenantContext();
|
||||||
await using ServiceProvider provider = BuildProvider(
|
await using ServiceProvider provider = BuildProvider(
|
||||||
new TestTenantRepository(Tenant(TenantA)).Repository,
|
new TestTenantRepository(Tenant(TenantA)).Repository,
|
||||||
tenantContext,
|
tenantContext,
|
||||||
new Dictionary<Guid, FakeKanjStore> { [TenantA] = cardStoreA },
|
new Dictionary<Guid, FakeKanjStore> { [TenantA] = cardStoreA },
|
||||||
new Dictionary<Guid, ISettingsStore> { [TenantA] = settingsA },
|
new Dictionary<Guid, ISettingsStore> { [TenantA] = settingsA.Store },
|
||||||
new Dictionary<Guid, TestPipelineStore> { [TenantA] = new() });
|
new Dictionary<Guid, TestPipelineStore> { [TenantA] = new() });
|
||||||
|
|
||||||
SseBroker broker = provider.GetRequiredService<SseBroker>();
|
SseBroker broker = provider.GetRequiredService<SseBroker>();
|
||||||
@@ -252,7 +253,7 @@ public sealed class StorageTickSchedulerTests
|
|||||||
new TestTenantRepository(Tenant(TenantA), Tenant(TenantB)).Repository,
|
new TestTenantRepository(Tenant(TenantA), Tenant(TenantB)).Repository,
|
||||||
tenantContext,
|
tenantContext,
|
||||||
new Dictionary<Guid, FakeKanjStore> { [TenantA] = cardStoreA, [TenantB] = storeB },
|
new Dictionary<Guid, FakeKanjStore> { [TenantA] = cardStoreA, [TenantB] = storeB },
|
||||||
new Dictionary<Guid, ISettingsStore> { [TenantA] = new FakeSettingsStore(), [TenantB] = new FakeSettingsStore() },
|
new Dictionary<Guid, ISettingsStore> { [TenantA] = new TestSettingsStore().Store, [TenantB] = new TestSettingsStore().Store },
|
||||||
new Dictionary<Guid, TestPipelineStore> { [TenantA] = new(), [TenantB] = new() });
|
new Dictionary<Guid, TestPipelineStore> { [TenantA] = new(), [TenantB] = new() });
|
||||||
|
|
||||||
SseBroker broker = provider.GetRequiredService<SseBroker>();
|
SseBroker broker = provider.GetRequiredService<SseBroker>();
|
||||||
@@ -283,13 +284,13 @@ public sealed class StorageTickSchedulerTests
|
|||||||
TenantContext tenantContext,
|
TenantContext tenantContext,
|
||||||
FakeKanjStore storeA,
|
FakeKanjStore storeA,
|
||||||
FakeKanjStore storeB,
|
FakeKanjStore storeB,
|
||||||
FakeSettingsStore settings)
|
TestSettingsStore settings)
|
||||||
{
|
{
|
||||||
return BuildProvider(
|
return BuildProvider(
|
||||||
tenants.Repository,
|
tenants.Repository,
|
||||||
tenantContext,
|
tenantContext,
|
||||||
new Dictionary<Guid, FakeKanjStore> { [TenantA] = storeA, [TenantB] = storeB },
|
new Dictionary<Guid, FakeKanjStore> { [TenantA] = storeA, [TenantB] = storeB },
|
||||||
new Dictionary<Guid, ISettingsStore> { [TenantA] = settings, [TenantB] = settings });
|
new Dictionary<Guid, ISettingsStore> { [TenantA] = settings.Store, [TenantB] = settings.Store });
|
||||||
}
|
}
|
||||||
|
|
||||||
// Строит DI-провайдер теста по явным картам тенант → хранилище/настройки.
|
// Строит DI-провайдер теста по явным картам тенант → хранилище/настройки.
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ using Deal.Api.Events;
|
|||||||
using Deal.Grpc.Telegram;
|
using Deal.Grpc.Telegram;
|
||||||
using Deal.Modules.Settings.Application.Abstractions;
|
using Deal.Modules.Settings.Application.Abstractions;
|
||||||
using Deal.Modules.Settings.Application.Models;
|
using Deal.Modules.Settings.Application.Models;
|
||||||
|
using Deal.Tests.Unit.Support;
|
||||||
using Deal.Modules.Telegram.Application;
|
using Deal.Modules.Telegram.Application;
|
||||||
using Deal.Modules.Tenants.Application.Abstractions;
|
using Deal.Modules.Tenants.Application.Abstractions;
|
||||||
using Deal.Modules.Tenants.Application.Models;
|
using Deal.Modules.Tenants.Application.Models;
|
||||||
@@ -61,7 +62,7 @@ public sealed class TelegramIngressServiceTests
|
|||||||
public async Task SyncDialogs_AutoMonitorNewDisabled_ReturnsEmptyMonitoredMirror()
|
public async Task SyncDialogs_AutoMonitorNewDisabled_ReturnsEmptyMonitoredMirror()
|
||||||
{
|
{
|
||||||
var registry = TestRegistry.With(Tenant(TenantA));
|
var registry = TestRegistry.With(Tenant(TenantA));
|
||||||
var settings = new FakeSettingsStore();
|
var settings = new TestSettingsStore();
|
||||||
settings.Preload(SettingsKeys.AutoMonitorNew, "false");
|
settings.Preload(SettingsKeys.AutoMonitorNew, "false");
|
||||||
var dialogs = new TestTelegramStore();
|
var dialogs = new TestTelegramStore();
|
||||||
|
|
||||||
@@ -69,7 +70,7 @@ public sealed class TelegramIngressServiceTests
|
|||||||
registry,
|
registry,
|
||||||
services =>
|
services =>
|
||||||
{
|
{
|
||||||
services.AddScoped<ISettingsStore>(_ => settings);
|
services.AddScoped<ISettingsStore>(_ => settings.Store);
|
||||||
services.AddScoped<ITelegramStore>(_ => dialogs.Store);
|
services.AddScoped<ITelegramStore>(_ => dialogs.Store);
|
||||||
},
|
},
|
||||||
async channel =>
|
async channel =>
|
||||||
@@ -91,7 +92,7 @@ public sealed class TelegramIngressServiceTests
|
|||||||
[Fact]
|
[Fact]
|
||||||
public async Task ReportStatus_PersistsStatusAndPublishesTransitionToasts()
|
public async Task ReportStatus_PersistsStatusAndPublishesTransitionToasts()
|
||||||
{
|
{
|
||||||
var settings = new FakeSettingsStore();
|
var settings = new TestSettingsStore();
|
||||||
var registry = TestRegistry.With(Tenant(TenantA));
|
var registry = TestRegistry.With(Tenant(TenantA));
|
||||||
var broker = new SseBroker();
|
var broker = new SseBroker();
|
||||||
|
|
||||||
@@ -100,7 +101,7 @@ public sealed class TelegramIngressServiceTests
|
|||||||
services =>
|
services =>
|
||||||
{
|
{
|
||||||
services.AddSingleton<SseBroker>(broker);
|
services.AddSingleton<SseBroker>(broker);
|
||||||
services.AddScoped<ISettingsStore>(_ => settings);
|
services.AddScoped<ISettingsStore>(_ => settings.Store);
|
||||||
},
|
},
|
||||||
async channel =>
|
async channel =>
|
||||||
{
|
{
|
||||||
@@ -138,12 +139,12 @@ public sealed class TelegramIngressServiceTests
|
|||||||
[Fact]
|
[Fact]
|
||||||
public async Task ReportStatus_UnknownTenant_NotSavedWithoutRpcError()
|
public async Task ReportStatus_UnknownTenant_NotSavedWithoutRpcError()
|
||||||
{
|
{
|
||||||
var settings = new FakeSettingsStore();
|
var settings = new TestSettingsStore();
|
||||||
var registry = TestRegistry.With(Tenant(TenantA));
|
var registry = TestRegistry.With(Tenant(TenantA));
|
||||||
|
|
||||||
await RunAsync(
|
await RunAsync(
|
||||||
registry,
|
registry,
|
||||||
services => services.AddScoped<ISettingsStore>(_ => settings),
|
services => services.AddScoped<ISettingsStore>(_ => settings.Store),
|
||||||
async channel =>
|
async channel =>
|
||||||
{
|
{
|
||||||
var client = new IngressService.IngressServiceClient(channel);
|
var client = new IngressService.IngressServiceClient(channel);
|
||||||
@@ -261,7 +262,7 @@ public sealed class TelegramIngressServiceTests
|
|||||||
// phase: Ожидаемая фаза.
|
// phase: Ожидаемая фаза.
|
||||||
// connected: Ожидаемый флаг connected.
|
// connected: Ожидаемый флаг connected.
|
||||||
private static void AssertStatusStored(
|
private static void AssertStatusStored(
|
||||||
FakeSettingsStore settings,
|
TestSettingsStore settings,
|
||||||
string phase,
|
string phase,
|
||||||
bool connected)
|
bool connected)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -99,7 +99,7 @@ internal static class TelegramIngressTestHost
|
|||||||
.AddCheck("ready", () => HealthCheckResult.Healthy("хост готов"));
|
.AddCheck("ready", () => HealthCheckResult.Healthy("хост готов"));
|
||||||
}
|
}
|
||||||
|
|
||||||
builder.Services.AddScoped<ISettingsStore>(_ => new FakeSettingsStore());
|
builder.Services.AddScoped<ISettingsStore>(_ => new TestSettingsStore().Store);
|
||||||
builder.Services.AddScoped<ITelegramStore>(_ => new TestTelegramStore().Store);
|
builder.Services.AddScoped<ITelegramStore>(_ => new TestTelegramStore().Store);
|
||||||
builder.Services.AddScoped<ITelegramGateway>(_ => new TestTelegramGateway().Gateway);
|
builder.Services.AddScoped<ITelegramGateway>(_ => new TestTelegramGateway().Gateway);
|
||||||
builder.Services.AddTelegramModule();
|
builder.Services.AddTelegramModule();
|
||||||
|
|||||||
@@ -71,7 +71,7 @@ public sealed class TelegramSourceIngestObserverTests
|
|||||||
var store = new TestTelegramStore();
|
var store = new TestTelegramStore();
|
||||||
var dialogs = new DialogsService(
|
var dialogs = new DialogsService(
|
||||||
store.Store,
|
store.Store,
|
||||||
new FakeSettingsStore(),
|
new TestSettingsStore().Store,
|
||||||
new TestTelegramGateway().Gateway,
|
new TestTelegramGateway().Gateway,
|
||||||
NullLogger<DialogsService>.Instance);
|
NullLogger<DialogsService>.Instance);
|
||||||
var observer = new TelegramSourceIngestObserver(dialogs, NullLogger<TelegramSourceIngestObserver>.Instance);
|
var observer = new TelegramSourceIngestObserver(dialogs, NullLogger<TelegramSourceIngestObserver>.Instance);
|
||||||
|
|||||||
@@ -0,0 +1,62 @@
|
|||||||
|
using Deal.Modules.Settings.Application.Abstractions;
|
||||||
|
using Deal.Modules.Settings.Application.Models;
|
||||||
|
using NSubstitute;
|
||||||
|
|
||||||
|
namespace Deal.Tests.Unit.Support;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Подставка <see cref="ISettingsStore"/> на словаре: сервисы получают NSubstitute-подставку
|
||||||
|
/// (<see cref="Store"/>), тесты сеют/проверяют настройки через <see cref="Preload"/>,
|
||||||
|
/// <see cref="GetStoredJson"/> и <see cref="Keys"/>.
|
||||||
|
/// </summary>
|
||||||
|
public sealed class TestSettingsStore
|
||||||
|
{
|
||||||
|
private readonly Dictionary<string, SettingValue> _rows = new(StringComparer.Ordinal);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Подставка порта настроек (создаётся в конструкторе).
|
||||||
|
/// </summary>
|
||||||
|
public ISettingsStore Store { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Ключи сохранённых настроек.
|
||||||
|
/// </summary>
|
||||||
|
public IReadOnlyCollection<string> Keys => _rows.Keys.ToList();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Создаёт подставку с пустым словарём.
|
||||||
|
/// </summary>
|
||||||
|
public TestSettingsStore()
|
||||||
|
{
|
||||||
|
Store = Substitute.For<ISettingsStore>();
|
||||||
|
Store.GetAsync(Arg.Any<string>(), Arg.Any<CancellationToken>())
|
||||||
|
.Returns(ci => _rows.TryGetValue(ci.ArgAt<string>(0), out SettingValue? row) ? row : null);
|
||||||
|
Store.GetAllAsync(Arg.Any<CancellationToken>())
|
||||||
|
.Returns(ci => _rows.Values.ToList());
|
||||||
|
Store.When(s => s.SetAsync(Arg.Any<string>(), Arg.Any<string>(), Arg.Any<CancellationToken>()))
|
||||||
|
.Do(ci => _rows[ci.ArgAt<string>(0)] =
|
||||||
|
new SettingValue(ci.ArgAt<string>(0), ci.ArgAt<string>(1), DateTimeOffset.UtcNow));
|
||||||
|
Store.When(s => s.RemoveAsync(Arg.Any<string>(), Arg.Any<CancellationToken>()))
|
||||||
|
.Do(ci => _rows.Remove(ci.ArgAt<string>(0)));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Кладёт готовую строку
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="key">Ключ настройки.</param>
|
||||||
|
/// <param name="valueJson">Значение, сериализованное в JSON.</param>
|
||||||
|
public void Preload(string key, string valueJson)
|
||||||
|
{
|
||||||
|
_rows[key] = new SettingValue(key, valueJson, DateTimeOffset.UtcNow);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Возвращает JSON сохранённого значения или null, если ключа нет.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="key">Ключ настройки.</param>
|
||||||
|
/// <returns>JSON-строка значения или null.</returns>
|
||||||
|
public string? GetStoredJson(string key)
|
||||||
|
{
|
||||||
|
return _rows.TryGetValue(key, out SettingValue? row) ? row.ValueJson : null;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -26,7 +26,7 @@ public sealed class TokenUsageRecorderTests
|
|||||||
[Fact]
|
[Fact]
|
||||||
public async Task AddAsync_ChargesTenantLimitsAccumulatesLifetimeKvAndWritesEvent()
|
public async Task AddAsync_ChargesTenantLimitsAccumulatesLifetimeKvAndWritesEvent()
|
||||||
{
|
{
|
||||||
(FakeSettingsStore settings, TestTenantLimitStore limits, TestTokenUsageEventStore events, TokenUsageRecorder recorder) = CreateRecorder();
|
(TestSettingsStore settings, TestTenantLimitStore limits, TestTokenUsageEventStore events, TokenUsageRecorder recorder) = CreateRecorder();
|
||||||
|
|
||||||
await recorder.AddAsync(new Usage { Prompt = 500, Completion = 40, Total = 540 }, "deepseek", "deepseek-chat", CancellationToken.None);
|
await recorder.AddAsync(new Usage { Prompt = 500, Completion = 40, Total = 540 }, "deepseek", "deepseek-chat", CancellationToken.None);
|
||||||
|
|
||||||
@@ -52,7 +52,7 @@ public sealed class TokenUsageRecorderTests
|
|||||||
[Fact]
|
[Fact]
|
||||||
public async Task AddAsync_AccumulatesAcrossCalls()
|
public async Task AddAsync_AccumulatesAcrossCalls()
|
||||||
{
|
{
|
||||||
(FakeSettingsStore settings, TestTenantLimitStore limits, TestTokenUsageEventStore events, TokenUsageRecorder recorder) = CreateRecorder();
|
(TestSettingsStore settings, TestTenantLimitStore limits, TestTokenUsageEventStore events, TokenUsageRecorder recorder) = CreateRecorder();
|
||||||
|
|
||||||
await recorder.AddAsync(new Usage { Prompt = 500, Completion = 40, Total = 540 }, "deepseek", "m", CancellationToken.None);
|
await recorder.AddAsync(new Usage { Prompt = 500, Completion = 40, Total = 540 }, "deepseek", "m", CancellationToken.None);
|
||||||
await recorder.AddAsync(new Usage { Prompt = 300, Completion = 60, Total = 360 }, "deepseek", "m", CancellationToken.None);
|
await recorder.AddAsync(new Usage { Prompt = 300, Completion = 60, Total = 360 }, "deepseek", "m", CancellationToken.None);
|
||||||
@@ -68,7 +68,7 @@ public sealed class TokenUsageRecorderTests
|
|||||||
[Fact]
|
[Fact]
|
||||||
public async Task AddAsync_NullUsage_IsNoop()
|
public async Task AddAsync_NullUsage_IsNoop()
|
||||||
{
|
{
|
||||||
(FakeSettingsStore settings, TestTenantLimitStore limits, TestTokenUsageEventStore events, TokenUsageRecorder recorder) = CreateRecorder();
|
(TestSettingsStore settings, TestTenantLimitStore limits, TestTokenUsageEventStore events, TokenUsageRecorder recorder) = CreateRecorder();
|
||||||
|
|
||||||
await recorder.AddAsync(null, "deepseek", "m", CancellationToken.None);
|
await recorder.AddAsync(null, "deepseek", "m", CancellationToken.None);
|
||||||
|
|
||||||
@@ -80,7 +80,7 @@ public sealed class TokenUsageRecorderTests
|
|||||||
[Fact]
|
[Fact]
|
||||||
public async Task AddAsync_ZeroTotal_AccumulatesKvAndWritesEventButSkipsTenantLimits()
|
public async Task AddAsync_ZeroTotal_AccumulatesKvAndWritesEventButSkipsTenantLimits()
|
||||||
{
|
{
|
||||||
(FakeSettingsStore settings, TestTenantLimitStore limits, TestTokenUsageEventStore events, TokenUsageRecorder recorder) = CreateRecorder();
|
(TestSettingsStore settings, TestTenantLimitStore limits, TestTokenUsageEventStore events, TokenUsageRecorder recorder) = CreateRecorder();
|
||||||
|
|
||||||
// Ответ модели без оценки (total=0): lifetime-KV и событие пишутся (вызов состоялся), строка лимита
|
// Ответ модели без оценки (total=0): lifetime-KV и событие пишутся (вызов состоялся), строка лимита
|
||||||
// нулевым расходом не заводится — ленивый GetOrCreate остаётся первому реальному списанию/чтению.
|
// нулевым расходом не заводится — ленивый GetOrCreate остаётся первому реальному списанию/чтению.
|
||||||
@@ -96,11 +96,11 @@ public sealed class TokenUsageRecorderTests
|
|||||||
[Fact]
|
[Fact]
|
||||||
public async Task AddAsync_OutsideTenantContext_Throws()
|
public async Task AddAsync_OutsideTenantContext_Throws()
|
||||||
{
|
{
|
||||||
var settings = new FakeSettingsStore();
|
var settings = new TestSettingsStore();
|
||||||
var limits = new TestTenantLimitStore();
|
var limits = new TestTenantLimitStore();
|
||||||
ITenantContext tenantContext = new TenantContext(); // без SetTenant — списание вне tenant-контекста невозможно.
|
ITenantContext tenantContext = new TenantContext(); // без SetTenant — списание вне tenant-контекста невозможно.
|
||||||
var recorder = new TokenUsageRecorder(
|
var recorder = new TokenUsageRecorder(
|
||||||
settings, limits.Store, tenantContext, new TokenUsageEventService(new TestTokenUsageEventStore().Store));
|
settings.Store, limits.Store, tenantContext, new TokenUsageEventService(new TestTokenUsageEventStore().Store));
|
||||||
|
|
||||||
await Assert.ThrowsAsync<InvalidOperationException>(
|
await Assert.ThrowsAsync<InvalidOperationException>(
|
||||||
() => recorder.AddAsync(new Usage { Prompt = 1, Completion = 1, Total = 2 }, "deepseek", "m", CancellationToken.None));
|
() => recorder.AddAsync(new Usage { Prompt = 1, Completion = 1, Total = 2 }, "deepseek", "m", CancellationToken.None));
|
||||||
@@ -111,7 +111,7 @@ public sealed class TokenUsageRecorderTests
|
|||||||
[Fact]
|
[Fact]
|
||||||
public async Task AddEstimatedAsync_MlCall_WritesMlEventOnlyWithCharEstimate()
|
public async Task AddEstimatedAsync_MlCall_WritesMlEventOnlyWithCharEstimate()
|
||||||
{
|
{
|
||||||
(FakeSettingsStore settings, TestTenantLimitStore limits, TestTokenUsageEventStore events, TokenUsageRecorder recorder) = CreateRecorder();
|
(TestSettingsStore settings, TestTenantLimitStore limits, TestTokenUsageEventStore events, TokenUsageRecorder recorder) = CreateRecorder();
|
||||||
|
|
||||||
// 20 символов → 5 токенов (≈chars/4); бюджет и lifetime AI-счётчик не трогаются.
|
// 20 символов → 5 токенов (≈chars/4); бюджет и lifetime AI-счётчик не трогаются.
|
||||||
long estimated = await recorder.AddEstimatedAsync(
|
long estimated = await recorder.AddEstimatedAsync(
|
||||||
@@ -141,14 +141,14 @@ public sealed class TokenUsageRecorderTests
|
|||||||
|
|
||||||
// Создаёт recorder сценария: tenant-контекст + фейки KV/лимитов/истории.
|
// Создаёт recorder сценария: tenant-контекст + фейки KV/лимитов/истории.
|
||||||
// Возвращает: Кортеж (настройки, лимиты, история, recorder).
|
// Возвращает: Кортеж (настройки, лимиты, история, recorder).
|
||||||
private static (FakeSettingsStore Settings, TestTenantLimitStore Limits, TestTokenUsageEventStore Events, TokenUsageRecorder Recorder) CreateRecorder()
|
private static (TestSettingsStore Settings, TestTenantLimitStore Limits, TestTokenUsageEventStore Events, TokenUsageRecorder Recorder) CreateRecorder()
|
||||||
{
|
{
|
||||||
var settings = new FakeSettingsStore();
|
var settings = new TestSettingsStore();
|
||||||
var limits = new TestTenantLimitStore();
|
var limits = new TestTenantLimitStore();
|
||||||
var events = new TestTokenUsageEventStore();
|
var events = new TestTokenUsageEventStore();
|
||||||
ITenantContext tenantContext = new TenantContext();
|
ITenantContext tenantContext = new TenantContext();
|
||||||
tenantContext.SetTenant(new TenantId(TenantIdValue));
|
tenantContext.SetTenant(new TenantId(TenantIdValue));
|
||||||
var recorder = new TokenUsageRecorder(settings, limits.Store, tenantContext, new TokenUsageEventService(events.Store));
|
var recorder = new TokenUsageRecorder(settings.Store, limits.Store, tenantContext, new TokenUsageEventService(events.Store));
|
||||||
return (settings, limits, events, recorder);
|
return (settings, limits, events, recorder);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user