SaaS-мониторинг Telegram: ядро (модули Cards/Kanban/Pipeline/Tenants/Settings/ Discovery, Api, Infrastructure), сервисы telegram/ai/ml/storage, фронт Vue, контракты и grpc-hosting, деплой-конфиги (dev/prod/observability/CI-раннер), Gitea Actions CI, документация (ТЗ, техдок, api-map, код-стайл, планы, бэклог). Текущее состояние: все этапы роадмапа 0–12 закрыты, сборка 5 sln 0/0, тесты 1340/130/52/38/9 зелёные.
This commit is contained in:
@@ -0,0 +1,144 @@
|
||||
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.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();
|
||||
FakeSecretCipher cipher = 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(), new FakeSecretCipher());
|
||||
|
||||
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(), new FakeSecretCipher(), 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(), new FakeSecretCipher());
|
||||
|
||||
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,
|
||||
FakeSecretCipher 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 FakeTokenUsageEventStore())),
|
||||
NullLogger<GrpcAiTools>.Instance);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user