Перевести FakeAiTools на NSubstitute
ci / build-test (push) Canceled after 0s

Хелпер Support/TestAiTools: по умолчанию оба метода бросают NotSupportedException
(семантика прежнего фейка — неожиданный вызов роняет тест), сценарии через
ReturnsFit/ThrowsOnEvaluate, счётчик EvaluateFitCalls -> Received/DidNotReceive.
Потребители (4 файла) перетипизированы на IAiTools, фейк удалён, тесты 1340.
This commit is contained in:
Rustam Khalimov
2026-09-12 20:23:41 +03:00
parent e3ade19353
commit 28b9777fa0
6 changed files with 76 additions and 65 deletions
@@ -5,6 +5,8 @@ using Deal.Infrastructure.Integrations.Services;
using Deal.Modules.Tenants.Application.Models;
using Deal.SharedKernel.Tenants.Models;
using Deal.Tests.Unit.Modules.Tenants;
using Deal.Tests.Unit.Support;
using NSubstitute;
using Microsoft.Extensions.Logging.Abstractions;
using Deal.Contracts.Integrations.Abstractions;
using Deal.SharedKernel.Tenants.Abstractions;
@@ -31,12 +33,12 @@ public sealed class BudgetedAiToolsTests
Context ctx = Create(
limits => limits.Preload(
TenantGuid, budgetTokens: 1000, TenantLimitPeriods.Month, PeriodStart, usedTokens: 1000));
ctx.Paid.Fit = new AiEvaluateFitResultDto(Fit: true, Reason: "подходит");
ctx.Paid.ReturnsFit(new AiEvaluateFitResultDto(Fit: true, Reason: "подходит"));
AiUnavailableException exception = await Assert.ThrowsAsync<AiUnavailableException>(
() => ctx.Decorator.EvaluateFitAsync("текст", "описание", new[] { "ключ" }, CancellationToken.None));
Assert.Equal(0, ctx.Paid.EvaluateFitCalls);
await ctx.Paid.DidNotReceive().EvaluateFitAsync(Arg.Any<string>(), Arg.Any<string>(), Arg.Any<IReadOnlyCollection<string>>(), Arg.Any<CancellationToken>());
Assert.Contains("исчерпан", exception.Message);
}
@@ -49,7 +51,7 @@ public sealed class BudgetedAiToolsTests
await Assert.ThrowsAsync<AiUnavailableException>(
() => ctx.Decorator.EvaluateFitAsync("текст", "описание", Array.Empty<string>(), CancellationToken.None));
Assert.Equal(0, ctx.Paid.EvaluateFitCalls);
await ctx.Paid.DidNotReceive().EvaluateFitAsync(Arg.Any<string>(), Arg.Any<string>(), Arg.Any<IReadOnlyCollection<string>>(), Arg.Any<CancellationToken>());
}
[Fact]
@@ -63,7 +65,7 @@ public sealed class BudgetedAiToolsTests
AiUnavailableException exception = await Assert.ThrowsAsync<AiUnavailableException>(
() => ctx.Decorator.EvaluateFitAsync("текст", "описание", Array.Empty<string>(), CancellationToken.None));
Assert.Equal(0, ctx.Paid.EvaluateFitCalls);
await ctx.Paid.DidNotReceive().EvaluateFitAsync(Arg.Any<string>(), Arg.Any<string>(), Arg.Any<IReadOnlyCollection<string>>(), Arg.Any<CancellationToken>());
Assert.Contains("приостановлен", exception.Message);
}
@@ -72,13 +74,13 @@ public sealed class BudgetedAiToolsTests
{
// Строки лимита нет — ленивый дефолт (10 000 000/месяц, расход 0) → Allowed=true → платный вызов.
Context ctx = Create();
ctx.Paid.Fit = new AiEvaluateFitResultDto(Fit: false, Reason: "не подходит");
ctx.Paid.ReturnsFit(new AiEvaluateFitResultDto(Fit: false, Reason: "не подходит"));
AiEvaluateFitResultDto result = await ctx.Decorator.EvaluateFitAsync(
"текст", "описание", new[] { "ключ" }, CancellationToken.None);
Assert.False(result.Fit);
Assert.Equal(1, ctx.Paid.EvaluateFitCalls);
await ctx.Paid.Received(1).EvaluateFitAsync(Arg.Any<string>(), Arg.Any<string>(), Arg.Any<IReadOnlyCollection<string>>(), Arg.Any<CancellationToken>());
}
[Fact]
@@ -116,7 +118,7 @@ public sealed class BudgetedAiToolsTests
public async Task GenerateKeywords_LargeBudget_DelegatesToPaid()
{
// Строки лимита нет — ленивый дефолт → Allowed=true → декоратор делегирует платному исполнителю.
// FakeAiTools.GenerateKeywordsAsync всегда бросает NotSupportedException (вызов не ждали) — само
// Подставка бросает NotSupportedException на GenerateKeywordsAsync (вызов не ждали) — само
// исключение здесь и есть признак делегирования (упало бы в тесте, если бы декоратор не позвал платный).
Context ctx = Create();
@@ -131,7 +133,7 @@ public sealed class BudgetedAiToolsTests
// Paid: Платный фейк-исполнитель (счётчик/ответы сценария).
private sealed record Context(
IAiTools Decorator,
FakeAiTools Paid);
IAiTools Paid);
// Собирает контекст: платный фейк + фейк лимитов и tenant-контекст (как регистрирует
// AddDealIntegrations при UseLocal=false).
@@ -143,7 +145,7 @@ public sealed class BudgetedAiToolsTests
configure?.Invoke(limits);
ITenantContext tenantContext = new TenantContext();
tenantContext.SetTenant(new TenantId(TenantIdValue));
var paid = new FakeAiTools();
var paid = TestAiTools.New();
IAiTools decorator = new BudgetedAiTools(
paid,
limits,