Хелпер Support/TestAiTools: по умолчанию оба метода бросают NotSupportedException (семантика прежнего фейка — неожиданный вызов роняет тест), сценарии через ReturnsFit/ThrowsOnEvaluate, счётчик EvaluateFitCalls -> Received/DidNotReceive. Потребители (4 файла) перетипизированы на IAiTools, фейк удалён, тесты 1340.
This commit is contained in:
@@ -6,6 +6,7 @@ using Deal.Modules.Discovery.Application.Abstractions;
|
|||||||
using Deal.Modules.Discovery.Application.Models;
|
using Deal.Modules.Discovery.Application.Models;
|
||||||
using Deal.Modules.Discovery.Application.Registrars;
|
using Deal.Modules.Discovery.Application.Registrars;
|
||||||
using Deal.Modules.Settings.Application.Abstractions;
|
using Deal.Modules.Settings.Application.Abstractions;
|
||||||
|
using Deal.Tests.Unit.Support;
|
||||||
using Deal.Modules.Settings.Application.Models;
|
using Deal.Modules.Settings.Application.Models;
|
||||||
using Deal.Modules.Tenants.Application.Abstractions;
|
using Deal.Modules.Tenants.Application.Abstractions;
|
||||||
using Deal.Modules.Tenants.Application.Models;
|
using Deal.Modules.Tenants.Application.Models;
|
||||||
@@ -104,7 +105,7 @@ public sealed class DiscoveryWorkerSchedulerTests
|
|||||||
services.AddSingleton<ITenantContext>(tenantContext);
|
services.AddSingleton<ITenantContext>(tenantContext);
|
||||||
services.AddSingleton<ITenantRepository>(tenants);
|
services.AddSingleton<ITenantRepository>(tenants);
|
||||||
services.AddSingleton<IMlClient>(new FakeMlClient());
|
services.AddSingleton<IMlClient>(new FakeMlClient());
|
||||||
services.AddSingleton<IAiTools>(new FakeAiTools());
|
services.AddSingleton<IAiTools>(_ => TestAiTools.New());
|
||||||
// Тенант-scoped адаптеры: фейк выбирает хранилище по ITenantContext, который цикл заполняет SetTenant
|
// Тенант-scoped адаптеры: фейк выбирает хранилище по ITenantContext, который цикл заполняет SetTenant
|
||||||
// (эталон PipelineWorkerSchedulerTests/ConnectionStringProvider.ForTenant).
|
// (эталон PipelineWorkerSchedulerTests/ConnectionStringProvider.ForTenant).
|
||||||
services.AddScoped<IDiscoveryStore>(provider => TenantOf(provider) == TenantA ? storeA : storeB);
|
services.AddScoped<IDiscoveryStore>(provider => TenantOf(provider) == TenantA ? storeA : storeB);
|
||||||
|
|||||||
@@ -5,6 +5,8 @@ using Deal.Infrastructure.Integrations.Services;
|
|||||||
using Deal.Modules.Tenants.Application.Models;
|
using Deal.Modules.Tenants.Application.Models;
|
||||||
using Deal.SharedKernel.Tenants.Models;
|
using Deal.SharedKernel.Tenants.Models;
|
||||||
using Deal.Tests.Unit.Modules.Tenants;
|
using Deal.Tests.Unit.Modules.Tenants;
|
||||||
|
using Deal.Tests.Unit.Support;
|
||||||
|
using NSubstitute;
|
||||||
using Microsoft.Extensions.Logging.Abstractions;
|
using Microsoft.Extensions.Logging.Abstractions;
|
||||||
using Deal.Contracts.Integrations.Abstractions;
|
using Deal.Contracts.Integrations.Abstractions;
|
||||||
using Deal.SharedKernel.Tenants.Abstractions;
|
using Deal.SharedKernel.Tenants.Abstractions;
|
||||||
@@ -31,12 +33,12 @@ public sealed class BudgetedAiToolsTests
|
|||||||
Context ctx = Create(
|
Context ctx = Create(
|
||||||
limits => limits.Preload(
|
limits => limits.Preload(
|
||||||
TenantGuid, budgetTokens: 1000, TenantLimitPeriods.Month, PeriodStart, usedTokens: 1000));
|
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>(
|
AiUnavailableException exception = await Assert.ThrowsAsync<AiUnavailableException>(
|
||||||
() => ctx.Decorator.EvaluateFitAsync("текст", "описание", new[] { "ключ" }, CancellationToken.None));
|
() => 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);
|
Assert.Contains("исчерпан", exception.Message);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -49,7 +51,7 @@ public sealed class BudgetedAiToolsTests
|
|||||||
await Assert.ThrowsAsync<AiUnavailableException>(
|
await Assert.ThrowsAsync<AiUnavailableException>(
|
||||||
() => ctx.Decorator.EvaluateFitAsync("текст", "описание", Array.Empty<string>(), CancellationToken.None));
|
() => 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]
|
[Fact]
|
||||||
@@ -63,7 +65,7 @@ public sealed class BudgetedAiToolsTests
|
|||||||
AiUnavailableException exception = await Assert.ThrowsAsync<AiUnavailableException>(
|
AiUnavailableException exception = await Assert.ThrowsAsync<AiUnavailableException>(
|
||||||
() => ctx.Decorator.EvaluateFitAsync("текст", "описание", Array.Empty<string>(), CancellationToken.None));
|
() => 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);
|
Assert.Contains("приостановлен", exception.Message);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -72,13 +74,13 @@ public sealed class BudgetedAiToolsTests
|
|||||||
{
|
{
|
||||||
// Строки лимита нет — ленивый дефолт (10 000 000/месяц, расход 0) → Allowed=true → платный вызов.
|
// Строки лимита нет — ленивый дефолт (10 000 000/месяц, расход 0) → Allowed=true → платный вызов.
|
||||||
Context ctx = Create();
|
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(
|
AiEvaluateFitResultDto result = await ctx.Decorator.EvaluateFitAsync(
|
||||||
"текст", "описание", new[] { "ключ" }, CancellationToken.None);
|
"текст", "описание", new[] { "ключ" }, CancellationToken.None);
|
||||||
|
|
||||||
Assert.False(result.Fit);
|
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]
|
[Fact]
|
||||||
@@ -116,7 +118,7 @@ public sealed class BudgetedAiToolsTests
|
|||||||
public async Task GenerateKeywords_LargeBudget_DelegatesToPaid()
|
public async Task GenerateKeywords_LargeBudget_DelegatesToPaid()
|
||||||
{
|
{
|
||||||
// Строки лимита нет — ленивый дефолт → Allowed=true → декоратор делегирует платному исполнителю.
|
// Строки лимита нет — ленивый дефолт → Allowed=true → декоратор делегирует платному исполнителю.
|
||||||
// FakeAiTools.GenerateKeywordsAsync всегда бросает NotSupportedException (вызов не ждали) — само
|
// Подставка бросает NotSupportedException на GenerateKeywordsAsync (вызов не ждали) — само
|
||||||
// исключение здесь и есть признак делегирования (упало бы в тесте, если бы декоратор не позвал платный).
|
// исключение здесь и есть признак делегирования (упало бы в тесте, если бы декоратор не позвал платный).
|
||||||
Context ctx = Create();
|
Context ctx = Create();
|
||||||
|
|
||||||
@@ -131,7 +133,7 @@ public sealed class BudgetedAiToolsTests
|
|||||||
// Paid: Платный фейк-исполнитель (счётчик/ответы сценария).
|
// Paid: Платный фейк-исполнитель (счётчик/ответы сценария).
|
||||||
private sealed record Context(
|
private sealed record Context(
|
||||||
IAiTools Decorator,
|
IAiTools Decorator,
|
||||||
FakeAiTools Paid);
|
IAiTools Paid);
|
||||||
|
|
||||||
// Собирает контекст: платный фейк + фейк лимитов и tenant-контекст (как регистрирует
|
// Собирает контекст: платный фейк + фейк лимитов и tenant-контекст (как регистрирует
|
||||||
// AddDealIntegrations при UseLocal=false).
|
// AddDealIntegrations при UseLocal=false).
|
||||||
@@ -143,7 +145,7 @@ public sealed class BudgetedAiToolsTests
|
|||||||
configure?.Invoke(limits);
|
configure?.Invoke(limits);
|
||||||
ITenantContext tenantContext = new TenantContext();
|
ITenantContext tenantContext = new TenantContext();
|
||||||
tenantContext.SetTenant(new TenantId(TenantIdValue));
|
tenantContext.SetTenant(new TenantId(TenantIdValue));
|
||||||
var paid = new FakeAiTools();
|
var paid = TestAiTools.New();
|
||||||
IAiTools decorator = new BudgetedAiTools(
|
IAiTools decorator = new BudgetedAiTools(
|
||||||
paid,
|
paid,
|
||||||
limits,
|
limits,
|
||||||
|
|||||||
@@ -1,8 +1,11 @@
|
|||||||
|
using Deal.Contracts.Integrations.Abstractions;
|
||||||
using Deal.Contracts.Integrations.Models;
|
using Deal.Contracts.Integrations.Models;
|
||||||
using Deal.Modules.Discovery.Application.Models;
|
using Deal.Modules.Discovery.Application.Models;
|
||||||
using Deal.Modules.Discovery.Application.Services;
|
using Deal.Modules.Discovery.Application.Services;
|
||||||
using Deal.Modules.Settings.Application.Models;
|
using Deal.Modules.Settings.Application.Models;
|
||||||
using Deal.Tests.Unit.Modules.Settings;
|
using Deal.Tests.Unit.Modules.Settings;
|
||||||
|
using Deal.Tests.Unit.Support;
|
||||||
|
using NSubstitute;
|
||||||
|
|
||||||
namespace Deal.Tests.Unit.Contracts;
|
namespace Deal.Tests.Unit.Contracts;
|
||||||
|
|
||||||
@@ -43,8 +46,8 @@ public sealed class DiscoveryEvaluatorTests
|
|||||||
[Fact]
|
[Fact]
|
||||||
public async Task EvaluateMessage_AiFits_ReturnsAiVerdict()
|
public async Task EvaluateMessage_AiFits_ReturnsAiVerdict()
|
||||||
{
|
{
|
||||||
(DiscoveryEvaluator evaluator, _, FakeMlClient ml, FakeAiTools ai) = Create(aiEnabled: true, mlEnabled: false);
|
(DiscoveryEvaluator evaluator, _, FakeMlClient ml, IAiTools ai) = Create(aiEnabled: true, mlEnabled: false);
|
||||||
ai.Fit = new AiEvaluateFitResultDto(Fit: true, "тема совпадает");
|
ai.ReturnsFit(new AiEvaluateFitResultDto(Fit: true, "тема совпадает"));
|
||||||
|
|
||||||
DiscoveryMessageFit fit = await evaluator.EvaluateMessageAsync(
|
DiscoveryMessageFit fit = await evaluator.EvaluateMessageAsync(
|
||||||
TaskDto(keywords: ["заказ"]), "Нужен заказ на разработку CRM для агентства недвижимости", CancellationToken.None);
|
TaskDto(keywords: ["заказ"]), "Нужен заказ на разработку CRM для агентства недвижимости", CancellationToken.None);
|
||||||
@@ -52,14 +55,14 @@ public sealed class DiscoveryEvaluatorTests
|
|||||||
Assert.True(fit.Fit);
|
Assert.True(fit.Fit);
|
||||||
Assert.Equal("тема совпадает", fit.Reason);
|
Assert.Equal("тема совпадает", fit.Reason);
|
||||||
Assert.Equal("ai", fit.Source);
|
Assert.Equal("ai", fit.Source);
|
||||||
Assert.Equal(1, ai.EvaluateFitCalls);
|
await ai.Received(1).EvaluateFitAsync(Arg.Any<string>(), Arg.Any<string>(), Arg.Any<IReadOnlyCollection<string>>(), Arg.Any<CancellationToken>());
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task EvaluateMessage_AiFailure_FallsBackToHeuristic()
|
public async Task EvaluateMessage_AiFailure_FallsBackToHeuristic()
|
||||||
{
|
{
|
||||||
(DiscoveryEvaluator evaluator, _, _, FakeAiTools ai) = Create(aiEnabled: true, mlEnabled: false);
|
(DiscoveryEvaluator evaluator, _, _, IAiTools ai) = Create(aiEnabled: true, mlEnabled: false);
|
||||||
ai.Error = new InvalidOperationException("ai-service недоступен"); // как gRPC AiUnavailableException
|
ai.ThrowsOnEvaluate(new InvalidOperationException("ai-service недоступен")); // как gRPC AiUnavailableException
|
||||||
DiscoveryTaskDto task = TaskDto(keywords: ["заказ"]);
|
DiscoveryTaskDto task = TaskDto(keywords: ["заказ"]);
|
||||||
|
|
||||||
DiscoveryMessageFit fit = await evaluator.EvaluateMessageAsync(
|
DiscoveryMessageFit fit = await evaluator.EvaluateMessageAsync(
|
||||||
@@ -165,13 +168,13 @@ 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, FakeMlClient Ml, FakeAiTools Ai) Create(bool aiEnabled, bool mlEnabled)
|
private static (DiscoveryEvaluator Evaluator, FakeSettingsStore Settings, FakeMlClient Ml, IAiTools Ai) Create(bool aiEnabled, bool mlEnabled)
|
||||||
{
|
{
|
||||||
var settings = new FakeSettingsStore();
|
var settings = new FakeSettingsStore();
|
||||||
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 FakeMlClient();
|
var ml = new FakeMlClient();
|
||||||
var ai = new FakeAiTools();
|
var ai = TestAiTools.New();
|
||||||
return (new DiscoveryEvaluator(settings, ml, ai), settings, ml, ai);
|
return (new DiscoveryEvaluator(settings, ml, ai), settings, ml, ai);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ using Deal.Tests.Unit.Modules.Discovery;
|
|||||||
using Deal.Modules.Discovery.Application.Services;
|
using Deal.Modules.Discovery.Application.Services;
|
||||||
using Deal.Modules.Settings.Application.Models;
|
using Deal.Modules.Settings.Application.Models;
|
||||||
using Deal.Tests.Unit.Modules.Settings;
|
using Deal.Tests.Unit.Modules.Settings;
|
||||||
|
using Deal.Tests.Unit.Support;
|
||||||
using NSubstitute;
|
using NSubstitute;
|
||||||
using Grpc.Core;
|
using Grpc.Core;
|
||||||
|
|
||||||
@@ -432,7 +433,7 @@ public sealed class DiscoveryWorkerServiceTests
|
|||||||
settings.Preload(SettingsKeys.MlEnabled, "false");
|
settings.Preload(SettingsKeys.MlEnabled, "false");
|
||||||
settings.Preload(SettingsKeys.AiEnabled, "false");
|
settings.Preload(SettingsKeys.AiEnabled, "false");
|
||||||
var ml = new FakeMlClient();
|
var ml = new FakeMlClient();
|
||||||
var ai = new FakeAiTools();
|
var ai = TestAiTools.New();
|
||||||
var log = new DiscoveryLogService(store);
|
var log = new DiscoveryLogService(store);
|
||||||
var blacklist = new DiscoveryBlacklistService(store);
|
var blacklist = new DiscoveryBlacklistService(store);
|
||||||
var candidates = new DiscoveryCandidatesService(store, log, blacklist);
|
var candidates = new DiscoveryCandidatesService(store, log, blacklist);
|
||||||
|
|||||||
@@ -1,47 +0,0 @@
|
|||||||
using Deal.Contracts.Integrations.Abstractions;
|
|
||||||
using Deal.Contracts.Integrations.Models;
|
|
||||||
|
|
||||||
namespace Deal.Tests.Unit.Contracts;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Фейковая реализация <see cref="IAiTools"/> для тестов Discovery-воркера/оценки.
|
|
||||||
/// </summary>
|
|
||||||
public sealed class FakeAiTools : IAiTools
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Ответ EvaluateFitAsync; null — вызов бросит <see cref="NotSupportedException"/>
|
|
||||||
/// </summary>
|
|
||||||
public AiEvaluateFitResultDto? Fit { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Явный сбой EvaluateFitAsync
|
|
||||||
/// </summary>
|
|
||||||
public Exception? Error { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Сколько раз вызван EvaluateFitAsync
|
|
||||||
/// </summary>
|
|
||||||
public int EvaluateFitCalls { get; private set; }
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public Task<AiEvaluateFitResultDto> EvaluateFitAsync(
|
|
||||||
string text,
|
|
||||||
string description,
|
|
||||||
IReadOnlyCollection<string> keywords,
|
|
||||||
CancellationToken ct)
|
|
||||||
{
|
|
||||||
EvaluateFitCalls++;
|
|
||||||
if (Error is not null)
|
|
||||||
{
|
|
||||||
throw Error;
|
|
||||||
}
|
|
||||||
|
|
||||||
return Fit is null
|
|
||||||
? throw new NotSupportedException()
|
|
||||||
: Task.FromResult(Fit);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public Task<AiGenerateKeywordsResultDto> GenerateKeywordsAsync(string description, CancellationToken ct)
|
|
||||||
=> throw new NotSupportedException();
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
using Deal.Contracts.Integrations.Abstractions;
|
||||||
|
using Deal.Contracts.Integrations.Models;
|
||||||
|
using NSubstitute;
|
||||||
|
|
||||||
|
namespace Deal.Tests.Unit.Support;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Подставка <see cref="IAiTools"/> для сценариев Discovery/бюджетного гейта.
|
||||||
|
/// </summary>
|
||||||
|
public static class TestAiTools
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Создаёт подставку: по умолчанию оба метода бросают <see cref="NotSupportedException"/> —
|
||||||
|
/// тесты переопределяют только то, что ждут (как раньше делал фейк).
|
||||||
|
/// </summary>
|
||||||
|
public static IAiTools New() => Configure(Substitute.For<IAiTools>());
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Настраивает «запрещённые по умолчанию» методы подставки.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="ai">Подставка инструментов ИИ.</param>
|
||||||
|
public static IAiTools Configure(IAiTools ai)
|
||||||
|
{
|
||||||
|
ai.EvaluateFitAsync(
|
||||||
|
Arg.Any<string>(), Arg.Any<string>(), Arg.Any<IReadOnlyCollection<string>>(), Arg.Any<CancellationToken>())
|
||||||
|
.Returns<AiEvaluateFitResultDto>(_ => throw new NotSupportedException());
|
||||||
|
ai.GenerateKeywordsAsync(Arg.Any<string>(), Arg.Any<CancellationToken>())
|
||||||
|
.Returns<AiGenerateKeywordsResultDto>(_ => throw new NotSupportedException());
|
||||||
|
return ai;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Сценарий: <see cref="IAiTools.EvaluateFitAsync"/> возвращает заданный результат.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="ai">Подставка инструментов ИИ.</param>
|
||||||
|
/// <param name="fit">Результат оценки соответствия.</param>
|
||||||
|
public static void ReturnsFit(this IAiTools ai, AiEvaluateFitResultDto fit) =>
|
||||||
|
ai.EvaluateFitAsync(
|
||||||
|
Arg.Any<string>(), Arg.Any<string>(), Arg.Any<IReadOnlyCollection<string>>(), Arg.Any<CancellationToken>())
|
||||||
|
.Returns(fit);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Сценарий: <see cref="IAiTools.EvaluateFitAsync"/> бросает заданное исключение.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="ai">Подставка инструментов ИИ.</param>
|
||||||
|
/// <param name="error">Исключение для вызова.</param>
|
||||||
|
public static void ThrowsOnEvaluate(this IAiTools ai, Exception error) =>
|
||||||
|
ai.EvaluateFitAsync(
|
||||||
|
Arg.Any<string>(), Arg.Any<string>(), Arg.Any<IReadOnlyCollection<string>>(), Arg.Any<CancellationToken>())
|
||||||
|
.Returns<AiEvaluateFitResultDto>(_ => throw error);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user