Почистить комментарии от упоминаний процесса
Удалены <remarks>, <summary> сжаты до короткой фразы, вырезаны ссылки на Task/Ruling/этап/python/прототип; //-комментарии со ссылками на процесс удалены; то же в .proto. Правила обновлены в docs/spec/Код-стайл-Дейл.md. Строк комментариев 27210 -> ~19100.
This commit is contained in:
@@ -7,11 +7,7 @@ using Microsoft.Extensions.DependencyInjection;
|
||||
namespace Deal.Ai.Tests.Ai;
|
||||
|
||||
/// <summary>
|
||||
/// In-proc gRPC-тесты AiService поверх фейк-провайдера (план Task 8, Acceptance): все 4 RPC
|
||||
/// (Filter/Classify/GenerateKeywords/EvaluateFit) с подменой LLM-фасада (без сети) через
|
||||
/// реальный хост (Kestrel HTTP/2, интерцептор service-token). Проверяются: разбор решений и
|
||||
/// usage в ответах, собранные сервисом промпты, недоступность провайдера → UNAVAILABLE с текстом
|
||||
/// 1:1 Ruling 5, ответ без JSON в Classify → ok=false (не ошибка), INVALID_ARGUMENT конфига.
|
||||
/// In-proc gRPC-тесты AiService поверх фейк-провайдера
|
||||
/// </summary>
|
||||
public sealed class AiRpcTests
|
||||
{
|
||||
@@ -21,7 +17,6 @@ public sealed class AiRpcTests
|
||||
// Usage API-ответа сценариев (проверка проброса в reply).
|
||||
private static readonly ProviderUsage SampleUsage = new(11, 5, 16);
|
||||
|
||||
// Текст ошибки UNAVAILABLE 1:1 Ruling 5 / ai.py L115–117.
|
||||
private const string UnavailableDetail =
|
||||
"ИИ (DeepSeek) не ответил корректно — повторите попытку через несколько секунд";
|
||||
|
||||
@@ -49,7 +44,6 @@ public sealed class AiRpcTests
|
||||
Assert.Equal("похоже на заявку", reply.Reason);
|
||||
AssertUsage(reply.Usage, SampleUsage);
|
||||
|
||||
// Сервис передаёт промпт system-сообщением и оборачивает текст как ai.py L193.
|
||||
FakeProviderCall call = Assert.Single(fake.Calls);
|
||||
Assert.Equal("Фильтр: {domain}", call.SystemPrompt);
|
||||
Assert.Equal("Сообщение:\nИщем разработчика на проект", call.UserText);
|
||||
@@ -77,7 +71,7 @@ public sealed class AiRpcTests
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Filter: модель не вернула pass — по умолчанию пропуск (1:1 ai.py L195: bool(get(pass, true))).
|
||||
/// Filter: модель не вернула pass — по умолчанию пропуск
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task Filter_MissingPassField_DefaultsToPass()
|
||||
@@ -97,8 +91,7 @@ public sealed class AiRpcTests
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Filter: провайдер недоступен после ретраев (3 попытки) → UNAVAILABLE с текстом 1:1 Ruling 5
|
||||
/// (ядро трактует как «ИИ недоступен» и пропускает сообщение локальным путём).
|
||||
/// Filter: провайдер недоступен после ретраев
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task Filter_ProviderUnavailable_ThrowsUnavailableWithDetail()
|
||||
@@ -120,7 +113,7 @@ public sealed class AiRpcTests
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Filter: ответ модели без JSON после ретраев — тоже UNAVAILABLE (у метода нет ok-поля).
|
||||
/// Filter: ответ модели без JSON после ретраев — тоже UNAVAILABLE
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task Filter_AnswerWithoutJson_ThrowsUnavailable()
|
||||
@@ -141,8 +134,7 @@ public sealed class AiRpcTests
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Classify: модель вернула JSON — ok=true, json = извлечённый ответ строкой (маппинг в ядре),
|
||||
/// usage пробрасывается.
|
||||
/// Classify: модель вернула JSON — ok=true, json = извлечённый ответ строкой
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task Classify_ModelAnsweredJson_ReturnsOkAndJson()
|
||||
@@ -178,8 +170,7 @@ public sealed class AiRpcTests
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Classify: модель отвечала, но без разбираемого JSON после ретраев → ok=false, НЕ RPC-ошибка
|
||||
/// (README ai.proto L201–204); usage последней попытки в ответе (оценка по символам).
|
||||
/// Classify: модель отвечала, но без разбираемого JSON после ретраев → ok=false, НЕ RPC-ошибка; usage последней попытки в ответе
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task Classify_AnswerWithoutJson_ReturnsOkFalseWithUsage()
|
||||
@@ -211,7 +202,7 @@ public sealed class AiRpcTests
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Classify: провайдер недоступен — UNAVAILABLE (ядро падает в локальный разбор, aiFail).
|
||||
/// Classify: провайдер недоступен — UNAVAILABLE
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task Classify_ProviderUnavailable_ThrowsUnavailable()
|
||||
@@ -232,7 +223,7 @@ public sealed class AiRpcTests
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// GenerateKeywords: ключи из JSON-ответа + фиксированный промпт с описанием задачи.
|
||||
/// GenerateKeywords
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task GenerateKeywords_ModelReturnedKeywords_ReturnsList()
|
||||
@@ -253,7 +244,6 @@ public sealed class AiRpcTests
|
||||
Assert.Equal(["стройка", "ремонт квартир", "подряды"], reply.Keywords);
|
||||
AssertUsage(reply.Usage, SampleUsage);
|
||||
|
||||
// Фиксированный промпт (routes L36–47) и пользовательское сообщение с описанием.
|
||||
FakeProviderCall call = Assert.Single(fake.Calls);
|
||||
Assert.Contains("эксперт по поиску Telegram-каналов", call.SystemPrompt, StringComparison.Ordinal);
|
||||
Assert.Contains("Верни строго JSON", call.SystemPrompt, StringComparison.Ordinal);
|
||||
@@ -262,7 +252,7 @@ public sealed class AiRpcTests
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// GenerateKeywords: не-строковые элементы списка пропускаются (чистку делает ядро).
|
||||
/// GenerateKeywords
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task GenerateKeywords_NonStringItems_Skipped()
|
||||
@@ -281,7 +271,7 @@ public sealed class AiRpcTests
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// GenerateKeywords: модель не вернула ключи — пустой список (не ошибка).
|
||||
/// GenerateKeywords
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task GenerateKeywords_NoKeywordsField_ReturnsEmpty()
|
||||
@@ -300,8 +290,7 @@ public sealed class AiRpcTests
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// EvaluateFit: fit=1 + причина; промпт собран сервисом из описания и ключей (discovery_eval
|
||||
/// L50–54), сообщение — как «Сообщение:\n…».
|
||||
/// EvaluateFit: fit=1 + причина; промпт собран сервисом из описания и ключей, сообщение — как «Сообщение:\n…».
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task EvaluateFit_ModelFits_ReturnsFitAndReason()
|
||||
@@ -332,7 +321,7 @@ public sealed class AiRpcTests
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// EvaluateFit: ключи задачи подставляются в промпт строкой через запятую (1:1 _ai_prompt).
|
||||
/// EvaluateFit: ключи задачи подставляются в промпт строкой через запятую.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task EvaluateFit_KeywordsJoinedIntoPrompt()
|
||||
@@ -358,8 +347,7 @@ public sealed class AiRpcTests
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// EvaluateFit: fit=0 без причины модели — причина по умолчанию «не подходит» (1:1 _ai_reason
|
||||
/// discovery_eval L167–171); строковое «нет» трактуется как ложь (1:1 _ai_fit L158–164).
|
||||
/// EvaluateFit: fit=0 без причины модели — причина по умолчанию «не подходит»; строковое «нет» трактуется как ложь.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task EvaluateFit_ModelNoFit_ReturnsDefaultReason()
|
||||
@@ -379,7 +367,7 @@ public sealed class AiRpcTests
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// EvaluateFit: fit строкой «нет» — false (паритет _ai_fit), причина из модели.
|
||||
/// EvaluateFit: fit строкой «нет» — false, причина из модели.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task EvaluateFit_StringFalsyFit_ReturnsFalse()
|
||||
@@ -399,7 +387,7 @@ public sealed class AiRpcTests
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// EvaluateFit: длинная причина модели усекается до 200 символов (1:1 _AI_REASON_LIMIT).
|
||||
/// EvaluateFit: длинная причина модели усекается до 200 символов.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task EvaluateFit_LongReason_IsTruncatedTo200()
|
||||
@@ -420,7 +408,7 @@ public sealed class AiRpcTests
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Конфиг-валидация: запрос без конфига провайдера (пустой base_url) → INVALID_ARGUMENT.
|
||||
/// Конфиг-валидация
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task Classify_WithoutProviderConfig_IsInvalidArgument()
|
||||
@@ -441,7 +429,7 @@ public sealed class AiRpcTests
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Конфиг-валидация: пустая model конфига → INVALID_ARGUMENT (вызов модели невозможен).
|
||||
/// Конфиг-валидация
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task GenerateKeywords_EmptyModel_IsInvalidArgument()
|
||||
@@ -466,7 +454,7 @@ public sealed class AiRpcTests
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Серверный лимит text (ai.proto Filter.text: core обрезает до 4000): превышение → INVALID_ARGUMENT.
|
||||
/// Серверный лимит text
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task Filter_TooLongText_IsInvalidArgument()
|
||||
@@ -492,7 +480,7 @@ public sealed class AiRpcTests
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Серверный лимит description (ai.proto GenerateKeywords.description: core обрезает до 4000).
|
||||
/// Серверный лимит description
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task GenerateKeywords_TooLongDescription_IsInvalidArgument()
|
||||
@@ -517,7 +505,7 @@ public sealed class AiRpcTests
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Обязательный tenant-id в metadata (Ruling 1): отсутствует → UNAUTHENTICATED до вызова.
|
||||
/// Обязательный tenant-id в metadata
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task Classify_WithoutTenantId_IsUnauthenticated()
|
||||
|
||||
Reference in New Issue
Block a user