Перевести FakeTokenUsageEventStore на NSubstitute
ci / build-test (push) Successful in 2m52s

Хелпер Support/TestTokenUsageEventStore: записи + полная логика агрегации
(фильтры, группировки day/tenant/provider/model) читают состояние на момент
вызова. Потребители (10 файлов) перетипизированы на .Store, фейк удалён,
тесты 1340 зелёные.
This commit is contained in:
Rustam Khalimov
2026-09-12 22:49:41 +03:00
parent 1db703c93b
commit 31add804eb
11 changed files with 111 additions and 28 deletions
@@ -165,7 +165,7 @@ public sealed class MlOutboxFlushSchedulerTests
services.AddScoped<IMlLearningStore>(provider => storesByTenant[TenantOf(provider)]); services.AddScoped<IMlLearningStore>(provider => storesByTenant[TenantOf(provider)]);
services.AddScoped<ISettingsStore>(_ => new FakeSettingsStore()); services.AddScoped<ISettingsStore>(_ => new FakeSettingsStore());
services.AddScoped<ITenantLimitStore>(_ => new FakeTenantLimitStore()); services.AddScoped<ITenantLimitStore>(_ => new FakeTenantLimitStore());
services.AddScoped<TokenUsageEventService>(_ => new TokenUsageEventService(new FakeTokenUsageEventStore())); services.AddScoped<TokenUsageEventService>(_ => new TokenUsageEventService(new TestTokenUsageEventStore().Store));
services.AddScoped<TokenUsageRecorder>(); services.AddScoped<TokenUsageRecorder>();
services.AddLogging(); services.AddLogging();
return services.BuildServiceProvider(); return services.BuildServiceProvider();
@@ -139,7 +139,7 @@ public sealed class GrpcAiToolsTests
tenantContext, tenantContext,
connection, connection,
new AiProviderConfigBuilder(settings, cipher), new AiProviderConfigBuilder(settings, cipher),
new TokenUsageRecorder(settings, limits ?? new FakeTenantLimitStore(), tenantContext, new TokenUsageEventService(new FakeTokenUsageEventStore())), new TokenUsageRecorder(settings, limits ?? new FakeTenantLimitStore(), tenantContext, new TokenUsageEventService(new TestTokenUsageEventStore().Store)),
NullLogger<GrpcAiTools>.Instance); NullLogger<GrpcAiTools>.Instance);
} }
} }
@@ -326,7 +326,7 @@ public sealed class GrpcMlClientTests
settings ?? new FakeSettingsStore(), settings ?? new FakeSettingsStore(),
new FakeTenantLimitStore(), new FakeTenantLimitStore(),
tenantContext, tenantContext,
new TokenUsageEventService(new FakeTokenUsageEventStore())), new TokenUsageEventService(new TestTokenUsageEventStore().Store)),
NullLogger<GrpcMlClient>.Instance); NullLogger<GrpcMlClient>.Instance);
} }
} }
@@ -149,7 +149,7 @@ public sealed class IntegrationsDiTests
services.AddScoped<ICardStore>(_ => new FakeKanjStore()); services.AddScoped<ICardStore>(_ => new FakeKanjStore());
services.AddScoped<IMlLearningStore>(_ => new FakeMlLearningStore()); services.AddScoped<IMlLearningStore>(_ => new FakeMlLearningStore());
services.AddScoped<ITenantLimitStore>(_ => new FakeTenantLimitStore()); services.AddScoped<ITenantLimitStore>(_ => new FakeTenantLimitStore());
services.AddScoped<TokenUsageEventService>(_ => new TokenUsageEventService(new FakeTokenUsageEventStore())); services.AddScoped<TokenUsageEventService>(_ => new TokenUsageEventService(new TestTokenUsageEventStore().Store));
services.AddScoped<LocalFieldsParser>(); services.AddScoped<LocalFieldsParser>();
services.AddScoped<AiClassifyContextBuilder>(); services.AddScoped<AiClassifyContextBuilder>();
services.AddLogging(); services.AddLogging();
@@ -181,7 +181,7 @@ public sealed class PipelineWorkerGrpcAiTests
connection, connection,
new AiProviderConfigBuilder(settings, TestCiphers.New()), new AiProviderConfigBuilder(settings, TestCiphers.New()),
new AiClassifyContextBuilder(settings, kanjStore), new AiClassifyContextBuilder(settings, kanjStore),
new TokenUsageRecorder(settings, limits, tenantContext, new TokenUsageEventService(new FakeTokenUsageEventStore())), new TokenUsageRecorder(settings, limits, tenantContext, new TokenUsageEventService(new TestTokenUsageEventStore().Store)),
NullLogger<GrpcAiClassifier>.Instance); NullLogger<GrpcAiClassifier>.Instance);
IAiClassifier aiClassifier = budgeted IAiClassifier aiClassifier = budgeted
? new BudgetedAiClassifier( ? new BudgetedAiClassifier(
@@ -1,4 +1,5 @@
using Deal.Modules.Tenants.Application.Models; using Deal.Modules.Tenants.Application.Models;
using Deal.Tests.Unit.Support;
using Deal.Modules.Tenants.Application.Services; using Deal.Modules.Tenants.Application.Services;
namespace Deal.Tests.Unit.Modules.Tenants; namespace Deal.Tests.Unit.Modules.Tenants;
@@ -13,8 +14,8 @@ public sealed class TokenUsageEventServiceTests
[Fact] [Fact]
public async Task AppendAsync_StampsAtWithUtcNow() public async Task AppendAsync_StampsAtWithUtcNow()
{ {
var store = new FakeTokenUsageEventStore(); var store = new TestTokenUsageEventStore();
var service = new TokenUsageEventService(store); var service = new TokenUsageEventService(store.Store);
DateTimeOffset before = DateTimeOffset.UtcNow; DateTimeOffset before = DateTimeOffset.UtcNow;
await service.AppendAsync( await service.AppendAsync(
@@ -31,9 +32,9 @@ public sealed class TokenUsageEventServiceTests
[Fact] [Fact]
public async Task AggregateAsync_ProxiesStoreGrouping() public async Task AggregateAsync_ProxiesStoreGrouping()
{ {
var store = new FakeTokenUsageEventStore(); var store = new TestTokenUsageEventStore();
var service = new TokenUsageEventService(store); var service = new TokenUsageEventService(store.Store);
await store.AppendAsync( await store.Store.AppendAsync(
new TokenUsageEventDto( new TokenUsageEventDto(
TenantId, DateTimeOffset.UtcNow, "deepseek", "m", TokenUsageEventKinds.Ai, 10, 5, 15, null), TenantId, DateTimeOffset.UtcNow, "deepseek", "m", TokenUsageEventKinds.Ai, 10, 5, 15, null),
CancellationToken.None); CancellationToken.None);
@@ -297,7 +297,7 @@ public sealed class GrpcAiClassifierTests
connection, connection,
new AiProviderConfigBuilder(settings, cipher), new AiProviderConfigBuilder(settings, cipher),
new AiClassifyContextBuilder(settings, kanjStore), new AiClassifyContextBuilder(settings, kanjStore),
new TokenUsageRecorder(settings, limits ?? new FakeTenantLimitStore(), tenantContext, new TokenUsageEventService(new FakeTokenUsageEventStore())), new TokenUsageRecorder(settings, limits ?? new FakeTenantLimitStore(), tenantContext, new TokenUsageEventService(new TestTokenUsageEventStore().Store)),
NullLogger<GrpcAiClassifier>.Instance); NullLogger<GrpcAiClassifier>.Instance);
} }
@@ -48,7 +48,7 @@ public sealed class OperatorAnalyticsEndpointsHttpTests
await SeedAuditAsync(auditStore, AuditEvents.TenantLogout, AuditActorTypes.Tenant, ActiveTenant); await SeedAuditAsync(auditStore, AuditEvents.TenantLogout, AuditActorTypes.Tenant, ActiveTenant);
await SeedAuditAsync(auditStore, AuditEvents.TenantLoginFailed, AuditActorTypes.Tenant, ActiveTenant); await SeedAuditAsync(auditStore, AuditEvents.TenantLoginFailed, AuditActorTypes.Tenant, ActiveTenant);
var events = new FakeTokenUsageEventStore(); var events = new TestTokenUsageEventStore();
await SeedTokensAsync(events, ActiveTenant, provider: "deepseek", prompt: 100, completion: 50, total: 150); await SeedTokensAsync(events, ActiveTenant, provider: "deepseek", prompt: 100, completion: 50, total: 150);
await SeedTokensAsync(events, ActiveTenant, provider: "openai", prompt: 40, completion: 10, total: 50); await SeedTokensAsync(events, ActiveTenant, provider: "openai", prompt: 40, completion: 10, total: 50);
@@ -127,7 +127,7 @@ public sealed class OperatorAnalyticsEndpointsHttpTests
[Fact] [Fact]
public async Task Tokens_GroupsByProviderWithTotal_AndRejectsUnknownGroupBy() public async Task Tokens_GroupsByProviderWithTotal_AndRejectsUnknownGroupBy()
{ {
var events = new FakeTokenUsageEventStore(); var events = new TestTokenUsageEventStore();
await SeedTokensAsync(events, ActiveTenant, provider: "deepseek", prompt: 100, completion: 50, total: 150); await SeedTokensAsync(events, ActiveTenant, provider: "deepseek", prompt: 100, completion: 50, total: 150);
await SeedTokensAsync(events, SuspendedTenant, provider: "openai", prompt: 40, completion: 10, total: 50); await SeedTokensAsync(events, SuspendedTenant, provider: "openai", prompt: 40, completion: 10, total: 50);
@@ -293,13 +293,13 @@ public sealed class OperatorAnalyticsEndpointsHttpTests
CancellationToken.None); CancellationToken.None);
private static Task SeedTokensAsync( private static Task SeedTokensAsync(
FakeTokenUsageEventStore store, TestTokenUsageEventStore store,
Guid tenantId, Guid tenantId,
string provider, string provider,
long prompt, long prompt,
long completion, long completion,
long total) => long total) =>
store.AppendAsync( store.Store.AppendAsync(
new TokenUsageEventDto( new TokenUsageEventDto(
TenantId: tenantId, TenantId: tenantId,
At: DateTimeOffset.UtcNow.AddMinutes(-5), At: DateTimeOffset.UtcNow.AddMinutes(-5),
@@ -121,7 +121,7 @@ internal static class OperatorAuthHttpHost
TestTenantStore? tenantStore = null, TestTenantStore? tenantStore = null,
FakeTenantLimitStore? limitStore = null, FakeTenantLimitStore? limitStore = null,
RateLimitOptions? rateLimitOptions = null, RateLimitOptions? rateLimitOptions = null,
FakeTokenUsageEventStore? tokenUsageStore = null) => TestTokenUsageEventStore? tokenUsageStore = null) =>
await RunCoreAsync( await RunCoreAsync(
operatorStore, operatorStore,
userStore, userStore,
@@ -167,14 +167,14 @@ internal static class OperatorAuthHttpHost
FakeTenantLimitStore? limitStore, FakeTenantLimitStore? limitStore,
Func<string, FakeOperatorAuthStore, FakeAuthStore, TestTenantStore, TestInviteStore, FakeAuditLogStore, FakeTenantLimitStore, TestGlobalSettingsStore, Task> scenario, Func<string, FakeOperatorAuthStore, FakeAuthStore, TestTenantStore, TestInviteStore, FakeAuditLogStore, FakeTenantLimitStore, TestGlobalSettingsStore, Task> scenario,
RateLimitOptions? rateLimitOptions = null, RateLimitOptions? rateLimitOptions = null,
FakeTokenUsageEventStore? tokenUsageStore = null, TestTokenUsageEventStore? tokenUsageStore = null,
TestGlobalSettingsStore? globalSettingsStore = null) TestGlobalSettingsStore? globalSettingsStore = null)
{ {
FakeAuditLogStore effectiveAuditStore = auditStore ?? new FakeAuditLogStore(); FakeAuditLogStore effectiveAuditStore = auditStore ?? new FakeAuditLogStore();
TestInviteStore effectiveInviteStore = inviteStore ?? new TestInviteStore(); TestInviteStore effectiveInviteStore = inviteStore ?? new TestInviteStore();
TestTenantStore effectiveTenantStore = tenantStore ?? new TestTenantStore(); TestTenantStore effectiveTenantStore = tenantStore ?? new TestTenantStore();
FakeTenantLimitStore effectiveLimitStore = limitStore ?? new FakeTenantLimitStore(); FakeTenantLimitStore effectiveLimitStore = limitStore ?? new FakeTenantLimitStore();
FakeTokenUsageEventStore effectiveTokenUsageStore = tokenUsageStore ?? new FakeTokenUsageEventStore(); TestTokenUsageEventStore effectiveTokenUsageStore = tokenUsageStore ?? new TestTokenUsageEventStore();
TestGlobalSettingsStore effectiveGlobalSettingsStore = globalSettingsStore ?? new TestGlobalSettingsStore(); TestGlobalSettingsStore effectiveGlobalSettingsStore = globalSettingsStore ?? new TestGlobalSettingsStore();
RateLimitOptions effectiveRateLimitOptions = rateLimitOptions ?? new RateLimitOptions(); RateLimitOptions effectiveRateLimitOptions = rateLimitOptions ?? new RateLimitOptions();
int port = TestPort.Allocate(); int port = TestPort.Allocate();
@@ -196,7 +196,7 @@ internal static class OperatorAuthHttpHost
builder.Services.AddSingleton<ISecretCipher>(TestCiphers.New()); builder.Services.AddSingleton<ISecretCipher>(TestCiphers.New());
// Сервис глобальных ключей Telegram (операторские ручки /api/operator/settings/telegram-keys). // Сервис глобальных ключей Telegram (операторские ручки /api/operator/settings/telegram-keys).
builder.Services.AddScoped<TelegramKeysService>(); builder.Services.AddScoped<TelegramKeysService>();
builder.Services.AddSingleton<ITokenUsageEventStore>(effectiveTokenUsageStore); builder.Services.AddSingleton<ITokenUsageEventStore>(effectiveTokenUsageStore.Store);
builder.Services.AddSingleton(_ => new TestTenantProvisioner().Provisioner); builder.Services.AddSingleton(_ => new TestTenantProvisioner().Provisioner);
builder.Services.AddScoped<TenantSchemaMigrationService>(); builder.Services.AddScoped<TenantSchemaMigrationService>();
// Опции кук — по умолчанию (deal_session/deal_operator_session, без конфиг-секции в тесте). // Опции кук — по умолчанию (deal_session/deal_operator_session, без конфиг-секции в тесте).
@@ -0,0 +1,81 @@
using Deal.Modules.Tenants.Application.Abstractions;
using Deal.Modules.Tenants.Application.Models;
using NSubstitute;
namespace Deal.Tests.Unit.Support;
/// <summary>
/// Подставка <see cref="ITokenUsageEventStore"/> на списках: сервисы получают NSubstitute-подставку
/// (<see cref="Store"/>), тесты проверяют записи через <see cref="Records"/>.
/// </summary>
public sealed class TestTokenUsageEventStore
{
private readonly List<TokenUsageEventDto> _records = [];
/// <summary>
/// Подставка порта событий расхода токенов (создаётся в конструкторе).
/// </summary>
public ITokenUsageEventStore Store { get; }
/// <summary>
/// Записи хранилища в порядке добавления.
/// </summary>
public IReadOnlyList<TokenUsageEventDto> Records => _records;
/// <summary>
/// Создаёт подставку с пустым хранилищем.
/// </summary>
public TestTokenUsageEventStore()
{
Store = Substitute.For<ITokenUsageEventStore>();
Store.When(s => s.AppendAsync(Arg.Any<TokenUsageEventDto>(), Arg.Any<CancellationToken>()))
.Do(ci => _records.Add(ci.Arg<TokenUsageEventDto>()));
Store.AggregateAsync(Arg.Any<TokenUsageEventQueryDto>(), Arg.Any<CancellationToken>())
.Returns(ci => Aggregate(ci.Arg<TokenUsageEventQueryDto>()));
}
private IReadOnlyList<TokenUsageAggregateDto> Aggregate(TokenUsageEventQueryDto query)
{
IEnumerable<TokenUsageEventDto> filtered = _records.Where(record =>
(query.TenantId is null || record.TenantId == query.TenantId) &&
(string.IsNullOrWhiteSpace(query.Provider) || record.Provider == query.Provider) &&
(string.IsNullOrWhiteSpace(query.Model) || record.Model == query.Model) &&
(string.IsNullOrWhiteSpace(query.Kind) || record.Kind == query.Kind) &&
(query.From is null || record.At >= query.From.Value) &&
(query.To is null || record.At <= query.To.Value));
return query.GroupBy switch
{
TokenUsageGroupBys.Day => GroupByDay(filtered),
TokenUsageGroupBys.Tenant => GroupByString(filtered, record => record.TenantId.ToString("D")),
TokenUsageGroupBys.Provider => GroupByString(filtered, record => record.Provider),
TokenUsageGroupBys.Model => GroupByString(filtered, record => record.Model),
_ => throw new ArgumentException($"Неизвестная группировка: '{query.GroupBy}'.", nameof(query)),
};
}
private static IReadOnlyList<TokenUsageAggregateDto> GroupByDay(IEnumerable<TokenUsageEventDto> source) =>
source
.GroupBy(record => record.At.UtcDateTime.Date)
.OrderBy(group => group.Key)
.Select(group => ToAggregate(group.Key.ToString("yyyy-MM-dd"), group))
.ToList();
private static IReadOnlyList<TokenUsageAggregateDto> GroupByString(IEnumerable<TokenUsageEventDto> source, Func<TokenUsageEventDto, string> keySelector) =>
source
.GroupBy(keySelector)
.Select(group => ToAggregate(group.Key, group))
.OrderByDescending(row => row.TotalTokens)
.ToList();
private static TokenUsageAggregateDto ToAggregate(string key, IEnumerable<TokenUsageEventDto> group)
{
List<TokenUsageEventDto> rows = group.ToList();
return new TokenUsageAggregateDto(
key,
rows.Sum(row => row.PromptTokens),
rows.Sum(row => row.CompletionTokens),
rows.Sum(row => row.TotalTokens),
rows.Count);
}
}
@@ -4,6 +4,7 @@ using Deal.Infrastructure.Data;
using Deal.Infrastructure.Integrations.Services; using Deal.Infrastructure.Integrations.Services;
using Deal.Modules.Settings.Application.Models; using Deal.Modules.Settings.Application.Models;
using Deal.Modules.Tenants.Application.Models; using Deal.Modules.Tenants.Application.Models;
using Deal.Tests.Unit.Support;
using Deal.Modules.Tenants.Application.Services; using Deal.Modules.Tenants.Application.Services;
using Deal.SharedKernel.Tenants.Models; using Deal.SharedKernel.Tenants.Models;
using Deal.Tests.Unit.Modules.Settings; using Deal.Tests.Unit.Modules.Settings;
@@ -25,7 +26,7 @@ public sealed class TokenUsageRecorderTests
[Fact] [Fact]
public async Task AddAsync_ChargesTenantLimitsAccumulatesLifetimeKvAndWritesEvent() public async Task AddAsync_ChargesTenantLimitsAccumulatesLifetimeKvAndWritesEvent()
{ {
(FakeSettingsStore settings, FakeTenantLimitStore limits, FakeTokenUsageEventStore events, TokenUsageRecorder recorder) = CreateRecorder(); (FakeSettingsStore settings, FakeTenantLimitStore 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);
@@ -51,7 +52,7 @@ public sealed class TokenUsageRecorderTests
[Fact] [Fact]
public async Task AddAsync_AccumulatesAcrossCalls() public async Task AddAsync_AccumulatesAcrossCalls()
{ {
(FakeSettingsStore settings, FakeTenantLimitStore limits, FakeTokenUsageEventStore events, TokenUsageRecorder recorder) = CreateRecorder(); (FakeSettingsStore settings, FakeTenantLimitStore 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);
@@ -67,7 +68,7 @@ public sealed class TokenUsageRecorderTests
[Fact] [Fact]
public async Task AddAsync_NullUsage_IsNoop() public async Task AddAsync_NullUsage_IsNoop()
{ {
(FakeSettingsStore settings, FakeTenantLimitStore limits, FakeTokenUsageEventStore events, TokenUsageRecorder recorder) = CreateRecorder(); (FakeSettingsStore settings, FakeTenantLimitStore limits, TestTokenUsageEventStore events, TokenUsageRecorder recorder) = CreateRecorder();
await recorder.AddAsync(null, "deepseek", "m", CancellationToken.None); await recorder.AddAsync(null, "deepseek", "m", CancellationToken.None);
@@ -79,7 +80,7 @@ public sealed class TokenUsageRecorderTests
[Fact] [Fact]
public async Task AddAsync_ZeroTotal_AccumulatesKvAndWritesEventButSkipsTenantLimits() public async Task AddAsync_ZeroTotal_AccumulatesKvAndWritesEventButSkipsTenantLimits()
{ {
(FakeSettingsStore settings, FakeTenantLimitStore limits, FakeTokenUsageEventStore events, TokenUsageRecorder recorder) = CreateRecorder(); (FakeSettingsStore settings, FakeTenantLimitStore limits, TestTokenUsageEventStore events, TokenUsageRecorder recorder) = CreateRecorder();
// Ответ модели без оценки (total=0): lifetime-KV и событие пишутся (вызов состоялся), строка лимита // Ответ модели без оценки (total=0): lifetime-KV и событие пишутся (вызов состоялся), строка лимита
// нулевым расходом не заводится — ленивый GetOrCreate остаётся первому реальному списанию/чтению. // нулевым расходом не заводится — ленивый GetOrCreate остаётся первому реальному списанию/чтению.
@@ -99,7 +100,7 @@ public sealed class TokenUsageRecorderTests
var limits = new FakeTenantLimitStore(); var limits = new FakeTenantLimitStore();
ITenantContext tenantContext = new TenantContext(); // без SetTenant — списание вне tenant-контекста невозможно. ITenantContext tenantContext = new TenantContext(); // без SetTenant — списание вне tenant-контекста невозможно.
var recorder = new TokenUsageRecorder( var recorder = new TokenUsageRecorder(
settings, limits, tenantContext, new TokenUsageEventService(new FakeTokenUsageEventStore())); settings, limits, 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));
@@ -110,7 +111,7 @@ public sealed class TokenUsageRecorderTests
[Fact] [Fact]
public async Task AddEstimatedAsync_MlCall_WritesMlEventOnlyWithCharEstimate() public async Task AddEstimatedAsync_MlCall_WritesMlEventOnlyWithCharEstimate()
{ {
(FakeSettingsStore settings, FakeTenantLimitStore limits, FakeTokenUsageEventStore events, TokenUsageRecorder recorder) = CreateRecorder(); (FakeSettingsStore settings, FakeTenantLimitStore 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(
@@ -140,14 +141,14 @@ public sealed class TokenUsageRecorderTests
// Создаёт recorder сценария: tenant-контекст + фейки KV/лимитов/истории. // Создаёт recorder сценария: tenant-контекст + фейки KV/лимитов/истории.
// Возвращает: Кортеж (настройки, лимиты, история, recorder). // Возвращает: Кортеж (настройки, лимиты, история, recorder).
private static (FakeSettingsStore Settings, FakeTenantLimitStore Limits, FakeTokenUsageEventStore Events, TokenUsageRecorder Recorder) CreateRecorder() private static (FakeSettingsStore Settings, FakeTenantLimitStore Limits, TestTokenUsageEventStore Events, TokenUsageRecorder Recorder) CreateRecorder()
{ {
var settings = new FakeSettingsStore(); var settings = new FakeSettingsStore();
var limits = new FakeTenantLimitStore(); var limits = new FakeTenantLimitStore();
var events = new FakeTokenUsageEventStore(); 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, tenantContext, new TokenUsageEventService(events)); var recorder = new TokenUsageRecorder(settings, limits, tenantContext, new TokenUsageEventService(events.Store));
return (settings, limits, events, recorder); return (settings, limits, events, recorder);
} }
} }