ci / build-test (push) Successful in 2m52s
Хелпер Support/TestTokenUsageEventStore: записи + полная логика агрегации (фильтры, группировки day/tenant/provider/model) читают состояние на момент вызова. Потребители (10 файлов) перетипизированы на .Store, фейк удалён, тесты 1340 зелёные.
146 lines
7.1 KiB
C#
146 lines
7.1 KiB
C#
using Deal.Contracts.Integrations.Models;
|
|
using Deal.Grpc.Ai;
|
|
using Deal.Infrastructure.Data;
|
|
using Deal.Infrastructure.Integrations.Exceptions;
|
|
using Deal.Infrastructure.Integrations.Models;
|
|
using Deal.Infrastructure.Integrations.Options;
|
|
using Deal.Infrastructure.Integrations.Services;
|
|
using Deal.Modules.Settings.Application.Abstractions;
|
|
using Deal.Modules.Settings.Application.Models;
|
|
using Deal.Modules.Tenants.Application.Services;
|
|
using Deal.SharedKernel.Tenants.Models;
|
|
using Deal.Tests.Unit.Grpc;
|
|
using Deal.Tests.Unit.Modules.Settings;
|
|
using Deal.Tests.Unit.Modules.Tenants;
|
|
using Deal.Tests.Unit.Support;
|
|
using Microsoft.Extensions.Logging.Abstractions;
|
|
using Deal.Contracts.Integrations.Abstractions;
|
|
using Deal.SharedKernel.Tenants.Abstractions;
|
|
|
|
namespace Deal.Tests.Unit.Contracts;
|
|
|
|
/// <summary>
|
|
/// Тесты gRPC-адаптера IAiTools к ai-service
|
|
/// </summary>
|
|
[Collection("MlGrpcTests")]
|
|
public sealed class GrpcAiToolsTests
|
|
{
|
|
// Id тенанта сценариев строкой (формат N) — ожидаемое значение metadata tenant-id.
|
|
private const string TenantIdValue = "fedcba9876543210fedcba9876543210";
|
|
|
|
private static readonly Guid TenantGuid = Guid.Parse(TenantIdValue);
|
|
|
|
[Fact]
|
|
public async Task GenerateKeywordsAsync_MapsKeywordsAndSendsProviderConfigAndMetadata()
|
|
{
|
|
await AiGrpcTestHost.RunAsync(AiGrpcTestHost.DefaultToken, new RecordingAiService(), async (port, service) =>
|
|
{
|
|
var reply = new GenerateKeywordsReply { Usage = new Usage { Prompt = 300, Completion = 60, Total = 360 } };
|
|
reply.Keywords.Add("python разработка");
|
|
reply.Keywords.Add("telegram каналы");
|
|
service.GenerateKeywordsReply = reply;
|
|
FakeSettingsStore settings = new();
|
|
ISecretCipher cipher = TestCiphers.New();
|
|
FakeTenantLimitStore limits = new();
|
|
|
|
IAiTools tools = CreateTools(port, settings, cipher, limits);
|
|
AiGenerateKeywordsResultDto result = await tools.GenerateKeywordsAsync("Бэкенд-разработка на Python", CancellationToken.None);
|
|
|
|
Assert.True(result.Ok);
|
|
Assert.Equal(new[] { "python разработка", "telegram каналы" }, result.Keywords);
|
|
Assert.Null(result.Error);
|
|
|
|
Assert.NotNull(service.LastKeywords);
|
|
Assert.Equal("Бэкенд-разработка на Python", service.LastKeywords!.Description);
|
|
Assert.Equal("deepseek", service.LastKeywords.ProviderConfig.ProviderId);
|
|
Assert.Equal(TenantIdValue, Assert.Single(service.RequestTenantIds));
|
|
Assert.Equal(AiGrpcTestHost.DefaultToken, Assert.Single(service.RequestTokens));
|
|
Assert.Equal("{\"prompt\":300,\"completion\":60,\"total\":360}", settings.GetStoredJson(SettingsKeys.AiTokenUsage));
|
|
Assert.Equal(360, limits.UsedTokens(TenantGuid));
|
|
});
|
|
}
|
|
|
|
[Fact]
|
|
public async Task GenerateKeywordsAsync_Unavailable_ReturnsSoftErrorWithKeywordsEmpty()
|
|
{
|
|
await AiGrpcTestHost.RunAsync(AiGrpcTestHost.DefaultToken, new RecordingAiService(), async (port, service) =>
|
|
{
|
|
service.KeywordsUnavailable = true;
|
|
IAiTools tools = CreateTools(port, new FakeSettingsStore(), TestCiphers.New());
|
|
|
|
AiGenerateKeywordsResultDto result = await tools.GenerateKeywordsAsync("описание", CancellationToken.None);
|
|
|
|
Assert.False(result.Ok);
|
|
Assert.Empty(result.Keywords);
|
|
Assert.NotNull(result.Error);
|
|
Assert.Contains("не ответил корректно", result.Error);
|
|
});
|
|
}
|
|
|
|
[Fact]
|
|
public async Task EvaluateFitAsync_MapsReplyAndSendsTaskFields()
|
|
{
|
|
await AiGrpcTestHost.RunAsync(AiGrpcTestHost.DefaultToken, new RecordingAiService(), async (port, service) =>
|
|
{
|
|
service.EvaluateFitReply = new EvaluateFitReply
|
|
{
|
|
Fit = false,
|
|
Reason = "другая сфера",
|
|
Usage = new Usage { Prompt = 200, Completion = 10, Total = 210 },
|
|
};
|
|
FakeTenantLimitStore limits = new();
|
|
IAiTools tools = CreateTools(port, new FakeSettingsStore(), TestCiphers.New(), limits);
|
|
|
|
AiEvaluateFitResultDto result = await tools.EvaluateFitAsync(
|
|
"Ищу дизайнера для лендинга", "Разработка сайтов на Python", new[] { "python", "бэкенд" }, CancellationToken.None);
|
|
|
|
Assert.False(result.Fit);
|
|
Assert.Equal("другая сфера", result.Reason);
|
|
Assert.NotNull(service.LastFit);
|
|
Assert.Equal("Ищу дизайнера для лендинга", service.LastFit!.Text);
|
|
Assert.Equal("Разработка сайтов на Python", service.LastFit.Description);
|
|
Assert.Equal(new[] { "python", "бэкенд" }, service.LastFit.Keywords);
|
|
Assert.Equal("deepseek", service.LastFit.ProviderConfig.ProviderId);
|
|
Assert.Equal(TenantIdValue, Assert.Single(service.RequestTenantIds));
|
|
Assert.Equal(AiGrpcTestHost.DefaultToken, Assert.Single(service.RequestTokens));
|
|
Assert.Equal(210, limits.UsedTokens(TenantGuid));
|
|
});
|
|
}
|
|
|
|
[Fact]
|
|
public async Task EvaluateFitAsync_Unavailable_ThrowsAiUnavailable()
|
|
{
|
|
await AiGrpcTestHost.RunAsync(AiGrpcTestHost.DefaultToken, new RecordingAiService(), async (port, service) =>
|
|
{
|
|
service.FitUnavailable = true;
|
|
IAiTools tools = CreateTools(port, new FakeSettingsStore(), TestCiphers.New());
|
|
|
|
await Assert.ThrowsAsync<AiUnavailableException>(
|
|
() => tools.EvaluateFitAsync("текст", "описание", new[] { "ключ" }, CancellationToken.None));
|
|
});
|
|
}
|
|
|
|
// Создаёт GrpcAiTools к хосту-фейку на эфемерном порту в tenant-контексте теста.
|
|
// port: Порт хоста-фейка ai-service.
|
|
// settings: KV-хранилище тенанта (дефолты при пустом).
|
|
// cipher: Шифр секретов (расшифровка apiKey).
|
|
// limits: Фейк-хранилище лимитов (списание usage; при null — собственный экземпляр).
|
|
// Возвращает: Экземпляр GrpcAiTools.
|
|
private static GrpcAiTools CreateTools(
|
|
int port,
|
|
FakeSettingsStore settings,
|
|
ISecretCipher cipher,
|
|
FakeTenantLimitStore? limits = null)
|
|
{
|
|
ITenantContext tenantContext = new TenantContext();
|
|
tenantContext.SetTenant(new TenantId(TenantIdValue));
|
|
var connection = new AiGrpcConnection(new AiServiceOptions { UseLocal = false, Endpoint = $"http://127.0.0.1:{port}" });
|
|
return new GrpcAiTools(
|
|
tenantContext,
|
|
connection,
|
|
new AiProviderConfigBuilder(settings, cipher),
|
|
new TokenUsageRecorder(settings, limits ?? new FakeTenantLimitStore(), tenantContext, new TokenUsageEventService(new TestTokenUsageEventStore().Store)),
|
|
NullLogger<GrpcAiTools>.Instance);
|
|
}
|
|
}
|