diff --git a/src/ai-service/Deal.Ai.Tests/AiRpcTests.cs b/src/ai-service/Deal.Ai.Tests/Ai/AiRpcTests.cs similarity index 97% rename from src/ai-service/Deal.Ai.Tests/AiRpcTests.cs rename to src/ai-service/Deal.Ai.Tests/Ai/AiRpcTests.cs index 5082571..0e34a66 100644 --- a/src/ai-service/Deal.Ai.Tests/AiRpcTests.cs +++ b/src/ai-service/Deal.Ai.Tests/Ai/AiRpcTests.cs @@ -4,8 +4,9 @@ using Grpc.Core; using Grpc.Net.Client; using Microsoft.Extensions.DependencyInjection; using System.Text.Json.Nodes; +using Deal.Ai.Tests.Support; -namespace Deal.Ai.Tests; +namespace Deal.Ai.Tests.Ai; /// /// In-proc gRPC-тесты AiService поверх фейк-провайдера (план Task 8, Acceptance): все 4 RPC diff --git a/src/ai-service/Deal.Ai.Tests/AiTestHost.cs b/src/ai-service/Deal.Ai.Tests/Ai/AiTestHost.cs similarity index 96% rename from src/ai-service/Deal.Ai.Tests/AiTestHost.cs rename to src/ai-service/Deal.Ai.Tests/Ai/AiTestHost.cs index 00c00d3..dff36d3 100644 --- a/src/ai-service/Deal.Ai.Tests/AiTestHost.cs +++ b/src/ai-service/Deal.Ai.Tests/Ai/AiTestHost.cs @@ -7,8 +7,9 @@ using Microsoft.AspNetCore.Builder; using Microsoft.Extensions.DependencyInjection; using System.Net; using System.Net.Sockets; +using Deal.Ai.Tests.Support; -namespace Deal.Ai.Tests; +namespace Deal.Ai.Tests.Ai; // Общий харнесс in-proc gRPC-тестов ai-service (план Task 8): поднимает хост (AiServiceHost.Create) // в процессе теста на эфемерном порту и через configureServices-хук подменяет LLM-фасад фейком diff --git a/src/ai-service/Deal.Ai.Tests/FakeProviderClient.cs b/src/ai-service/Deal.Ai.Tests/Ai/FakeProviderClient.cs similarity index 95% rename from src/ai-service/Deal.Ai.Tests/FakeProviderClient.cs rename to src/ai-service/Deal.Ai.Tests/Ai/FakeProviderClient.cs index f218b42..5b8dee3 100644 --- a/src/ai-service/Deal.Ai.Tests/FakeProviderClient.cs +++ b/src/ai-service/Deal.Ai.Tests/Ai/FakeProviderClient.cs @@ -1,6 +1,7 @@ using Deal.Ai.Llm; +using Deal.Ai.Tests.Support; -namespace Deal.Ai.Tests; +namespace Deal.Ai.Tests.Ai; // Зафиксированный вызов фейк-провайдера (для проверки собранных RPC промптов). // SystemPrompt: Системный промпт вызова. diff --git a/src/ai-service/Deal.Ai.Tests/ProviderCallerTests.cs b/src/ai-service/Deal.Ai.Tests/Ai/ProviderCallerTests.cs similarity index 97% rename from src/ai-service/Deal.Ai.Tests/ProviderCallerTests.cs rename to src/ai-service/Deal.Ai.Tests/Ai/ProviderCallerTests.cs index 180d6ce..718379b 100644 --- a/src/ai-service/Deal.Ai.Tests/ProviderCallerTests.cs +++ b/src/ai-service/Deal.Ai.Tests/Ai/ProviderCallerTests.cs @@ -1,7 +1,8 @@ using Deal.Ai.Llm; using Microsoft.Extensions.Logging.Abstractions; +using Deal.Ai.Tests.Support; -namespace Deal.Ai.Tests; +namespace Deal.Ai.Tests.Ai; /// /// Unit-тесты оркестратора вызовов модели (план Task 7; ProviderCaller, 1:1 chat_json ai.py L80–117): diff --git a/src/ai-service/Deal.Ai.Tests/TokenEstimatorTests.cs b/src/ai-service/Deal.Ai.Tests/Ai/TokenEstimatorTests.cs similarity index 95% rename from src/ai-service/Deal.Ai.Tests/TokenEstimatorTests.cs rename to src/ai-service/Deal.Ai.Tests/Ai/TokenEstimatorTests.cs index 41b043a..7be3743 100644 --- a/src/ai-service/Deal.Ai.Tests/TokenEstimatorTests.cs +++ b/src/ai-service/Deal.Ai.Tests/Ai/TokenEstimatorTests.cs @@ -1,6 +1,7 @@ using Deal.Ai.Llm; +using Deal.Ai.Tests.Support; -namespace Deal.Ai.Tests; +namespace Deal.Ai.Tests.Ai; /// /// Unit-тесты оценки токенов (план Task 7; TokenEstimator, Ruling 5): usage API-ответа провайдера diff --git a/src/ai-service/Deal.Ai.Tests/AiServiceHostTests.cs b/src/ai-service/Deal.Ai.Tests/Grpc/AiServiceHostTests.cs similarity index 97% rename from src/ai-service/Deal.Ai.Tests/AiServiceHostTests.cs rename to src/ai-service/Deal.Ai.Tests/Grpc/AiServiceHostTests.cs index ceb63d5..c628090 100644 --- a/src/ai-service/Deal.Ai.Tests/AiServiceHostTests.cs +++ b/src/ai-service/Deal.Ai.Tests/Grpc/AiServiceHostTests.cs @@ -7,8 +7,9 @@ using Grpc.Net.Client; using System.Net; using System.Net.Sockets; using Xunit; +using Deal.Ai.Tests.Support; -namespace Deal.Ai.Tests; +namespace Deal.Ai.Tests.Grpc; /// /// Интеграционные тесты хоста ai-service (каркас Task 4 + логика Task 8). diff --git a/src/ai-service/Deal.Ai.Tests/JsonExtractorTests.cs b/src/ai-service/Deal.Ai.Tests/Support/JsonExtractorTests.cs similarity index 96% rename from src/ai-service/Deal.Ai.Tests/JsonExtractorTests.cs rename to src/ai-service/Deal.Ai.Tests/Support/JsonExtractorTests.cs index ad2abd6..1d726b5 100644 --- a/src/ai-service/Deal.Ai.Tests/JsonExtractorTests.cs +++ b/src/ai-service/Deal.Ai.Tests/Support/JsonExtractorTests.cs @@ -1,7 +1,7 @@ using System.Text.Json.Nodes; using Deal.Ai.Llm; -namespace Deal.Ai.Tests; +namespace Deal.Ai.Tests.Support; /// /// Unit-тесты извлечения JSON из ответа модели (план Task 7; JsonExtractor, 1:1 extract_json diff --git a/src/ai-service/Deal.Ai.Tests/LlmHttpClientTests.cs b/src/ai-service/Deal.Ai.Tests/Support/LlmHttpClientTests.cs similarity index 97% rename from src/ai-service/Deal.Ai.Tests/LlmHttpClientTests.cs rename to src/ai-service/Deal.Ai.Tests/Support/LlmHttpClientTests.cs index 540b2ba..db60f45 100644 --- a/src/ai-service/Deal.Ai.Tests/LlmHttpClientTests.cs +++ b/src/ai-service/Deal.Ai.Tests/Support/LlmHttpClientTests.cs @@ -2,7 +2,7 @@ using System.Net; using System.Text.Json.Nodes; using Deal.Ai.Llm; -namespace Deal.Ai.Tests; +namespace Deal.Ai.Tests.Support; /// /// HTTP-тесты LlmHttpClient на заглушке HttpMessageHandler (план Task 7 Acceptance; без сети): diff --git a/src/ai-service/Deal.Ai.Tests/StubHttpMessageHandler.cs b/src/ai-service/Deal.Ai.Tests/Support/StubHttpMessageHandler.cs similarity index 96% rename from src/ai-service/Deal.Ai.Tests/StubHttpMessageHandler.cs rename to src/ai-service/Deal.Ai.Tests/Support/StubHttpMessageHandler.cs index 26c9464..8e7c20b 100644 --- a/src/ai-service/Deal.Ai.Tests/StubHttpMessageHandler.cs +++ b/src/ai-service/Deal.Ai.Tests/Support/StubHttpMessageHandler.cs @@ -3,7 +3,7 @@ using System.Text; using System.Text.Json.Nodes; using Deal.Ai.Llm; -namespace Deal.Ai.Tests; +namespace Deal.Ai.Tests.Support; // Снимок HTTP-запроса, записанный заглушкой обработчика (для проверок wire-контракта). // Url: Полный URL запроса. diff --git a/src/core/tests/Deal.Tests.Unit/DataRetentionSchedulerTests.cs b/src/core/tests/Deal.Tests.Unit/Api/DataRetentionSchedulerTests.cs similarity index 98% rename from src/core/tests/Deal.Tests.Unit/DataRetentionSchedulerTests.cs rename to src/core/tests/Deal.Tests.Unit/Api/DataRetentionSchedulerTests.cs index 9d14574..fe9c2b6 100644 --- a/src/core/tests/Deal.Tests.Unit/DataRetentionSchedulerTests.cs +++ b/src/core/tests/Deal.Tests.Unit/Api/DataRetentionSchedulerTests.cs @@ -7,8 +7,10 @@ using Deal.Modules.Tenants.Application.Registrars; using Deal.Modules.Tenants.Application.Services; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging.Abstractions; +using Deal.Tests.Unit.Support; +using Deal.Tests.Unit.Modules.Tenants; -namespace Deal.Tests.Unit; +namespace Deal.Tests.Unit.Api; /// /// Тесты логики прохода (этап 12, пакет B): авто-очистка diff --git a/src/core/tests/Deal.Tests.Unit/DiscoveryEndpointsHelpersTests.cs b/src/core/tests/Deal.Tests.Unit/Api/DiscoveryEndpointsHelpersTests.cs similarity index 97% rename from src/core/tests/Deal.Tests.Unit/DiscoveryEndpointsHelpersTests.cs rename to src/core/tests/Deal.Tests.Unit/Api/DiscoveryEndpointsHelpersTests.cs index 566cbad..f743f52 100644 --- a/src/core/tests/Deal.Tests.Unit/DiscoveryEndpointsHelpersTests.cs +++ b/src/core/tests/Deal.Tests.Unit/Api/DiscoveryEndpointsHelpersTests.cs @@ -1,6 +1,7 @@ using Deal.Api.Endpoints; +using Deal.Tests.Unit.Support; -namespace Deal.Tests.Unit; +namespace Deal.Tests.Unit.Api; /// /// Тесты чистых хелперов эндпоинтов /api/discovery (план Task 19, Ruling 11): очистка ключей diff --git a/src/core/tests/Deal.Tests.Unit/DiscoveryWorkerSchedulerTests.cs b/src/core/tests/Deal.Tests.Unit/Api/DiscoveryWorkerSchedulerTests.cs similarity index 98% rename from src/core/tests/Deal.Tests.Unit/DiscoveryWorkerSchedulerTests.cs rename to src/core/tests/Deal.Tests.Unit/Api/DiscoveryWorkerSchedulerTests.cs index cd164e3..1389d1b 100644 --- a/src/core/tests/Deal.Tests.Unit/DiscoveryWorkerSchedulerTests.cs +++ b/src/core/tests/Deal.Tests.Unit/Api/DiscoveryWorkerSchedulerTests.cs @@ -21,8 +21,13 @@ using Deal.SharedKernel.Tenants.Abstractions; using Deal.SharedKernel.Tenants.Models; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; +using Deal.Tests.Unit.Support; +using Deal.Tests.Unit.Modules.Settings; +using Deal.Tests.Unit.Modules.Discovery; +using Deal.Tests.Unit.Contracts; +using Deal.Tests.Unit.Modules.Tenants; -namespace Deal.Tests.Unit; +namespace Deal.Tests.Unit.Api; /// /// Тесты DiscoveryWorkerScheduler — фоновый цикл воркера Discovery (план Task 18, Ruling 10; эталон diff --git a/src/core/tests/Deal.Tests.Unit/IngressRateLimitInterceptorTests.cs b/src/core/tests/Deal.Tests.Unit/Api/IngressRateLimitInterceptorTests.cs similarity index 98% rename from src/core/tests/Deal.Tests.Unit/IngressRateLimitInterceptorTests.cs rename to src/core/tests/Deal.Tests.Unit/Api/IngressRateLimitInterceptorTests.cs index ddb0213..eff2d48 100644 --- a/src/core/tests/Deal.Tests.Unit/IngressRateLimitInterceptorTests.cs +++ b/src/core/tests/Deal.Tests.Unit/Api/IngressRateLimitInterceptorTests.cs @@ -14,8 +14,11 @@ using Grpc.Core; using Grpc.Health.V1; using Grpc.Net.Client; using Microsoft.Extensions.DependencyInjection; +using Deal.Tests.Unit.Support; +using Deal.Tests.Unit.Modules.Tenants; +using Deal.Tests.Unit.Modules.Kanban; -namespace Deal.Tests.Unit; +namespace Deal.Tests.Unit.Api; /// /// In-proc gRPC-тесты IngressRateLimitInterceptor (план Task 11, Ruling 5): фиксированное окно diff --git a/src/core/tests/Deal.Tests.Unit/LoginAttemptGuardTests.cs b/src/core/tests/Deal.Tests.Unit/Api/LoginAttemptGuardTests.cs similarity index 98% rename from src/core/tests/Deal.Tests.Unit/LoginAttemptGuardTests.cs rename to src/core/tests/Deal.Tests.Unit/Api/LoginAttemptGuardTests.cs index 76e4ec6..0ee0e97 100644 --- a/src/core/tests/Deal.Tests.Unit/LoginAttemptGuardTests.cs +++ b/src/core/tests/Deal.Tests.Unit/Api/LoginAttemptGuardTests.cs @@ -2,8 +2,10 @@ using Deal.Api.Configuration; using Deal.Api.Extensions; using Deal.Api.Models; using Deal.Api.Services; +using Deal.Tests.Unit.Support; +using Deal.Tests.Unit.Modules.Tenants; -namespace Deal.Tests.Unit; +namespace Deal.Tests.Unit.Api; /// /// Unit-тесты LoginAttemptGuard (план Task 11, Ruling 5; этап 12, пакет B — хранилище Postgres): diff --git a/src/core/tests/Deal.Tests.Unit/MlOutboxFlushSchedulerTests.cs b/src/core/tests/Deal.Tests.Unit/Api/MlOutboxFlushSchedulerTests.cs similarity index 98% rename from src/core/tests/Deal.Tests.Unit/MlOutboxFlushSchedulerTests.cs rename to src/core/tests/Deal.Tests.Unit/Api/MlOutboxFlushSchedulerTests.cs index 4a63f9e..450b915 100644 --- a/src/core/tests/Deal.Tests.Unit/MlOutboxFlushSchedulerTests.cs +++ b/src/core/tests/Deal.Tests.Unit/Api/MlOutboxFlushSchedulerTests.cs @@ -26,8 +26,13 @@ using Deal.SharedKernel.Tenants.Abstractions; using Deal.SharedKernel.Tenants.Models; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging.Abstractions; +using Deal.Tests.Unit.Support; +using Deal.Tests.Unit.Modules.Kanban; +using Deal.Tests.Unit.Grpc; +using Deal.Tests.Unit.Modules.Tenants; +using Deal.Tests.Unit.Modules.Settings; -namespace Deal.Tests.Unit; +namespace Deal.Tests.Unit.Api; /// /// Тесты MlOutboxFlushScheduler — логика прохода цикла выгрузки обучения ML (план Task 16, Ruling 6; diff --git a/src/core/tests/Deal.Tests.Unit/OperatorAuditEndpointsHelpersTests.cs b/src/core/tests/Deal.Tests.Unit/Api/OperatorAuditEndpointsHelpersTests.cs similarity index 96% rename from src/core/tests/Deal.Tests.Unit/OperatorAuditEndpointsHelpersTests.cs rename to src/core/tests/Deal.Tests.Unit/Api/OperatorAuditEndpointsHelpersTests.cs index 776723f..53b6e2f 100644 --- a/src/core/tests/Deal.Tests.Unit/OperatorAuditEndpointsHelpersTests.cs +++ b/src/core/tests/Deal.Tests.Unit/Api/OperatorAuditEndpointsHelpersTests.cs @@ -4,8 +4,9 @@ using Deal.Modules.Tenants.Application.Extensions; using Deal.Modules.Tenants.Application.Models; using Deal.Modules.Tenants.Application.Registrars; using Deal.Modules.Tenants.Application.Services; +using Deal.Tests.Unit.Support; -namespace Deal.Tests.Unit; +namespace Deal.Tests.Unit.Api; /// /// Тесты хелпера фильтров эндпоинта GET /api/operator/audit (Task 4, Ruling 4): нормализация limit. diff --git a/src/core/tests/Deal.Tests.Unit/OperatorBootstrapHostedServiceTests.cs b/src/core/tests/Deal.Tests.Unit/Api/OperatorBootstrapHostedServiceTests.cs similarity index 98% rename from src/core/tests/Deal.Tests.Unit/OperatorBootstrapHostedServiceTests.cs rename to src/core/tests/Deal.Tests.Unit/Api/OperatorBootstrapHostedServiceTests.cs index ccebd4f..29c1dc6 100644 --- a/src/core/tests/Deal.Tests.Unit/OperatorBootstrapHostedServiceTests.cs +++ b/src/core/tests/Deal.Tests.Unit/Api/OperatorBootstrapHostedServiceTests.cs @@ -9,8 +9,10 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.FileProviders; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; +using Deal.Tests.Unit.Support; +using Deal.Tests.Unit.Modules.Tenants; -namespace Deal.Tests.Unit; +namespace Deal.Tests.Unit.Api; /// /// Тесты hosted-шага bootstrap оператора (Task 3, Ruling 1): env DEAL_OPERATOR_* → EnsureOperatorAsync diff --git a/src/core/tests/Deal.Tests.Unit/OperatorCookieOptionsTests.cs b/src/core/tests/Deal.Tests.Unit/Api/OperatorCookieOptionsTests.cs similarity index 96% rename from src/core/tests/Deal.Tests.Unit/OperatorCookieOptionsTests.cs rename to src/core/tests/Deal.Tests.Unit/Api/OperatorCookieOptionsTests.cs index ce7c564..3f21e96 100644 --- a/src/core/tests/Deal.Tests.Unit/OperatorCookieOptionsTests.cs +++ b/src/core/tests/Deal.Tests.Unit/Api/OperatorCookieOptionsTests.cs @@ -4,8 +4,9 @@ using Deal.Modules.Tenants.Application.Extensions; using Deal.Modules.Tenants.Application.Models; using Deal.Modules.Tenants.Application.Registrars; using Deal.Modules.Tenants.Application.Services; +using Deal.Tests.Unit.Support; -namespace Deal.Tests.Unit; +namespace Deal.Tests.Unit.Api; /// /// Юнит-тесты : имя операторской куки не пересекается с тенантной diff --git a/src/core/tests/Deal.Tests.Unit/OperatorLimitsEndpointsHelpersTests.cs b/src/core/tests/Deal.Tests.Unit/Api/OperatorLimitsEndpointsHelpersTests.cs similarity index 95% rename from src/core/tests/Deal.Tests.Unit/OperatorLimitsEndpointsHelpersTests.cs rename to src/core/tests/Deal.Tests.Unit/Api/OperatorLimitsEndpointsHelpersTests.cs index be1c7c1..c6bd5bc 100644 --- a/src/core/tests/Deal.Tests.Unit/OperatorLimitsEndpointsHelpersTests.cs +++ b/src/core/tests/Deal.Tests.Unit/Api/OperatorLimitsEndpointsHelpersTests.cs @@ -1,6 +1,7 @@ using Deal.Api.Endpoints; +using Deal.Tests.Unit.Support; -namespace Deal.Tests.Unit; +namespace Deal.Tests.Unit.Api; /// /// Тесты хелперов операторских ручек лимитов (план Task 10): процент расхода бюджета (0..100). diff --git a/src/core/tests/Deal.Tests.Unit/PipelinePumpGateTests.cs b/src/core/tests/Deal.Tests.Unit/Api/PipelinePumpGateTests.cs similarity index 97% rename from src/core/tests/Deal.Tests.Unit/PipelinePumpGateTests.cs rename to src/core/tests/Deal.Tests.Unit/Api/PipelinePumpGateTests.cs index 5798334..330e0a3 100644 --- a/src/core/tests/Deal.Tests.Unit/PipelinePumpGateTests.cs +++ b/src/core/tests/Deal.Tests.Unit/Api/PipelinePumpGateTests.cs @@ -1,8 +1,9 @@ using Deal.Api; using Deal.Api.Dtos; using Deal.Api.Services; +using Deal.Tests.Unit.Support; -namespace Deal.Tests.Unit; +namespace Deal.Tests.Unit.Api; /// /// Тесты PipelinePumpGate — общий воркер-гейт pump тенанта (план Task 11, Ruling 8; аналог diff --git a/src/core/tests/Deal.Tests.Unit/RuntimeDepthsCollectorTests.cs b/src/core/tests/Deal.Tests.Unit/Api/RuntimeDepthsCollectorTests.cs similarity index 97% rename from src/core/tests/Deal.Tests.Unit/RuntimeDepthsCollectorTests.cs rename to src/core/tests/Deal.Tests.Unit/Api/RuntimeDepthsCollectorTests.cs index 9def148..dd417af 100644 --- a/src/core/tests/Deal.Tests.Unit/RuntimeDepthsCollectorTests.cs +++ b/src/core/tests/Deal.Tests.Unit/Api/RuntimeDepthsCollectorTests.cs @@ -20,8 +20,12 @@ using Deal.SharedKernel.Tenants.Abstractions; using Deal.SharedKernel.Tenants.Models; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging.Abstractions; +using Deal.Tests.Unit.Support; +using Deal.Tests.Unit.Modules.Kanban; +using Deal.Tests.Unit.Modules.Tenants; +using Deal.Tests.Unit.Contracts; -namespace Deal.Tests.Unit; +namespace Deal.Tests.Unit.Api; /// /// Тесты сборщика глубин очередей/сессий (§10.2): агрегат по тенантам через существующие сервисы. diff --git a/src/core/tests/Deal.Tests.Unit/TelegramEndpointsMappingTests.cs b/src/core/tests/Deal.Tests.Unit/Api/TelegramEndpointsMappingTests.cs similarity index 97% rename from src/core/tests/Deal.Tests.Unit/TelegramEndpointsMappingTests.cs rename to src/core/tests/Deal.Tests.Unit/Api/TelegramEndpointsMappingTests.cs index 22b5be9..d8da4f5 100644 --- a/src/core/tests/Deal.Tests.Unit/TelegramEndpointsMappingTests.cs +++ b/src/core/tests/Deal.Tests.Unit/Api/TelegramEndpointsMappingTests.cs @@ -1,7 +1,8 @@ using Deal.Api.Endpoints; using Grpc.Core; +using Deal.Tests.Unit.Support; -namespace Deal.Tests.Unit; +namespace Deal.Tests.Unit.Api; /// /// Тесты чистых хелперов эндпоинтов /api/tg (план Task 14): RU-маппинг типа диалога на границе diff --git a/src/core/tests/Deal.Tests.Unit/TelegramKeysServiceTests.cs b/src/core/tests/Deal.Tests.Unit/Api/TelegramKeysServiceTests.cs similarity index 95% rename from src/core/tests/Deal.Tests.Unit/TelegramKeysServiceTests.cs rename to src/core/tests/Deal.Tests.Unit/Api/TelegramKeysServiceTests.cs index 7db4fde..a43034c 100644 --- a/src/core/tests/Deal.Tests.Unit/TelegramKeysServiceTests.cs +++ b/src/core/tests/Deal.Tests.Unit/Api/TelegramKeysServiceTests.cs @@ -3,8 +3,10 @@ using Deal.Modules.Settings.Application.Abstractions; using Deal.Modules.Settings.Application.Models; using Deal.Modules.Settings.Application.Registrars; using Deal.Modules.Settings.Application.Services; +using Deal.Tests.Unit.Support; +using Deal.Tests.Unit.Modules.Settings; -namespace Deal.Tests.Unit; +namespace Deal.Tests.Unit.Api; /// /// Тесты сервиса глобальных ключей Telegram (ТЗ §4.1/§8.1): шифрование/маскирование, валидация, diff --git a/src/core/tests/Deal.Tests.Unit/TgStatusServiceTests.cs b/src/core/tests/Deal.Tests.Unit/Api/TgStatusServiceTests.cs similarity index 98% rename from src/core/tests/Deal.Tests.Unit/TgStatusServiceTests.cs rename to src/core/tests/Deal.Tests.Unit/Api/TgStatusServiceTests.cs index 74c52b7..b1b33fd 100644 --- a/src/core/tests/Deal.Tests.Unit/TgStatusServiceTests.cs +++ b/src/core/tests/Deal.Tests.Unit/Api/TgStatusServiceTests.cs @@ -8,8 +8,11 @@ using Deal.Modules.Telegram.Application; using Deal.Modules.Telegram.Application.Models; using Grpc.Core; using Microsoft.Extensions.Logging.Abstractions; +using Deal.Tests.Unit.Support; +using Deal.Tests.Unit.Contracts; +using Deal.Tests.Unit.Modules.Settings; -namespace Deal.Tests.Unit; +namespace Deal.Tests.Unit.Api; /// /// Тесты сборки статуса вкладки Telegram — GET /api/tg/status (Ruling 8, api-map §4.9; план Task 14). diff --git a/src/core/tests/Deal.Tests.Unit/BudgetedAiClassifierTests.cs b/src/core/tests/Deal.Tests.Unit/Contracts/BudgetedAiClassifierTests.cs similarity index 98% rename from src/core/tests/Deal.Tests.Unit/BudgetedAiClassifierTests.cs rename to src/core/tests/Deal.Tests.Unit/Contracts/BudgetedAiClassifierTests.cs index 54f6a6d..b83ca30 100644 --- a/src/core/tests/Deal.Tests.Unit/BudgetedAiClassifierTests.cs +++ b/src/core/tests/Deal.Tests.Unit/Contracts/BudgetedAiClassifierTests.cs @@ -20,8 +20,11 @@ using Deal.Modules.Tenants.Application.Services; using Deal.SharedKernel.Tenants.Abstractions; using Deal.SharedKernel.Tenants.Models; using Microsoft.Extensions.Logging.Abstractions; +using Deal.Tests.Unit.Support; +using Deal.Tests.Unit.Modules.Tenants; +using Deal.Tests.Unit.Modules.Settings; -namespace Deal.Tests.Unit; +namespace Deal.Tests.Unit.Contracts; /// /// Тесты декоратора бюджетного гейта (план Task 9, Ruling 3): перед diff --git a/src/core/tests/Deal.Tests.Unit/BudgetedAiToolsTests.cs b/src/core/tests/Deal.Tests.Unit/Contracts/BudgetedAiToolsTests.cs similarity index 98% rename from src/core/tests/Deal.Tests.Unit/BudgetedAiToolsTests.cs rename to src/core/tests/Deal.Tests.Unit/Contracts/BudgetedAiToolsTests.cs index bb60003..2e55bee 100644 --- a/src/core/tests/Deal.Tests.Unit/BudgetedAiToolsTests.cs +++ b/src/core/tests/Deal.Tests.Unit/Contracts/BudgetedAiToolsTests.cs @@ -15,8 +15,10 @@ using Deal.Modules.Tenants.Application.Services; using Deal.SharedKernel.Tenants.Abstractions; using Deal.SharedKernel.Tenants.Models; using Microsoft.Extensions.Logging.Abstractions; +using Deal.Tests.Unit.Support; +using Deal.Tests.Unit.Modules.Tenants; -namespace Deal.Tests.Unit; +namespace Deal.Tests.Unit.Contracts; /// /// Тесты декоратора бюджетного гейта (план Task 9, Ruling 3): при запрете diff --git a/src/core/tests/Deal.Tests.Unit/CardComposerTests.cs b/src/core/tests/Deal.Tests.Unit/Contracts/CardComposerTests.cs similarity index 99% rename from src/core/tests/Deal.Tests.Unit/CardComposerTests.cs rename to src/core/tests/Deal.Tests.Unit/Contracts/CardComposerTests.cs index 6165301..2b61105 100644 --- a/src/core/tests/Deal.Tests.Unit/CardComposerTests.cs +++ b/src/core/tests/Deal.Tests.Unit/Contracts/CardComposerTests.cs @@ -12,8 +12,11 @@ using Deal.Modules.Settings.Application.Abstractions; using Deal.Modules.Settings.Application.Models; using Deal.Modules.Settings.Application.Registrars; using Deal.Modules.Settings.Application.Services; +using Deal.Tests.Unit.Support; +using Deal.Tests.Unit.Modules.Cards; +using Deal.Tests.Unit.Modules.Settings; -namespace Deal.Tests.Unit; +namespace Deal.Tests.Unit.Contracts; /// /// Тесты композитора карточки CardComposer (план Task 7 L403–406, Ruling 4; python _store_lead L433–514). diff --git a/src/core/tests/Deal.Tests.Unit/CardsServiceTests.cs b/src/core/tests/Deal.Tests.Unit/Contracts/CardsServiceTests.cs similarity index 99% rename from src/core/tests/Deal.Tests.Unit/CardsServiceTests.cs rename to src/core/tests/Deal.Tests.Unit/Contracts/CardsServiceTests.cs index e12d2aa..e87bfb6 100644 --- a/src/core/tests/Deal.Tests.Unit/CardsServiceTests.cs +++ b/src/core/tests/Deal.Tests.Unit/Contracts/CardsServiceTests.cs @@ -5,8 +5,11 @@ using Deal.Modules.Kanban.Application.Models; using Deal.Modules.Kanban.Application.Registrars; using Deal.Modules.Kanban.Application.Services; using Deal.Modules.Kanban.Application.ColumnRules; +using Deal.Tests.Unit.Support; +using Deal.Tests.Unit.Modules.Cards; +using Deal.Tests.Unit.Modules.Settings; -namespace Deal.Tests.Unit; +namespace Deal.Tests.Unit.Contracts; /// /// Тесты CardsService — карточки: чтение, переносы, архив/корзина, комментарии, counts, поиск diff --git a/src/core/tests/Deal.Tests.Unit/DialogsServiceTests.cs b/src/core/tests/Deal.Tests.Unit/Contracts/DialogsServiceTests.cs similarity index 99% rename from src/core/tests/Deal.Tests.Unit/DialogsServiceTests.cs rename to src/core/tests/Deal.Tests.Unit/Contracts/DialogsServiceTests.cs index 94b3f11..1b29c79 100644 --- a/src/core/tests/Deal.Tests.Unit/DialogsServiceTests.cs +++ b/src/core/tests/Deal.Tests.Unit/Contracts/DialogsServiceTests.cs @@ -8,8 +8,10 @@ using Deal.Modules.Telegram.Application; using Deal.Modules.Telegram.Application.Models; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; +using Deal.Tests.Unit.Support; +using Deal.Tests.Unit.Modules.Settings; -namespace Deal.Tests.Unit; +namespace Deal.Tests.Unit.Contracts; /// /// Тесты DialogsService — каталог диалогов/каналов и зеркало мониторинга ядра (Ruling 7, план Task 13). diff --git a/src/core/tests/Deal.Tests.Unit/DiscoveryEvaluatorTests.cs b/src/core/tests/Deal.Tests.Unit/Contracts/DiscoveryEvaluatorTests.cs similarity index 98% rename from src/core/tests/Deal.Tests.Unit/DiscoveryEvaluatorTests.cs rename to src/core/tests/Deal.Tests.Unit/Contracts/DiscoveryEvaluatorTests.cs index ad04990..b0149e7 100644 --- a/src/core/tests/Deal.Tests.Unit/DiscoveryEvaluatorTests.cs +++ b/src/core/tests/Deal.Tests.Unit/Contracts/DiscoveryEvaluatorTests.cs @@ -9,8 +9,10 @@ using Deal.Modules.Settings.Application.Abstractions; using Deal.Modules.Settings.Application.Models; using Deal.Modules.Settings.Application.Registrars; using Deal.Modules.Settings.Application.Services; +using Deal.Tests.Unit.Support; +using Deal.Tests.Unit.Modules.Settings; -namespace Deal.Tests.Unit; +namespace Deal.Tests.Unit.Contracts; /// /// Тесты DiscoveryEvaluator — каскад оценки сообщения и агрегаты выборки (план Task 18, 1:1 diff --git a/src/core/tests/Deal.Tests.Unit/DiscoveryWorkerServiceTests.cs b/src/core/tests/Deal.Tests.Unit/Contracts/DiscoveryWorkerServiceTests.cs similarity index 99% rename from src/core/tests/Deal.Tests.Unit/DiscoveryWorkerServiceTests.cs rename to src/core/tests/Deal.Tests.Unit/Contracts/DiscoveryWorkerServiceTests.cs index 48ffad5..427832c 100644 --- a/src/core/tests/Deal.Tests.Unit/DiscoveryWorkerServiceTests.cs +++ b/src/core/tests/Deal.Tests.Unit/Contracts/DiscoveryWorkerServiceTests.cs @@ -10,8 +10,11 @@ using Deal.Modules.Settings.Application.Models; using Deal.Modules.Settings.Application.Registrars; using Deal.Modules.Settings.Application.Services; using Grpc.Core; +using Deal.Tests.Unit.Support; +using Deal.Tests.Unit.Modules.Discovery; +using Deal.Tests.Unit.Modules.Settings; -namespace Deal.Tests.Unit; +namespace Deal.Tests.Unit.Contracts; /// /// Тесты DiscoveryWorkerService — один шаг за тик (план Task 18, 1:1 discovery_worker.py целиком, diff --git a/src/core/tests/Deal.Tests.Unit/FakeAiClassifier.cs b/src/core/tests/Deal.Tests.Unit/Contracts/FakeAiClassifier.cs similarity index 98% rename from src/core/tests/Deal.Tests.Unit/FakeAiClassifier.cs rename to src/core/tests/Deal.Tests.Unit/Contracts/FakeAiClassifier.cs index 600a8d8..591991a 100644 --- a/src/core/tests/Deal.Tests.Unit/FakeAiClassifier.cs +++ b/src/core/tests/Deal.Tests.Unit/Contracts/FakeAiClassifier.cs @@ -1,7 +1,8 @@ using Deal.Contracts.Integrations.Abstractions; using Deal.Contracts.Integrations.Models; +using Deal.Tests.Unit.Support; -namespace Deal.Tests.Unit; +namespace Deal.Tests.Unit.Contracts; /// /// In-memory реализация для unit-тестов воркера pump (план Task 8 L423: diff --git a/src/core/tests/Deal.Tests.Unit/FakeAiTools.cs b/src/core/tests/Deal.Tests.Unit/Contracts/FakeAiTools.cs similarity index 96% rename from src/core/tests/Deal.Tests.Unit/FakeAiTools.cs rename to src/core/tests/Deal.Tests.Unit/Contracts/FakeAiTools.cs index 43b4e2d..16bfc65 100644 --- a/src/core/tests/Deal.Tests.Unit/FakeAiTools.cs +++ b/src/core/tests/Deal.Tests.Unit/Contracts/FakeAiTools.cs @@ -1,7 +1,8 @@ using Deal.Contracts.Integrations.Abstractions; using Deal.Contracts.Integrations.Models; +using Deal.Tests.Unit.Support; -namespace Deal.Tests.Unit; +namespace Deal.Tests.Unit.Contracts; /// /// Фейковая реализация для тестов Discovery-воркера/оценки (план Task 18). diff --git a/src/core/tests/Deal.Tests.Unit/FakeDiscoveryGateway.cs b/src/core/tests/Deal.Tests.Unit/Contracts/FakeDiscoveryGateway.cs similarity index 99% rename from src/core/tests/Deal.Tests.Unit/FakeDiscoveryGateway.cs rename to src/core/tests/Deal.Tests.Unit/Contracts/FakeDiscoveryGateway.cs index 2810ae9..492e7f5 100644 --- a/src/core/tests/Deal.Tests.Unit/FakeDiscoveryGateway.cs +++ b/src/core/tests/Deal.Tests.Unit/Contracts/FakeDiscoveryGateway.cs @@ -1,7 +1,8 @@ using Deal.Contracts.Integrations.Abstractions; using Deal.Contracts.Integrations.Models; +using Deal.Tests.Unit.Support; -namespace Deal.Tests.Unit; +namespace Deal.Tests.Unit.Contracts; /// /// Фейковый для тестов Discovery-воркера (план Task 18): поиск/инфо/чтение diff --git a/src/core/tests/Deal.Tests.Unit/FakeFileStorage.cs b/src/core/tests/Deal.Tests.Unit/Contracts/FakeFileStorage.cs similarity index 98% rename from src/core/tests/Deal.Tests.Unit/FakeFileStorage.cs rename to src/core/tests/Deal.Tests.Unit/Contracts/FakeFileStorage.cs index e515dd5..e833cb7 100644 --- a/src/core/tests/Deal.Tests.Unit/FakeFileStorage.cs +++ b/src/core/tests/Deal.Tests.Unit/Contracts/FakeFileStorage.cs @@ -1,7 +1,8 @@ using Deal.Contracts.Integrations.Abstractions; using Deal.Contracts.Integrations.Models; +using Deal.Tests.Unit.Support; -namespace Deal.Tests.Unit; +namespace Deal.Tests.Unit.Contracts; /// /// In-memory реализация для unit-тестов файлов карточки (CardsService.Files). diff --git a/src/core/tests/Deal.Tests.Unit/FakeMlClient.cs b/src/core/tests/Deal.Tests.Unit/Contracts/FakeMlClient.cs similarity index 98% rename from src/core/tests/Deal.Tests.Unit/FakeMlClient.cs rename to src/core/tests/Deal.Tests.Unit/Contracts/FakeMlClient.cs index 5836d4f..3313cdf 100644 --- a/src/core/tests/Deal.Tests.Unit/FakeMlClient.cs +++ b/src/core/tests/Deal.Tests.Unit/Contracts/FakeMlClient.cs @@ -1,7 +1,8 @@ using Deal.Contracts.Integrations.Abstractions; using Deal.Contracts.Integrations.Models; +using Deal.Tests.Unit.Support; -namespace Deal.Tests.Unit; +namespace Deal.Tests.Unit.Contracts; /// /// In-memory реализация для unit-тестов CardsService (план Task 7) и воркера pump diff --git a/src/core/tests/Deal.Tests.Unit/FakeTelegramGateway.cs b/src/core/tests/Deal.Tests.Unit/Contracts/FakeTelegramGateway.cs similarity index 99% rename from src/core/tests/Deal.Tests.Unit/FakeTelegramGateway.cs rename to src/core/tests/Deal.Tests.Unit/Contracts/FakeTelegramGateway.cs index 204e621..59f06e8 100644 --- a/src/core/tests/Deal.Tests.Unit/FakeTelegramGateway.cs +++ b/src/core/tests/Deal.Tests.Unit/Contracts/FakeTelegramGateway.cs @@ -1,8 +1,9 @@ using Deal.Contracts.Integrations.Abstractions; using Deal.Contracts.Integrations.Models; using Grpc.Core; +using Deal.Tests.Unit.Support; -namespace Deal.Tests.Unit; +namespace Deal.Tests.Unit.Contracts; /// /// In-memory реализация для юнит-тестов модуля Telegram/ингресса (Task 13/14). diff --git a/src/core/tests/Deal.Tests.Unit/FakeTelegramStore.cs b/src/core/tests/Deal.Tests.Unit/Contracts/FakeTelegramStore.cs similarity index 99% rename from src/core/tests/Deal.Tests.Unit/FakeTelegramStore.cs rename to src/core/tests/Deal.Tests.Unit/Contracts/FakeTelegramStore.cs index cf3797b..3fc5df1 100644 --- a/src/core/tests/Deal.Tests.Unit/FakeTelegramStore.cs +++ b/src/core/tests/Deal.Tests.Unit/Contracts/FakeTelegramStore.cs @@ -1,8 +1,9 @@ using Deal.Contracts.Integrations.Models; using Deal.Modules.Telegram.Application; using Deal.Modules.Telegram.Application.Models; +using Deal.Tests.Unit.Support; -namespace Deal.Tests.Unit; +namespace Deal.Tests.Unit.Contracts; /// /// In-memory реализация для юнит-тестов DialogsService/ингресса (Task 13). diff --git a/src/core/tests/Deal.Tests.Unit/GrpcAiToolsTests.cs b/src/core/tests/Deal.Tests.Unit/Contracts/GrpcAiToolsTests.cs similarity index 97% rename from src/core/tests/Deal.Tests.Unit/GrpcAiToolsTests.cs rename to src/core/tests/Deal.Tests.Unit/Contracts/GrpcAiToolsTests.cs index 5dd1849..55f84fe 100644 --- a/src/core/tests/Deal.Tests.Unit/GrpcAiToolsTests.cs +++ b/src/core/tests/Deal.Tests.Unit/Contracts/GrpcAiToolsTests.cs @@ -20,8 +20,12 @@ using Deal.Modules.Tenants.Application.Services; using Deal.SharedKernel.Tenants.Abstractions; using Deal.SharedKernel.Tenants.Models; using Microsoft.Extensions.Logging.Abstractions; +using Deal.Tests.Unit.Support; +using Deal.Tests.Unit.Modules.Settings; +using Deal.Tests.Unit.Modules.Tenants; +using Deal.Tests.Unit.Grpc; -namespace Deal.Tests.Unit; +namespace Deal.Tests.Unit.Contracts; /// /// Тесты gRPC-адаптера IAiTools к ai-service (Ruling 9, план Task 15): маппинг GenerateKeywords/ diff --git a/src/core/tests/Deal.Tests.Unit/GrpcMlClientTests.cs b/src/core/tests/Deal.Tests.Unit/Contracts/GrpcMlClientTests.cs similarity index 98% rename from src/core/tests/Deal.Tests.Unit/GrpcMlClientTests.cs rename to src/core/tests/Deal.Tests.Unit/Contracts/GrpcMlClientTests.cs index a0bea74..e969e16 100644 --- a/src/core/tests/Deal.Tests.Unit/GrpcMlClientTests.cs +++ b/src/core/tests/Deal.Tests.Unit/Contracts/GrpcMlClientTests.cs @@ -20,8 +20,13 @@ using Deal.Modules.Tenants.Application.Services; using Deal.SharedKernel.Tenants.Abstractions; using Deal.SharedKernel.Tenants.Models; using Microsoft.Extensions.Logging.Abstractions; +using Deal.Tests.Unit.Support; +using Deal.Tests.Unit.Modules.Settings; +using Deal.Tests.Unit.Modules.Kanban; +using Deal.Tests.Unit.Grpc; +using Deal.Tests.Unit.Modules.Tenants; -namespace Deal.Tests.Unit; +namespace Deal.Tests.Unit.Contracts; /// /// Тесты gRPC-адаптера IMlClient/IMlTrainClient к ml-service (план Task 16, Ruling 6; Acceptance L449). diff --git a/src/core/tests/Deal.Tests.Unit/GrpcTelegramClientTests.cs b/src/core/tests/Deal.Tests.Unit/Contracts/GrpcTelegramClientTests.cs similarity index 99% rename from src/core/tests/Deal.Tests.Unit/GrpcTelegramClientTests.cs rename to src/core/tests/Deal.Tests.Unit/Contracts/GrpcTelegramClientTests.cs index 0a5bd2f..55d0e8b 100644 --- a/src/core/tests/Deal.Tests.Unit/GrpcTelegramClientTests.cs +++ b/src/core/tests/Deal.Tests.Unit/Contracts/GrpcTelegramClientTests.cs @@ -12,8 +12,10 @@ using Deal.SharedKernel.Tenants.Abstractions; using Deal.SharedKernel.Tenants.Models; using Grpc.Core; using Microsoft.Extensions.Logging.Abstractions; +using Deal.Tests.Unit.Support; +using Deal.Tests.Unit.Grpc; -namespace Deal.Tests.Unit; +namespace Deal.Tests.Unit.Contracts; /// /// In-proc тесты gRPC-адаптера гейта GrpcTelegramClient «по проводу» (план Task 14, Ruling 1/2/7). diff --git a/src/core/tests/Deal.Tests.Unit/IntegrationsDiTests.cs b/src/core/tests/Deal.Tests.Unit/Contracts/IntegrationsDiTests.cs similarity index 96% rename from src/core/tests/Deal.Tests.Unit/IntegrationsDiTests.cs rename to src/core/tests/Deal.Tests.Unit/Contracts/IntegrationsDiTests.cs index 3aea81c..81ac2d6 100644 --- a/src/core/tests/Deal.Tests.Unit/IntegrationsDiTests.cs +++ b/src/core/tests/Deal.Tests.Unit/Contracts/IntegrationsDiTests.cs @@ -30,8 +30,13 @@ using Deal.Modules.Tenants.Application.Services; using Deal.SharedKernel.Tenants.Abstractions; using Deal.SharedKernel.Tenants.Models; using Microsoft.Extensions.DependencyInjection; +using Deal.Tests.Unit.Support; +using Deal.Tests.Unit.Modules.Settings; +using Deal.Tests.Unit.Modules.Cards; +using Deal.Tests.Unit.Modules.Kanban; +using Deal.Tests.Unit.Modules.Tenants; -namespace Deal.Tests.Unit; +namespace Deal.Tests.Unit.Contracts; /// /// Тесты выбора реализации портов интеграций по флагам Services:Ml|Ai|Telegram:UseLocal (Ruling 6, планы Task diff --git a/src/core/tests/Deal.Tests.Unit/LocalAiClassifierTests.cs b/src/core/tests/Deal.Tests.Unit/Contracts/LocalAiClassifierTests.cs similarity index 98% rename from src/core/tests/Deal.Tests.Unit/LocalAiClassifierTests.cs rename to src/core/tests/Deal.Tests.Unit/Contracts/LocalAiClassifierTests.cs index cf94954..5509135 100644 --- a/src/core/tests/Deal.Tests.Unit/LocalAiClassifierTests.cs +++ b/src/core/tests/Deal.Tests.Unit/Contracts/LocalAiClassifierTests.cs @@ -11,8 +11,10 @@ using Deal.Modules.Settings.Application.Abstractions; using Deal.Modules.Settings.Application.Models; using Deal.Modules.Settings.Application.Registrars; using Deal.Modules.Settings.Application.Services; +using Deal.Tests.Unit.Support; +using Deal.Tests.Unit.Modules.Settings; -namespace Deal.Tests.Unit; +namespace Deal.Tests.Unit.Contracts; /// /// Тесты детерминированного ИИ-классификатора (Ruling 5, план Task 6 L382–384). diff --git a/src/core/tests/Deal.Tests.Unit/LocalAiToolsTests.cs b/src/core/tests/Deal.Tests.Unit/Contracts/LocalAiToolsTests.cs similarity index 96% rename from src/core/tests/Deal.Tests.Unit/LocalAiToolsTests.cs rename to src/core/tests/Deal.Tests.Unit/Contracts/LocalAiToolsTests.cs index ac4cb90..0108e81 100644 --- a/src/core/tests/Deal.Tests.Unit/LocalAiToolsTests.cs +++ b/src/core/tests/Deal.Tests.Unit/Contracts/LocalAiToolsTests.cs @@ -6,8 +6,9 @@ using Deal.Infrastructure.Integrations.Extensions; using Deal.Infrastructure.Integrations.Models; using Deal.Infrastructure.Integrations.Options; using Deal.Infrastructure.Integrations.Services; +using Deal.Tests.Unit.Support; -namespace Deal.Tests.Unit; +namespace Deal.Tests.Unit.Contracts; /// /// Тесты Local-реализации порта ИИ-инструментов (Ruling 9, план Task 15): diff --git a/src/core/tests/Deal.Tests.Unit/PipelineCardWriterTests.cs b/src/core/tests/Deal.Tests.Unit/Contracts/PipelineCardWriterTests.cs similarity index 97% rename from src/core/tests/Deal.Tests.Unit/PipelineCardWriterTests.cs rename to src/core/tests/Deal.Tests.Unit/Contracts/PipelineCardWriterTests.cs index 9b33a06..7dbdaba 100644 --- a/src/core/tests/Deal.Tests.Unit/PipelineCardWriterTests.cs +++ b/src/core/tests/Deal.Tests.Unit/Contracts/PipelineCardWriterTests.cs @@ -8,8 +8,12 @@ using Deal.Modules.Pipeline.Application.Abstractions; using Deal.Modules.Pipeline.Application.Models; using Deal.Modules.Pipeline.Application.Registrars; using Deal.Modules.Pipeline.Application.Services; +using Deal.Tests.Unit.Support; +using Deal.Tests.Unit.Modules.Cards; +using Deal.Tests.Unit.Modules.Kanban; +using Deal.Tests.Unit.Modules.Settings; -namespace Deal.Tests.Unit; +namespace Deal.Tests.Unit.Contracts; /// /// Тесты обёртки создания карточки PipelineCardWriter (план Task 7 L400–402, Ruling 3/4; diff --git a/src/core/tests/Deal.Tests.Unit/PipelineWorkerGrpcAiTests.cs b/src/core/tests/Deal.Tests.Unit/Contracts/PipelineWorkerGrpcAiTests.cs similarity index 98% rename from src/core/tests/Deal.Tests.Unit/PipelineWorkerGrpcAiTests.cs rename to src/core/tests/Deal.Tests.Unit/Contracts/PipelineWorkerGrpcAiTests.cs index 92720fb..99f8bd6 100644 --- a/src/core/tests/Deal.Tests.Unit/PipelineWorkerGrpcAiTests.cs +++ b/src/core/tests/Deal.Tests.Unit/Contracts/PipelineWorkerGrpcAiTests.cs @@ -30,8 +30,14 @@ using Deal.Modules.Tenants.Application.Services; using Deal.SharedKernel.Tenants.Abstractions; using Deal.SharedKernel.Tenants.Models; using Microsoft.Extensions.Logging.Abstractions; +using Deal.Tests.Unit.Support; +using Deal.Tests.Unit.Modules.Tenants; +using Deal.Tests.Unit.Modules.Kanban; +using Deal.Tests.Unit.Modules.Cards; +using Deal.Tests.Unit.Modules.Settings; +using Deal.Tests.Unit.Grpc; -namespace Deal.Tests.Unit; +namespace Deal.Tests.Unit.Contracts; /// /// Сквозной тест воркера pump с gRPC-классификатором ai-service (план Task 15, Acceptance L433–434: diff --git a/src/core/tests/Deal.Tests.Unit/TelegramGatewayPortTests.cs b/src/core/tests/Deal.Tests.Unit/Contracts/TelegramGatewayPortTests.cs similarity index 97% rename from src/core/tests/Deal.Tests.Unit/TelegramGatewayPortTests.cs rename to src/core/tests/Deal.Tests.Unit/Contracts/TelegramGatewayPortTests.cs index 1908971..674271b 100644 --- a/src/core/tests/Deal.Tests.Unit/TelegramGatewayPortTests.cs +++ b/src/core/tests/Deal.Tests.Unit/Contracts/TelegramGatewayPortTests.cs @@ -1,7 +1,8 @@ using Deal.Contracts.Integrations.Abstractions; using Deal.Contracts.Integrations.Models; +using Deal.Tests.Unit.Support; -namespace Deal.Tests.Unit; +namespace Deal.Tests.Unit.Contracts; /// /// Порт-контракт гейта telegram-service: набор методов 1:1 со списком Ruling 7 (план Task 13). diff --git a/src/core/tests/Deal.Tests.Unit/RecordingAiService.cs b/src/core/tests/Deal.Tests.Unit/Grpc/RecordingAiService.cs similarity index 99% rename from src/core/tests/Deal.Tests.Unit/RecordingAiService.cs rename to src/core/tests/Deal.Tests.Unit/Grpc/RecordingAiService.cs index 2fcda82..bf06d65 100644 --- a/src/core/tests/Deal.Tests.Unit/RecordingAiService.cs +++ b/src/core/tests/Deal.Tests.Unit/Grpc/RecordingAiService.cs @@ -1,7 +1,8 @@ using Deal.Grpc.Ai; using Grpc.Core; +using Deal.Tests.Unit.Support; -namespace Deal.Tests.Unit; +namespace Deal.Tests.Unit.Grpc; /// /// In-proc фейк ai-service для тестов gRPC-адаптеров ядра (план Task 15): Recording-сервер AiService. diff --git a/src/core/tests/Deal.Tests.Unit/RecordingMlService.cs b/src/core/tests/Deal.Tests.Unit/Grpc/RecordingMlService.cs similarity index 98% rename from src/core/tests/Deal.Tests.Unit/RecordingMlService.cs rename to src/core/tests/Deal.Tests.Unit/Grpc/RecordingMlService.cs index bdeeeb5..bca947f 100644 --- a/src/core/tests/Deal.Tests.Unit/RecordingMlService.cs +++ b/src/core/tests/Deal.Tests.Unit/Grpc/RecordingMlService.cs @@ -1,7 +1,8 @@ using Deal.Grpc.Ml; using Grpc.Core; +using Deal.Tests.Unit.Support; -namespace Deal.Tests.Unit; +namespace Deal.Tests.Unit.Grpc; /// /// In-proc фейк ml-service для тестов gRPC-клиента ядра (план Task 16): Recording-сервер MlService. diff --git a/src/core/tests/Deal.Tests.Unit/RecordingTelegramService.cs b/src/core/tests/Deal.Tests.Unit/Grpc/RecordingTelegramService.cs similarity index 98% rename from src/core/tests/Deal.Tests.Unit/RecordingTelegramService.cs rename to src/core/tests/Deal.Tests.Unit/Grpc/RecordingTelegramService.cs index 4aa1742..8d23993 100644 --- a/src/core/tests/Deal.Tests.Unit/RecordingTelegramService.cs +++ b/src/core/tests/Deal.Tests.Unit/Grpc/RecordingTelegramService.cs @@ -1,7 +1,8 @@ using Deal.Grpc.Telegram; using Grpc.Core; +using Deal.Tests.Unit.Support; -namespace Deal.Tests.Unit; +namespace Deal.Tests.Unit.Grpc; /// /// Фейк-сервер TelegramService (telegram.proto) для in-proc тестов GrpcTelegramClient (план Task 14). diff --git a/src/core/tests/Deal.Tests.Unit/AuditLogStoreTests.cs b/src/core/tests/Deal.Tests.Unit/Infrastructure/AuditLogStoreTests.cs similarity index 99% rename from src/core/tests/Deal.Tests.Unit/AuditLogStoreTests.cs rename to src/core/tests/Deal.Tests.Unit/Infrastructure/AuditLogStoreTests.cs index 930dba0..30c83ee 100644 --- a/src/core/tests/Deal.Tests.Unit/AuditLogStoreTests.cs +++ b/src/core/tests/Deal.Tests.Unit/Infrastructure/AuditLogStoreTests.cs @@ -7,8 +7,9 @@ using Deal.Modules.Tenants.Application.Models; using Deal.Modules.Tenants.Application.Registrars; using Deal.Modules.Tenants.Application.Services; using Microsoft.EntityFrameworkCore; +using Deal.Tests.Unit.Support; -namespace Deal.Tests.Unit; +namespace Deal.Tests.Unit.Infrastructure; /// /// Юнит-тесты EF-адаптера на InMemory-провайдере: маппинг DTO↔сущности, diff --git a/src/core/tests/Deal.Tests.Unit/AuthStoreTests.cs b/src/core/tests/Deal.Tests.Unit/Infrastructure/AuthStoreTests.cs similarity index 98% rename from src/core/tests/Deal.Tests.Unit/AuthStoreTests.cs rename to src/core/tests/Deal.Tests.Unit/Infrastructure/AuthStoreTests.cs index 491ea1f..a2a3739 100644 --- a/src/core/tests/Deal.Tests.Unit/AuthStoreTests.cs +++ b/src/core/tests/Deal.Tests.Unit/Infrastructure/AuthStoreTests.cs @@ -7,8 +7,9 @@ using Deal.Modules.Tenants.Application.Models; using Deal.Modules.Tenants.Application.Registrars; using Deal.Modules.Tenants.Application.Services; using Microsoft.EntityFrameworkCore; +using Deal.Tests.Unit.Support; -namespace Deal.Tests.Unit; +namespace Deal.Tests.Unit.Infrastructure; /// /// Юнит-тесты EF-адаптера на InMemory-провайдере: список пользователей тенанта diff --git a/src/core/tests/Deal.Tests.Unit/CardMoverTests.cs b/src/core/tests/Deal.Tests.Unit/Infrastructure/CardMoverTests.cs similarity index 95% rename from src/core/tests/Deal.Tests.Unit/CardMoverTests.cs rename to src/core/tests/Deal.Tests.Unit/Infrastructure/CardMoverTests.cs index 1db5aa9..55568a4 100644 --- a/src/core/tests/Deal.Tests.Unit/CardMoverTests.cs +++ b/src/core/tests/Deal.Tests.Unit/Infrastructure/CardMoverTests.cs @@ -7,8 +7,12 @@ using Deal.Modules.Kanban.Application.Extensions; using Deal.Modules.Kanban.Application.Models; using Deal.Modules.Kanban.Application.Registrars; using Deal.Modules.Kanban.Application.Services; +using Deal.Tests.Unit.Support; +using Deal.Tests.Unit.Modules.Cards; +using Deal.Tests.Unit.Modules.Settings; +using Deal.Tests.Unit.Contracts; -namespace Deal.Tests.Unit; +namespace Deal.Tests.Unit.Infrastructure; /// /// Тесты единой точки перехода карточки (R4 этапа 9): маршрутизация цели diff --git a/src/core/tests/Deal.Tests.Unit/GlobalSettingsStoreTests.cs b/src/core/tests/Deal.Tests.Unit/Infrastructure/GlobalSettingsStoreTests.cs similarity index 97% rename from src/core/tests/Deal.Tests.Unit/GlobalSettingsStoreTests.cs rename to src/core/tests/Deal.Tests.Unit/Infrastructure/GlobalSettingsStoreTests.cs index e70f4df..b42dda6 100644 --- a/src/core/tests/Deal.Tests.Unit/GlobalSettingsStoreTests.cs +++ b/src/core/tests/Deal.Tests.Unit/Infrastructure/GlobalSettingsStoreTests.cs @@ -6,8 +6,9 @@ using Deal.Modules.Settings.Application.Models; using Deal.Modules.Settings.Application.Registrars; using Deal.Modules.Settings.Application.Services; using Microsoft.EntityFrameworkCore; +using Deal.Tests.Unit.Support; -namespace Deal.Tests.Unit; +namespace Deal.Tests.Unit.Infrastructure; /// /// Юнит-тесты EF-адаптера на InMemory-провайдере (ТЗ §4.1/§8.1): diff --git a/src/core/tests/Deal.Tests.Unit/InviteStoreTests.cs b/src/core/tests/Deal.Tests.Unit/Infrastructure/InviteStoreTests.cs similarity index 98% rename from src/core/tests/Deal.Tests.Unit/InviteStoreTests.cs rename to src/core/tests/Deal.Tests.Unit/Infrastructure/InviteStoreTests.cs index 622efbd..19734e3 100644 --- a/src/core/tests/Deal.Tests.Unit/InviteStoreTests.cs +++ b/src/core/tests/Deal.Tests.Unit/Infrastructure/InviteStoreTests.cs @@ -7,8 +7,9 @@ using Deal.Modules.Tenants.Application.Models; using Deal.Modules.Tenants.Application.Registrars; using Deal.Modules.Tenants.Application.Services; using Microsoft.EntityFrameworkCore; +using Deal.Tests.Unit.Support; -namespace Deal.Tests.Unit; +namespace Deal.Tests.Unit.Infrastructure; /// /// Юнит-тесты EF-адаптера на InMemory-провайдере: маппинг DTO↔сущности и статусные операции. diff --git a/src/core/tests/Deal.Tests.Unit/MtlsOptionsTests.cs b/src/core/tests/Deal.Tests.Unit/Infrastructure/MtlsOptionsTests.cs similarity index 98% rename from src/core/tests/Deal.Tests.Unit/MtlsOptionsTests.cs rename to src/core/tests/Deal.Tests.Unit/Infrastructure/MtlsOptionsTests.cs index ae9ad83..163e92b 100644 --- a/src/core/tests/Deal.Tests.Unit/MtlsOptionsTests.cs +++ b/src/core/tests/Deal.Tests.Unit/Infrastructure/MtlsOptionsTests.cs @@ -5,8 +5,9 @@ using Deal.Infrastructure.Integrations.Models; using Deal.Infrastructure.Integrations.Options; using Deal.Infrastructure.Integrations.Services; using Microsoft.Extensions.Configuration; +using Deal.Tests.Unit.Support; -namespace Deal.Tests.Unit; +namespace Deal.Tests.Unit.Infrastructure; /// /// Тесты конфиг-парсинга mTLS (план Task 13, Ruling 6): env DEAL_MTLS_* — флаг и пути/пароли; diff --git a/src/core/tests/Deal.Tests.Unit/OperatorAuthStoreTests.cs b/src/core/tests/Deal.Tests.Unit/Infrastructure/OperatorAuthStoreTests.cs similarity index 98% rename from src/core/tests/Deal.Tests.Unit/OperatorAuthStoreTests.cs rename to src/core/tests/Deal.Tests.Unit/Infrastructure/OperatorAuthStoreTests.cs index 405feba..50f20cb 100644 --- a/src/core/tests/Deal.Tests.Unit/OperatorAuthStoreTests.cs +++ b/src/core/tests/Deal.Tests.Unit/Infrastructure/OperatorAuthStoreTests.cs @@ -3,8 +3,9 @@ using Deal.Infrastructure.Persistence.Configurations; using Deal.Infrastructure.Persistence.Repositories; using Deal.Modules.Tenants.Application.Models; using Microsoft.EntityFrameworkCore; +using Deal.Tests.Unit.Support; -namespace Deal.Tests.Unit; +namespace Deal.Tests.Unit.Infrastructure; /// /// Юнит-тесты EF-адаптера на InMemory-провайдере: маппинг DTO↔сущности (минимально). diff --git a/src/core/tests/Deal.Tests.Unit/RateLimitCounterStoreTests.cs b/src/core/tests/Deal.Tests.Unit/Infrastructure/RateLimitCounterStoreTests.cs similarity index 96% rename from src/core/tests/Deal.Tests.Unit/RateLimitCounterStoreTests.cs rename to src/core/tests/Deal.Tests.Unit/Infrastructure/RateLimitCounterStoreTests.cs index 5542553..fe5e8e4 100644 --- a/src/core/tests/Deal.Tests.Unit/RateLimitCounterStoreTests.cs +++ b/src/core/tests/Deal.Tests.Unit/Infrastructure/RateLimitCounterStoreTests.cs @@ -2,8 +2,9 @@ using Deal.Infrastructure.Persistence; using Deal.Infrastructure.Persistence.Configurations; using Deal.Infrastructure.Persistence.Repositories; using Microsoft.EntityFrameworkCore; +using Deal.Tests.Unit.Support; -namespace Deal.Tests.Unit; +namespace Deal.Tests.Unit.Infrastructure; /// /// Юнит-тесты EF-адаптера на InMemory-провайдере (этап 12, diff --git a/src/core/tests/Deal.Tests.Unit/SecretCipherTests.cs b/src/core/tests/Deal.Tests.Unit/Infrastructure/SecretCipherTests.cs similarity index 97% rename from src/core/tests/Deal.Tests.Unit/SecretCipherTests.cs rename to src/core/tests/Deal.Tests.Unit/Infrastructure/SecretCipherTests.cs index dd5af9c..90e06e9 100644 --- a/src/core/tests/Deal.Tests.Unit/SecretCipherTests.cs +++ b/src/core/tests/Deal.Tests.Unit/Infrastructure/SecretCipherTests.cs @@ -3,8 +3,9 @@ using Deal.Modules.Settings.Application.Abstractions; using Deal.Modules.Settings.Application.Models; using Deal.Modules.Settings.Application.Registrars; using Deal.Modules.Settings.Application.Services; +using Deal.Tests.Unit.Support; -namespace Deal.Tests.Unit; +namespace Deal.Tests.Unit.Infrastructure; /// /// Тесты AES-256-GCM-шифра секретов: roundtrip, nonce, устойчивость к повреждению (Task 1, Ruling 2). diff --git a/src/core/tests/Deal.Tests.Unit/TenantEntityTests.cs b/src/core/tests/Deal.Tests.Unit/Infrastructure/TenantEntityTests.cs similarity index 83% rename from src/core/tests/Deal.Tests.Unit/TenantEntityTests.cs rename to src/core/tests/Deal.Tests.Unit/Infrastructure/TenantEntityTests.cs index 59f25bf..4b03591 100644 --- a/src/core/tests/Deal.Tests.Unit/TenantEntityTests.cs +++ b/src/core/tests/Deal.Tests.Unit/Infrastructure/TenantEntityTests.cs @@ -1,6 +1,7 @@ using Deal.Infrastructure.Persistence.Entities; +using Deal.Tests.Unit.Support; -namespace Deal.Tests.Unit; +namespace Deal.Tests.Unit.Infrastructure; public sealed class TenantEntityTests { diff --git a/src/core/tests/Deal.Tests.Unit/TenantLimitStoreTests.cs b/src/core/tests/Deal.Tests.Unit/Infrastructure/TenantLimitStoreTests.cs similarity index 99% rename from src/core/tests/Deal.Tests.Unit/TenantLimitStoreTests.cs rename to src/core/tests/Deal.Tests.Unit/Infrastructure/TenantLimitStoreTests.cs index f71bf8d..d1c8aca 100644 --- a/src/core/tests/Deal.Tests.Unit/TenantLimitStoreTests.cs +++ b/src/core/tests/Deal.Tests.Unit/Infrastructure/TenantLimitStoreTests.cs @@ -8,8 +8,9 @@ using Deal.Modules.Tenants.Application.Models; using Deal.Modules.Tenants.Application.Registrars; using Deal.Modules.Tenants.Application.Services; using Microsoft.EntityFrameworkCore; +using Deal.Tests.Unit.Support; -namespace Deal.Tests.Unit; +namespace Deal.Tests.Unit.Infrastructure; /// /// Юнит-тесты EF-адаптера на InMemory-провайдере (Task 8, Ruling 3 этапа 7): diff --git a/src/core/tests/Deal.Tests.Unit/TenantRepositoryTests.cs b/src/core/tests/Deal.Tests.Unit/Infrastructure/TenantRepositoryTests.cs similarity index 97% rename from src/core/tests/Deal.Tests.Unit/TenantRepositoryTests.cs rename to src/core/tests/Deal.Tests.Unit/Infrastructure/TenantRepositoryTests.cs index db5dd17..cfc0a88 100644 --- a/src/core/tests/Deal.Tests.Unit/TenantRepositoryTests.cs +++ b/src/core/tests/Deal.Tests.Unit/Infrastructure/TenantRepositoryTests.cs @@ -7,8 +7,9 @@ using Deal.Modules.Tenants.Application.Models; using Deal.Modules.Tenants.Application.Registrars; using Deal.Modules.Tenants.Application.Services; using Microsoft.EntityFrameworkCore; +using Deal.Tests.Unit.Support; -namespace Deal.Tests.Unit; +namespace Deal.Tests.Unit.Infrastructure; /// /// Юнит-тесты EF-адаптера на InMemory-провайдере: смена статуса (Task 7). diff --git a/src/core/tests/Deal.Tests.Unit/TenantSchemaMigrationServiceTests.cs b/src/core/tests/Deal.Tests.Unit/Infrastructure/TenantSchemaMigrationServiceTests.cs similarity index 97% rename from src/core/tests/Deal.Tests.Unit/TenantSchemaMigrationServiceTests.cs rename to src/core/tests/Deal.Tests.Unit/Infrastructure/TenantSchemaMigrationServiceTests.cs index 9b2fb05..c36efd4 100644 --- a/src/core/tests/Deal.Tests.Unit/TenantSchemaMigrationServiceTests.cs +++ b/src/core/tests/Deal.Tests.Unit/Infrastructure/TenantSchemaMigrationServiceTests.cs @@ -5,8 +5,10 @@ using Deal.Modules.Tenants.Application.Models; using Deal.Modules.Tenants.Application.Registrars; using Deal.Modules.Tenants.Application.Services; using Microsoft.Extensions.Logging.Abstractions; +using Deal.Tests.Unit.Support; +using Deal.Tests.Unit.Modules.Tenants; -namespace Deal.Tests.Unit; +namespace Deal.Tests.Unit.Infrastructure; /// /// Тесты пакетной миграции схем тенантов (этап 12, пакет C): провижининг всех схем реестра, отказ одной diff --git a/src/core/tests/Deal.Tests.Unit/TenantSchemaMigratorTests.cs b/src/core/tests/Deal.Tests.Unit/Infrastructure/TenantSchemaMigratorTests.cs similarity index 89% rename from src/core/tests/Deal.Tests.Unit/TenantSchemaMigratorTests.cs rename to src/core/tests/Deal.Tests.Unit/Infrastructure/TenantSchemaMigratorTests.cs index da98dfd..f9cef86 100644 --- a/src/core/tests/Deal.Tests.Unit/TenantSchemaMigratorTests.cs +++ b/src/core/tests/Deal.Tests.Unit/Infrastructure/TenantSchemaMigratorTests.cs @@ -1,6 +1,7 @@ using Deal.Infrastructure.Migrations; +using Deal.Tests.Unit.Support; -namespace Deal.Tests.Unit; +namespace Deal.Tests.Unit.Infrastructure; public sealed class TenantSchemaMigratorTests { diff --git a/src/core/tests/Deal.Tests.Unit/TenantSettingEntityTests.cs b/src/core/tests/Deal.Tests.Unit/Infrastructure/TenantSettingEntityTests.cs similarity index 93% rename from src/core/tests/Deal.Tests.Unit/TenantSettingEntityTests.cs rename to src/core/tests/Deal.Tests.Unit/Infrastructure/TenantSettingEntityTests.cs index e5b94b0..040d1b9 100644 --- a/src/core/tests/Deal.Tests.Unit/TenantSettingEntityTests.cs +++ b/src/core/tests/Deal.Tests.Unit/Infrastructure/TenantSettingEntityTests.cs @@ -1,6 +1,7 @@ using Deal.Infrastructure.Persistence.Entities; +using Deal.Tests.Unit.Support; -namespace Deal.Tests.Unit; +namespace Deal.Tests.Unit.Infrastructure; /// /// Лёгкий тест сущности настройки тенанта (EF-адаптер SettingsStore работает с TenantSettingEntity). diff --git a/src/core/tests/Deal.Tests.Unit/CardsDomainTests.cs b/src/core/tests/Deal.Tests.Unit/Modules/Cards/CardsDomainTests.cs similarity index 98% rename from src/core/tests/Deal.Tests.Unit/CardsDomainTests.cs rename to src/core/tests/Deal.Tests.Unit/Modules/Cards/CardsDomainTests.cs index dcfa2ee..4c03a56 100644 --- a/src/core/tests/Deal.Tests.Unit/CardsDomainTests.cs +++ b/src/core/tests/Deal.Tests.Unit/Modules/Cards/CardsDomainTests.cs @@ -2,8 +2,9 @@ using Deal.Modules.Cards; using Deal.Modules.Cards.Application.Abstractions; using Deal.Modules.Cards.Application.Dtos; using Deal.Modules.Cards.Application.Models; +using Deal.Tests.Unit.Support; -namespace Deal.Tests.Unit; +namespace Deal.Tests.Unit.Modules.Cards; /// /// Тесты каркаса единой карточки (T1): маркер модуля, реестры id/контейнеров, агрегат Card. diff --git a/src/core/tests/Deal.Tests.Unit/FakeKanjStore.cs b/src/core/tests/Deal.Tests.Unit/Modules/Cards/FakeKanjStore.cs similarity index 99% rename from src/core/tests/Deal.Tests.Unit/FakeKanjStore.cs rename to src/core/tests/Deal.Tests.Unit/Modules/Cards/FakeKanjStore.cs index 0a0d17d..9bf2bb4 100644 --- a/src/core/tests/Deal.Tests.Unit/FakeKanjStore.cs +++ b/src/core/tests/Deal.Tests.Unit/Modules/Cards/FakeKanjStore.cs @@ -6,8 +6,9 @@ using Deal.Modules.Kanban.Application.Extensions; using Deal.Modules.Kanban.Application.Models; using Deal.Modules.Kanban.Application.Registrars; using Deal.Modules.Kanban.Application.Services; +using Deal.Tests.Unit.Support; -namespace Deal.Tests.Unit; +namespace Deal.Tests.Unit.Modules.Cards; /// /// In-memory реализация для unit-тестов сервисов единого домена карточки. diff --git a/src/core/tests/Deal.Tests.Unit/LocalSourceStub.cs b/src/core/tests/Deal.Tests.Unit/Modules/Cards/LocalSourceStub.cs similarity index 89% rename from src/core/tests/Deal.Tests.Unit/LocalSourceStub.cs rename to src/core/tests/Deal.Tests.Unit/Modules/Cards/LocalSourceStub.cs index 6f996b3..7a82592 100644 --- a/src/core/tests/Deal.Tests.Unit/LocalSourceStub.cs +++ b/src/core/tests/Deal.Tests.Unit/Modules/Cards/LocalSourceStub.cs @@ -1,8 +1,9 @@ using Deal.Modules.Cards.Application.Abstractions; using Deal.Modules.Cards.Application.Dtos; using Deal.Modules.Cards.Application.Models; +using Deal.Tests.Unit.Support; -namespace Deal.Tests.Unit; +namespace Deal.Tests.Unit.Modules.Cards; // Тестовая реализация локального источника (каркас T1; боевая — в T2/T3). internal sealed class LocalSourceStub(string authorId) : ILocalSource diff --git a/src/core/tests/Deal.Tests.Unit/TelegramSourceStub.cs b/src/core/tests/Deal.Tests.Unit/Modules/Cards/TelegramSourceStub.cs similarity index 93% rename from src/core/tests/Deal.Tests.Unit/TelegramSourceStub.cs rename to src/core/tests/Deal.Tests.Unit/Modules/Cards/TelegramSourceStub.cs index 0c3d3be..19937c6 100644 --- a/src/core/tests/Deal.Tests.Unit/TelegramSourceStub.cs +++ b/src/core/tests/Deal.Tests.Unit/Modules/Cards/TelegramSourceStub.cs @@ -1,8 +1,9 @@ using Deal.Modules.Cards.Application.Abstractions; using Deal.Modules.Cards.Application.Dtos; using Deal.Modules.Cards.Application.Models; +using Deal.Tests.Unit.Support; -namespace Deal.Tests.Unit; +namespace Deal.Tests.Unit.Modules.Cards; // Тестовая реализация Telegram-источника (каркас T1; боевая — в T2/T3). internal sealed class TelegramSourceStub(string dialogId, long messageId, string? peerHandle, string peerName, string? topicId) diff --git a/src/core/tests/Deal.Tests.Unit/DiscoveryBanGuardTests.cs b/src/core/tests/Deal.Tests.Unit/Modules/Discovery/DiscoveryBanGuardTests.cs similarity index 98% rename from src/core/tests/Deal.Tests.Unit/DiscoveryBanGuardTests.cs rename to src/core/tests/Deal.Tests.Unit/Modules/Discovery/DiscoveryBanGuardTests.cs index f310f12..458641a 100644 --- a/src/core/tests/Deal.Tests.Unit/DiscoveryBanGuardTests.cs +++ b/src/core/tests/Deal.Tests.Unit/Modules/Discovery/DiscoveryBanGuardTests.cs @@ -8,8 +8,10 @@ using Deal.Modules.Settings.Application.Abstractions; using Deal.Modules.Settings.Application.Models; using Deal.Modules.Settings.Application.Registrars; using Deal.Modules.Settings.Application.Services; +using Deal.Tests.Unit.Support; +using Deal.Tests.Unit.Modules.Settings; -namespace Deal.Tests.Unit; +namespace Deal.Tests.Unit.Modules.Discovery; /// /// Тесты DiscoveryBanGuard — суточный лимит авто-вступлений, flood-день, стоп-кран (план Task 18, diff --git a/src/core/tests/Deal.Tests.Unit/DiscoveryBlacklistServiceTests.cs b/src/core/tests/Deal.Tests.Unit/Modules/Discovery/DiscoveryBlacklistServiceTests.cs similarity index 98% rename from src/core/tests/Deal.Tests.Unit/DiscoveryBlacklistServiceTests.cs rename to src/core/tests/Deal.Tests.Unit/Modules/Discovery/DiscoveryBlacklistServiceTests.cs index 83a124a..e04b0cb 100644 --- a/src/core/tests/Deal.Tests.Unit/DiscoveryBlacklistServiceTests.cs +++ b/src/core/tests/Deal.Tests.Unit/Modules/Discovery/DiscoveryBlacklistServiceTests.cs @@ -4,8 +4,9 @@ using Deal.Modules.Discovery.Application.Extensions; using Deal.Modules.Discovery.Application.Models; using Deal.Modules.Discovery.Application.Registrars; using Deal.Modules.Discovery.Application.Services; +using Deal.Tests.Unit.Support; -namespace Deal.Tests.Unit; +namespace Deal.Tests.Unit.Modules.Discovery; /// /// Тесты DiscoveryBlacklistService — добавление с upsert-перезаписью, снятие, список (план Task 17, 1:1 L568–589). diff --git a/src/core/tests/Deal.Tests.Unit/DiscoveryCandidatesServiceTests.cs b/src/core/tests/Deal.Tests.Unit/Modules/Discovery/DiscoveryCandidatesServiceTests.cs similarity index 99% rename from src/core/tests/Deal.Tests.Unit/DiscoveryCandidatesServiceTests.cs rename to src/core/tests/Deal.Tests.Unit/Modules/Discovery/DiscoveryCandidatesServiceTests.cs index b3154cb..e5d08bd 100644 --- a/src/core/tests/Deal.Tests.Unit/DiscoveryCandidatesServiceTests.cs +++ b/src/core/tests/Deal.Tests.Unit/Modules/Discovery/DiscoveryCandidatesServiceTests.cs @@ -4,8 +4,9 @@ using Deal.Modules.Discovery.Application.Extensions; using Deal.Modules.Discovery.Application.Models; using Deal.Modules.Discovery.Application.Registrars; using Deal.Modules.Discovery.Application.Services; +using Deal.Tests.Unit.Support; -namespace Deal.Tests.Unit; +namespace Deal.Tests.Unit.Modules.Discovery; /// /// Тесты DiscoveryCandidatesService — add с исключениями, set_candidate, review, mark_joined/rejected (план Task 17, 1:1 discovery.py L385–563). diff --git a/src/core/tests/Deal.Tests.Unit/DiscoveryLangDetectorTests.cs b/src/core/tests/Deal.Tests.Unit/Modules/Discovery/DiscoveryLangDetectorTests.cs similarity index 95% rename from src/core/tests/Deal.Tests.Unit/DiscoveryLangDetectorTests.cs rename to src/core/tests/Deal.Tests.Unit/Modules/Discovery/DiscoveryLangDetectorTests.cs index 48d244b..ca13eca 100644 --- a/src/core/tests/Deal.Tests.Unit/DiscoveryLangDetectorTests.cs +++ b/src/core/tests/Deal.Tests.Unit/Modules/Discovery/DiscoveryLangDetectorTests.cs @@ -4,8 +4,9 @@ using Deal.Modules.Discovery.Application.Extensions; using Deal.Modules.Discovery.Application.Models; using Deal.Modules.Discovery.Application.Registrars; using Deal.Modules.Discovery.Application.Services; +using Deal.Tests.Unit.Support; -namespace Deal.Tests.Unit; +namespace Deal.Tests.Unit.Modules.Discovery; /// /// Тесты DiscoveryLangDetector — доля кириллицы в выборке (1:1 discovery_eval.detect_lang_ru L62–83, diff --git a/src/core/tests/Deal.Tests.Unit/DiscoveryLogServiceTests.cs b/src/core/tests/Deal.Tests.Unit/Modules/Discovery/DiscoveryLogServiceTests.cs similarity index 97% rename from src/core/tests/Deal.Tests.Unit/DiscoveryLogServiceTests.cs rename to src/core/tests/Deal.Tests.Unit/Modules/Discovery/DiscoveryLogServiceTests.cs index 9847593..b2ed9e6 100644 --- a/src/core/tests/Deal.Tests.Unit/DiscoveryLogServiceTests.cs +++ b/src/core/tests/Deal.Tests.Unit/Modules/Discovery/DiscoveryLogServiceTests.cs @@ -4,8 +4,9 @@ using Deal.Modules.Discovery.Application.Extensions; using Deal.Modules.Discovery.Application.Models; using Deal.Modules.Discovery.Application.Registrars; using Deal.Modules.Discovery.Application.Services; +using Deal.Tests.Unit.Support; -namespace Deal.Tests.Unit; +namespace Deal.Tests.Unit.Modules.Discovery; /// /// Тесты DiscoveryLogService — запись событий и чтение истории (план Task 17, 1:1 add_log/task_log L594–608). diff --git a/src/core/tests/Deal.Tests.Unit/DiscoverySearchErrorCounterTests.cs b/src/core/tests/Deal.Tests.Unit/Modules/Discovery/DiscoverySearchErrorCounterTests.cs similarity index 97% rename from src/core/tests/Deal.Tests.Unit/DiscoverySearchErrorCounterTests.cs rename to src/core/tests/Deal.Tests.Unit/Modules/Discovery/DiscoverySearchErrorCounterTests.cs index c7536f7..d793716 100644 --- a/src/core/tests/Deal.Tests.Unit/DiscoverySearchErrorCounterTests.cs +++ b/src/core/tests/Deal.Tests.Unit/Modules/Discovery/DiscoverySearchErrorCounterTests.cs @@ -4,8 +4,9 @@ using Deal.Modules.Discovery.Application.Extensions; using Deal.Modules.Discovery.Application.Models; using Deal.Modules.Discovery.Application.Registrars; using Deal.Modules.Discovery.Application.Services; +using Deal.Tests.Unit.Support; -namespace Deal.Tests.Unit; +namespace Deal.Tests.Unit.Modules.Discovery; /// /// Тесты — инкремент/сброс ошибок ключа поиска и TTL-эвикция diff --git a/src/core/tests/Deal.Tests.Unit/DiscoveryTasksServiceTests.cs b/src/core/tests/Deal.Tests.Unit/Modules/Discovery/DiscoveryTasksServiceTests.cs similarity index 99% rename from src/core/tests/Deal.Tests.Unit/DiscoveryTasksServiceTests.cs rename to src/core/tests/Deal.Tests.Unit/Modules/Discovery/DiscoveryTasksServiceTests.cs index b6cdf06..25310dc 100644 --- a/src/core/tests/Deal.Tests.Unit/DiscoveryTasksServiceTests.cs +++ b/src/core/tests/Deal.Tests.Unit/Modules/Discovery/DiscoveryTasksServiceTests.cs @@ -4,8 +4,10 @@ using Deal.Modules.Discovery.Application.Extensions; using Deal.Modules.Discovery.Application.Models; using Deal.Modules.Discovery.Application.Registrars; using Deal.Modules.Discovery.Application.Services; +using Deal.Tests.Unit.Support; +using Deal.Tests.Unit.Modules.Settings; -namespace Deal.Tests.Unit; +namespace Deal.Tests.Unit.Modules.Discovery; /// /// Тесты DiscoveryTasksService — создание/патч/удаление/start/pause и план-бюджет (план Task 17, 1:1 discovery.py L234–381). diff --git a/src/core/tests/Deal.Tests.Unit/FakeDiscoveryPacer.cs b/src/core/tests/Deal.Tests.Unit/Modules/Discovery/FakeDiscoveryPacer.cs similarity index 95% rename from src/core/tests/Deal.Tests.Unit/FakeDiscoveryPacer.cs rename to src/core/tests/Deal.Tests.Unit/Modules/Discovery/FakeDiscoveryPacer.cs index bc03fa9..18575db 100644 --- a/src/core/tests/Deal.Tests.Unit/FakeDiscoveryPacer.cs +++ b/src/core/tests/Deal.Tests.Unit/Modules/Discovery/FakeDiscoveryPacer.cs @@ -4,8 +4,9 @@ using Deal.Modules.Discovery.Application.Extensions; using Deal.Modules.Discovery.Application.Models; using Deal.Modules.Discovery.Application.Registrars; using Deal.Modules.Discovery.Application.Services; +using Deal.Tests.Unit.Support; -namespace Deal.Tests.Unit; +namespace Deal.Tests.Unit.Modules.Discovery; /// /// Фейковый для тестов воркера Discovery (план Task 18): паузы мгновенные, diff --git a/src/core/tests/Deal.Tests.Unit/FakeDiscoveryStore.cs b/src/core/tests/Deal.Tests.Unit/Modules/Discovery/FakeDiscoveryStore.cs similarity index 99% rename from src/core/tests/Deal.Tests.Unit/FakeDiscoveryStore.cs rename to src/core/tests/Deal.Tests.Unit/Modules/Discovery/FakeDiscoveryStore.cs index 7600ea4..d4aefe7 100644 --- a/src/core/tests/Deal.Tests.Unit/FakeDiscoveryStore.cs +++ b/src/core/tests/Deal.Tests.Unit/Modules/Discovery/FakeDiscoveryStore.cs @@ -4,8 +4,9 @@ using Deal.Modules.Discovery.Application.Extensions; using Deal.Modules.Discovery.Application.Models; using Deal.Modules.Discovery.Application.Registrars; using Deal.Modules.Discovery.Application.Services; +using Deal.Tests.Unit.Support; -namespace Deal.Tests.Unit; +namespace Deal.Tests.Unit.Modules.Discovery; /// /// In-memory реализация для unit-тестов Discovery (план Task 17). diff --git a/src/core/tests/Deal.Tests.Unit/AiClassifyContextBuilderTests.cs b/src/core/tests/Deal.Tests.Unit/Modules/Kanban/AiClassifyContextBuilderTests.cs similarity index 98% rename from src/core/tests/Deal.Tests.Unit/AiClassifyContextBuilderTests.cs rename to src/core/tests/Deal.Tests.Unit/Modules/Kanban/AiClassifyContextBuilderTests.cs index 64c5bce..078b683 100644 --- a/src/core/tests/Deal.Tests.Unit/AiClassifyContextBuilderTests.cs +++ b/src/core/tests/Deal.Tests.Unit/Modules/Kanban/AiClassifyContextBuilderTests.cs @@ -7,8 +7,11 @@ using Deal.Modules.Settings.Application.Abstractions; using Deal.Modules.Settings.Application.Models; using Deal.Modules.Settings.Application.Registrars; using Deal.Modules.Settings.Application.Services; +using Deal.Tests.Unit.Support; +using Deal.Tests.Unit.Modules.Cards; +using Deal.Tests.Unit.Modules.Settings; -namespace Deal.Tests.Unit; +namespace Deal.Tests.Unit.Modules.Kanban; /// /// Тесты контекст-билдера ИИ-классификации (план Task 15, Ruling 5; diff --git a/src/core/tests/Deal.Tests.Unit/AmountParserTests.cs b/src/core/tests/Deal.Tests.Unit/Modules/Kanban/AmountParserTests.cs similarity index 98% rename from src/core/tests/Deal.Tests.Unit/AmountParserTests.cs rename to src/core/tests/Deal.Tests.Unit/Modules/Kanban/AmountParserTests.cs index 90bbf19..1ca4327 100644 --- a/src/core/tests/Deal.Tests.Unit/AmountParserTests.cs +++ b/src/core/tests/Deal.Tests.Unit/Modules/Kanban/AmountParserTests.cs @@ -1,6 +1,7 @@ using Deal.Modules.Kanban.Application.ColumnRules; +using Deal.Tests.Unit.Support; -namespace Deal.Tests.Unit; +namespace Deal.Tests.Unit.Modules.Kanban; /// /// Тесты парсера сумм — AmountParser (план Task 3 L234–235, L244–246; rules.py extract_amounts L93–144). diff --git a/src/core/tests/Deal.Tests.Unit/BudgetNormalizerTests.cs b/src/core/tests/Deal.Tests.Unit/Modules/Kanban/BudgetNormalizerTests.cs similarity index 99% rename from src/core/tests/Deal.Tests.Unit/BudgetNormalizerTests.cs rename to src/core/tests/Deal.Tests.Unit/Modules/Kanban/BudgetNormalizerTests.cs index 8ab024e..f30aefe 100644 --- a/src/core/tests/Deal.Tests.Unit/BudgetNormalizerTests.cs +++ b/src/core/tests/Deal.Tests.Unit/Modules/Kanban/BudgetNormalizerTests.cs @@ -3,8 +3,9 @@ using Deal.Modules.Kanban.Application.Extensions; using Deal.Modules.Kanban.Application.Models; using Deal.Modules.Kanban.Application.Registrars; using Deal.Modules.Kanban.Application.Services; +using Deal.Tests.Unit.Support; -namespace Deal.Tests.Unit; +namespace Deal.Tests.Unit.Modules.Kanban; /// /// Тесты нормализации бюджета — BudgetNormalizer (план Task 3 L241–246, Ruling 7; ai.py clean_budget L316–326, diff --git a/src/core/tests/Deal.Tests.Unit/CardsServiceRemindersTests.cs b/src/core/tests/Deal.Tests.Unit/Modules/Kanban/CardsServiceRemindersTests.cs similarity index 98% rename from src/core/tests/Deal.Tests.Unit/CardsServiceRemindersTests.cs rename to src/core/tests/Deal.Tests.Unit/Modules/Kanban/CardsServiceRemindersTests.cs index b82bbf1..bed4507 100644 --- a/src/core/tests/Deal.Tests.Unit/CardsServiceRemindersTests.cs +++ b/src/core/tests/Deal.Tests.Unit/Modules/Kanban/CardsServiceRemindersTests.cs @@ -7,8 +7,12 @@ using Deal.Modules.Settings.Application.Abstractions; using Deal.Modules.Settings.Application.Models; using Deal.Modules.Settings.Application.Registrars; using Deal.Modules.Settings.Application.Services; +using Deal.Tests.Unit.Support; +using Deal.Tests.Unit.Modules.Cards; +using Deal.Tests.Unit.Modules.Settings; +using Deal.Tests.Unit.Contracts; -namespace Deal.Tests.Unit; +namespace Deal.Tests.Unit.Modules.Kanban; /// /// Тесты напоминаний «Отложено» — (единый домен карточки, этап 9): set diff --git a/src/core/tests/Deal.Tests.Unit/ColumnRulesTests.cs b/src/core/tests/Deal.Tests.Unit/Modules/Kanban/ColumnRulesTests.cs similarity index 99% rename from src/core/tests/Deal.Tests.Unit/ColumnRulesTests.cs rename to src/core/tests/Deal.Tests.Unit/Modules/Kanban/ColumnRulesTests.cs index c0a8e6b..6299d50 100644 --- a/src/core/tests/Deal.Tests.Unit/ColumnRulesTests.cs +++ b/src/core/tests/Deal.Tests.Unit/Modules/Kanban/ColumnRulesTests.cs @@ -1,7 +1,8 @@ using Deal.Modules.Kanban.Application.ColumnRules; using Deal.Modules.Kanban.Application.Models; +using Deal.Tests.Unit.Support; -namespace Deal.Tests.Unit; +namespace Deal.Tests.Unit.Modules.Kanban; /// /// Тесты чистых правил колонок — ColumnRules (план Task 3 L231–250, Ruling 2; rules.py match_text L176–209, diff --git a/src/core/tests/Deal.Tests.Unit/FakeMlLearningStore.cs b/src/core/tests/Deal.Tests.Unit/Modules/Kanban/FakeMlLearningStore.cs similarity index 98% rename from src/core/tests/Deal.Tests.Unit/FakeMlLearningStore.cs rename to src/core/tests/Deal.Tests.Unit/Modules/Kanban/FakeMlLearningStore.cs index 013522c..29db669 100644 --- a/src/core/tests/Deal.Tests.Unit/FakeMlLearningStore.cs +++ b/src/core/tests/Deal.Tests.Unit/Modules/Kanban/FakeMlLearningStore.cs @@ -3,8 +3,9 @@ using Deal.Modules.Kanban.Application.Extensions; using Deal.Modules.Kanban.Application.Models; using Deal.Modules.Kanban.Application.Registrars; using Deal.Modules.Kanban.Application.Services; +using Deal.Tests.Unit.Support; -namespace Deal.Tests.Unit; +namespace Deal.Tests.Unit.Modules.Kanban; /// /// In-memory реализация для unit-тестов ML-интеграции (Tasks 5/16). diff --git a/src/core/tests/Deal.Tests.Unit/FakePipelineStore.cs b/src/core/tests/Deal.Tests.Unit/Modules/Kanban/FakePipelineStore.cs similarity index 99% rename from src/core/tests/Deal.Tests.Unit/FakePipelineStore.cs rename to src/core/tests/Deal.Tests.Unit/Modules/Kanban/FakePipelineStore.cs index e565919..671a725 100644 --- a/src/core/tests/Deal.Tests.Unit/FakePipelineStore.cs +++ b/src/core/tests/Deal.Tests.Unit/Modules/Kanban/FakePipelineStore.cs @@ -7,8 +7,9 @@ using Deal.Modules.Pipeline.Application.Abstractions; using Deal.Modules.Pipeline.Application.Models; using Deal.Modules.Pipeline.Application.Registrars; using Deal.Modules.Pipeline.Application.Services; +using Deal.Tests.Unit.Support; -namespace Deal.Tests.Unit; +namespace Deal.Tests.Unit.Modules.Kanban; /// /// In-memory реализация для unit-тестов PipelineIngestService/PipelineProcessingService (план Task 5). diff --git a/src/core/tests/Deal.Tests.Unit/FileKindDetectorTests.cs b/src/core/tests/Deal.Tests.Unit/Modules/Kanban/FileKindDetectorTests.cs similarity index 98% rename from src/core/tests/Deal.Tests.Unit/FileKindDetectorTests.cs rename to src/core/tests/Deal.Tests.Unit/Modules/Kanban/FileKindDetectorTests.cs index b681b53..3a4c8f4 100644 --- a/src/core/tests/Deal.Tests.Unit/FileKindDetectorTests.cs +++ b/src/core/tests/Deal.Tests.Unit/Modules/Kanban/FileKindDetectorTests.cs @@ -3,8 +3,9 @@ using Deal.Modules.Kanban.Application.Extensions; using Deal.Modules.Kanban.Application.Models; using Deal.Modules.Kanban.Application.Registrars; using Deal.Modules.Kanban.Application.Services; +using Deal.Tests.Unit.Support; -namespace Deal.Tests.Unit; +namespace Deal.Tests.Unit.Modules.Kanban; /// /// Тесты FileKindDetector — категории вложений по MIME/расширению (план Task 6 L333–334; files.py KIND_BY_EXT/KIND_LABELS L13–28, detect L31–45; Ruling 4). diff --git a/src/core/tests/Deal.Tests.Unit/MarkerTests.cs b/src/core/tests/Deal.Tests.Unit/Modules/Kanban/MarkerTests.cs similarity index 87% rename from src/core/tests/Deal.Tests.Unit/MarkerTests.cs rename to src/core/tests/Deal.Tests.Unit/Modules/Kanban/MarkerTests.cs index ec7c42c..7c8736d 100644 --- a/src/core/tests/Deal.Tests.Unit/MarkerTests.cs +++ b/src/core/tests/Deal.Tests.Unit/Modules/Kanban/MarkerTests.cs @@ -1,7 +1,8 @@ using Deal.Modules.Kanban; using Deal.Modules.Pipeline; +using Deal.Tests.Unit.Support; -namespace Deal.Tests.Unit; +namespace Deal.Tests.Unit.Modules.Kanban; public sealed class MarkerTests { diff --git a/src/core/tests/Deal.Tests.Unit/MlReviewServiceTests.cs b/src/core/tests/Deal.Tests.Unit/Modules/Kanban/MlReviewServiceTests.cs similarity index 98% rename from src/core/tests/Deal.Tests.Unit/MlReviewServiceTests.cs rename to src/core/tests/Deal.Tests.Unit/Modules/Kanban/MlReviewServiceTests.cs index 767c88d..ffbca0f 100644 --- a/src/core/tests/Deal.Tests.Unit/MlReviewServiceTests.cs +++ b/src/core/tests/Deal.Tests.Unit/Modules/Kanban/MlReviewServiceTests.cs @@ -7,8 +7,12 @@ using Deal.Modules.Pipeline.Application.Abstractions; using Deal.Modules.Pipeline.Application.Models; using Deal.Modules.Pipeline.Application.Registrars; using Deal.Modules.Pipeline.Application.Services; +using Deal.Tests.Unit.Support; +using Deal.Tests.Unit.Modules.Cards; +using Deal.Tests.Unit.Contracts; +using Deal.Tests.Unit.Modules.Settings; -namespace Deal.Tests.Unit; +namespace Deal.Tests.Unit.Modules.Kanban; /// /// Тесты ручной проверки/разметки ML (§8): кандидаты канала/выборки и применение решения. diff --git a/src/core/tests/Deal.Tests.Unit/StorageTickServiceTests.cs b/src/core/tests/Deal.Tests.Unit/Modules/Kanban/StorageTickServiceTests.cs similarity index 98% rename from src/core/tests/Deal.Tests.Unit/StorageTickServiceTests.cs rename to src/core/tests/Deal.Tests.Unit/Modules/Kanban/StorageTickServiceTests.cs index 2b9290f..a69b17c 100644 --- a/src/core/tests/Deal.Tests.Unit/StorageTickServiceTests.cs +++ b/src/core/tests/Deal.Tests.Unit/Modules/Kanban/StorageTickServiceTests.cs @@ -7,8 +7,11 @@ using Deal.Modules.Settings.Application.Abstractions; using Deal.Modules.Settings.Application.Models; using Deal.Modules.Settings.Application.Registrars; using Deal.Modules.Settings.Application.Services; +using Deal.Tests.Unit.Support; +using Deal.Tests.Unit.Modules.Cards; +using Deal.Tests.Unit.Modules.Settings; -namespace Deal.Tests.Unit; +namespace Deal.Tests.Unit.Modules.Kanban; /// /// Тесты StorageTickService — тик правил хранения (Ruling 8, план Task 10 L395; leads.py tick_storage L454–493). diff --git a/src/core/tests/Deal.Tests.Unit/SuggestHeuristicsTests.cs b/src/core/tests/Deal.Tests.Unit/Modules/Kanban/SuggestHeuristicsTests.cs similarity index 99% rename from src/core/tests/Deal.Tests.Unit/SuggestHeuristicsTests.cs rename to src/core/tests/Deal.Tests.Unit/Modules/Kanban/SuggestHeuristicsTests.cs index 69df26a..f590282 100644 --- a/src/core/tests/Deal.Tests.Unit/SuggestHeuristicsTests.cs +++ b/src/core/tests/Deal.Tests.Unit/Modules/Kanban/SuggestHeuristicsTests.cs @@ -3,8 +3,9 @@ using Deal.Modules.Kanban.Application.Extensions; using Deal.Modules.Kanban.Application.Models; using Deal.Modules.Kanban.Application.Registrars; using Deal.Modules.Kanban.Application.Services; +using Deal.Tests.Unit.Support; -namespace Deal.Tests.Unit; +namespace Deal.Tests.Unit.Modules.Kanban; /// /// Тесты чистого ядра ИИ-предложений (Ruling 3, план Task 14 L467–471). diff --git a/src/core/tests/Deal.Tests.Unit/GlobalExclusionRulesTests.cs b/src/core/tests/Deal.Tests.Unit/Modules/Pipeline/GlobalExclusionRulesTests.cs similarity index 97% rename from src/core/tests/Deal.Tests.Unit/GlobalExclusionRulesTests.cs rename to src/core/tests/Deal.Tests.Unit/Modules/Pipeline/GlobalExclusionRulesTests.cs index dca9377..fb422bf 100644 --- a/src/core/tests/Deal.Tests.Unit/GlobalExclusionRulesTests.cs +++ b/src/core/tests/Deal.Tests.Unit/Modules/Pipeline/GlobalExclusionRulesTests.cs @@ -2,8 +2,9 @@ using Deal.Modules.Pipeline.Application.Abstractions; using Deal.Modules.Pipeline.Application.Models; using Deal.Modules.Pipeline.Application.Registrars; using Deal.Modules.Pipeline.Application.Services; +using Deal.Tests.Unit.Support; -namespace Deal.Tests.Unit; +namespace Deal.Tests.Unit.Modules.Pipeline; /// /// Тесты глобальных исключений (§5.14): слова/локации/типы/бюджет — чистая проверка до ML/ИИ. diff --git a/src/core/tests/Deal.Tests.Unit/PipelineIngestServiceTests.cs b/src/core/tests/Deal.Tests.Unit/Modules/Pipeline/PipelineIngestServiceTests.cs similarity index 98% rename from src/core/tests/Deal.Tests.Unit/PipelineIngestServiceTests.cs rename to src/core/tests/Deal.Tests.Unit/Modules/Pipeline/PipelineIngestServiceTests.cs index 174f731..94a46c9 100644 --- a/src/core/tests/Deal.Tests.Unit/PipelineIngestServiceTests.cs +++ b/src/core/tests/Deal.Tests.Unit/Modules/Pipeline/PipelineIngestServiceTests.cs @@ -2,8 +2,10 @@ using Deal.Modules.Pipeline.Application.Abstractions; using Deal.Modules.Pipeline.Application.Models; using Deal.Modules.Pipeline.Application.Registrars; using Deal.Modules.Pipeline.Application.Services; +using Deal.Tests.Unit.Support; +using Deal.Tests.Unit.Modules.Kanban; -namespace Deal.Tests.Unit; +namespace Deal.Tests.Unit.Modules.Pipeline; /// /// Тесты приёма входящих сообщений — PipelineIngestService (план Task 5 L353–354, Ruling 2; pipeline.py enqueue L53–85). diff --git a/src/core/tests/Deal.Tests.Unit/PipelineProcessingServiceTests.cs b/src/core/tests/Deal.Tests.Unit/Modules/Pipeline/PipelineProcessingServiceTests.cs similarity index 99% rename from src/core/tests/Deal.Tests.Unit/PipelineProcessingServiceTests.cs rename to src/core/tests/Deal.Tests.Unit/Modules/Pipeline/PipelineProcessingServiceTests.cs index 8085cbb..f3f6e6b 100644 --- a/src/core/tests/Deal.Tests.Unit/PipelineProcessingServiceTests.cs +++ b/src/core/tests/Deal.Tests.Unit/Modules/Pipeline/PipelineProcessingServiceTests.cs @@ -2,8 +2,11 @@ using Deal.Modules.Pipeline.Application.Abstractions; using Deal.Modules.Pipeline.Application.Models; using Deal.Modules.Pipeline.Application.Registrars; using Deal.Modules.Pipeline.Application.Services; +using Deal.Tests.Unit.Support; +using Deal.Tests.Unit.Modules.Kanban; +using Deal.Tests.Unit.Contracts; -namespace Deal.Tests.Unit; +namespace Deal.Tests.Unit.Modules.Pipeline; /// /// Тесты обслуживания пайплайна — PipelineProcessingService (план Task 5 L355–361, Ruling 8/10; diff --git a/src/core/tests/Deal.Tests.Unit/FakeGlobalSettingsStore.cs b/src/core/tests/Deal.Tests.Unit/Modules/Settings/FakeGlobalSettingsStore.cs similarity index 97% rename from src/core/tests/Deal.Tests.Unit/FakeGlobalSettingsStore.cs rename to src/core/tests/Deal.Tests.Unit/Modules/Settings/FakeGlobalSettingsStore.cs index f66c53e..770bac9 100644 --- a/src/core/tests/Deal.Tests.Unit/FakeGlobalSettingsStore.cs +++ b/src/core/tests/Deal.Tests.Unit/Modules/Settings/FakeGlobalSettingsStore.cs @@ -2,8 +2,9 @@ using Deal.Modules.Settings.Application.Abstractions; using Deal.Modules.Settings.Application.Models; using Deal.Modules.Settings.Application.Registrars; using Deal.Modules.Settings.Application.Services; +using Deal.Tests.Unit.Support; -namespace Deal.Tests.Unit; +namespace Deal.Tests.Unit.Modules.Settings; /// /// In-memory реализация для юнит-тестов глобальных настроек. diff --git a/src/core/tests/Deal.Tests.Unit/FakeRatesListener.cs b/src/core/tests/Deal.Tests.Unit/Modules/Settings/FakeRatesListener.cs similarity index 94% rename from src/core/tests/Deal.Tests.Unit/FakeRatesListener.cs rename to src/core/tests/Deal.Tests.Unit/Modules/Settings/FakeRatesListener.cs index 61819f8..3a48285 100644 --- a/src/core/tests/Deal.Tests.Unit/FakeRatesListener.cs +++ b/src/core/tests/Deal.Tests.Unit/Modules/Settings/FakeRatesListener.cs @@ -2,8 +2,9 @@ using Deal.Modules.Settings.Application.Abstractions; using Deal.Modules.Settings.Application.Models; using Deal.Modules.Settings.Application.Registrars; using Deal.Modules.Settings.Application.Services; +using Deal.Tests.Unit.Support; -namespace Deal.Tests.Unit; +namespace Deal.Tests.Unit.Modules.Settings; /// /// In-memory реализация для тестов триггеров пересчёта (Task 12). diff --git a/src/core/tests/Deal.Tests.Unit/FakeRatesSource.cs b/src/core/tests/Deal.Tests.Unit/Modules/Settings/FakeRatesSource.cs similarity index 96% rename from src/core/tests/Deal.Tests.Unit/FakeRatesSource.cs rename to src/core/tests/Deal.Tests.Unit/Modules/Settings/FakeRatesSource.cs index 9004073..139a481 100644 --- a/src/core/tests/Deal.Tests.Unit/FakeRatesSource.cs +++ b/src/core/tests/Deal.Tests.Unit/Modules/Settings/FakeRatesSource.cs @@ -2,8 +2,9 @@ using Deal.Modules.Settings.Application.Abstractions; using Deal.Modules.Settings.Application.Models; using Deal.Modules.Settings.Application.Registrars; using Deal.Modules.Settings.Application.Services; +using Deal.Tests.Unit.Support; -namespace Deal.Tests.Unit; +namespace Deal.Tests.Unit.Modules.Settings; /// /// Фейковый источник курсов для тестов RatesService (Task 8, Ruling 6). diff --git a/src/core/tests/Deal.Tests.Unit/FakeSettingsStore.cs b/src/core/tests/Deal.Tests.Unit/Modules/Settings/FakeSettingsStore.cs similarity index 97% rename from src/core/tests/Deal.Tests.Unit/FakeSettingsStore.cs rename to src/core/tests/Deal.Tests.Unit/Modules/Settings/FakeSettingsStore.cs index a453b15..df856d1 100644 --- a/src/core/tests/Deal.Tests.Unit/FakeSettingsStore.cs +++ b/src/core/tests/Deal.Tests.Unit/Modules/Settings/FakeSettingsStore.cs @@ -2,8 +2,9 @@ using Deal.Modules.Settings.Application.Abstractions; using Deal.Modules.Settings.Application.Models; using Deal.Modules.Settings.Application.Registrars; using Deal.Modules.Settings.Application.Services; +using Deal.Tests.Unit.Support; -namespace Deal.Tests.Unit; +namespace Deal.Tests.Unit.Modules.Settings; /// /// In-memory реализация для юнит-тестов SettingsService (Task 3). diff --git a/src/core/tests/Deal.Tests.Unit/PromptDefaultsTests.cs b/src/core/tests/Deal.Tests.Unit/Modules/Settings/PromptDefaultsTests.cs similarity index 99% rename from src/core/tests/Deal.Tests.Unit/PromptDefaultsTests.cs rename to src/core/tests/Deal.Tests.Unit/Modules/Settings/PromptDefaultsTests.cs index 49056f9..5802812 100644 --- a/src/core/tests/Deal.Tests.Unit/PromptDefaultsTests.cs +++ b/src/core/tests/Deal.Tests.Unit/Modules/Settings/PromptDefaultsTests.cs @@ -2,8 +2,9 @@ using Deal.Modules.Settings.Application.Abstractions; using Deal.Modules.Settings.Application.Models; using Deal.Modules.Settings.Application.Registrars; using Deal.Modules.Settings.Application.Services; +using Deal.Tests.Unit.Support; -namespace Deal.Tests.Unit; +namespace Deal.Tests.Unit.Modules.Settings; /// /// Тесты границы промптов с фронтом: дефолтные тексты 1:1 с ресурсами фронта и подстановка PromptFiller (Task 7). diff --git a/src/core/tests/Deal.Tests.Unit/SettingsCatalogTests.cs b/src/core/tests/Deal.Tests.Unit/Modules/Settings/SettingsCatalogTests.cs similarity index 99% rename from src/core/tests/Deal.Tests.Unit/SettingsCatalogTests.cs rename to src/core/tests/Deal.Tests.Unit/Modules/Settings/SettingsCatalogTests.cs index 2f54923..638a53c 100644 --- a/src/core/tests/Deal.Tests.Unit/SettingsCatalogTests.cs +++ b/src/core/tests/Deal.Tests.Unit/Modules/Settings/SettingsCatalogTests.cs @@ -2,8 +2,9 @@ using Deal.Modules.Settings.Application.Abstractions; using Deal.Modules.Settings.Application.Models; using Deal.Modules.Settings.Application.Registrars; using Deal.Modules.Settings.Application.Services; +using Deal.Tests.Unit.Support; -namespace Deal.Tests.Unit; +namespace Deal.Tests.Unit.Modules.Settings; /// /// Тесты каталога ключей настроек, дефолтов и провайдеров (Task 2, Ruling 1/3). diff --git a/src/core/tests/Deal.Tests.Unit/AuditEventsTests.cs b/src/core/tests/Deal.Tests.Unit/Modules/Tenants/AuditEventsTests.cs similarity index 94% rename from src/core/tests/Deal.Tests.Unit/AuditEventsTests.cs rename to src/core/tests/Deal.Tests.Unit/Modules/Tenants/AuditEventsTests.cs index 2183d98..c9149ae 100644 --- a/src/core/tests/Deal.Tests.Unit/AuditEventsTests.cs +++ b/src/core/tests/Deal.Tests.Unit/Modules/Tenants/AuditEventsTests.cs @@ -3,8 +3,9 @@ using Deal.Modules.Tenants.Application.Extensions; using Deal.Modules.Tenants.Application.Models; using Deal.Modules.Tenants.Application.Registrars; using Deal.Modules.Tenants.Application.Services; +using Deal.Tests.Unit.Support; -namespace Deal.Tests.Unit; +namespace Deal.Tests.Unit.Modules.Tenants; /// /// Стабильность каталога типов событий аудита (Ruling 4 этапа 7; расширение этапа 10, T1). diff --git a/src/core/tests/Deal.Tests.Unit/AuditServiceTests.cs b/src/core/tests/Deal.Tests.Unit/Modules/Tenants/AuditServiceTests.cs similarity index 99% rename from src/core/tests/Deal.Tests.Unit/AuditServiceTests.cs rename to src/core/tests/Deal.Tests.Unit/Modules/Tenants/AuditServiceTests.cs index 44998d3..b8e66da 100644 --- a/src/core/tests/Deal.Tests.Unit/AuditServiceTests.cs +++ b/src/core/tests/Deal.Tests.Unit/Modules/Tenants/AuditServiceTests.cs @@ -3,8 +3,9 @@ using Deal.Modules.Tenants.Application.Extensions; using Deal.Modules.Tenants.Application.Models; using Deal.Modules.Tenants.Application.Registrars; using Deal.Modules.Tenants.Application.Services; +using Deal.Tests.Unit.Support; -namespace Deal.Tests.Unit; +namespace Deal.Tests.Unit.Modules.Tenants; /// /// Юнит-тесты AuditService (Task 4, Ruling 4): append-only запись (At=UTC-now), чтение/счёт по фильтрам, diff --git a/src/core/tests/Deal.Tests.Unit/AuthServiceTests.cs b/src/core/tests/Deal.Tests.Unit/Modules/Tenants/AuthServiceTests.cs similarity index 99% rename from src/core/tests/Deal.Tests.Unit/AuthServiceTests.cs rename to src/core/tests/Deal.Tests.Unit/Modules/Tenants/AuthServiceTests.cs index 6de7a99..d30fdb5 100644 --- a/src/core/tests/Deal.Tests.Unit/AuthServiceTests.cs +++ b/src/core/tests/Deal.Tests.Unit/Modules/Tenants/AuthServiceTests.cs @@ -3,8 +3,9 @@ using Deal.Modules.Tenants.Application.Extensions; using Deal.Modules.Tenants.Application.Models; using Deal.Modules.Tenants.Application.Registrars; using Deal.Modules.Tenants.Application.Services; +using Deal.Tests.Unit.Support; -namespace Deal.Tests.Unit; +namespace Deal.Tests.Unit.Modules.Tenants; /// /// Юнит-тесты AuthService на fake-хранилище и детерминированном «хэшере». diff --git a/src/core/tests/Deal.Tests.Unit/FailingTenantProvisioner.cs b/src/core/tests/Deal.Tests.Unit/Modules/Tenants/FailingTenantProvisioner.cs similarity index 94% rename from src/core/tests/Deal.Tests.Unit/FailingTenantProvisioner.cs rename to src/core/tests/Deal.Tests.Unit/Modules/Tenants/FailingTenantProvisioner.cs index 0163841..5519904 100644 --- a/src/core/tests/Deal.Tests.Unit/FailingTenantProvisioner.cs +++ b/src/core/tests/Deal.Tests.Unit/Modules/Tenants/FailingTenantProvisioner.cs @@ -5,8 +5,9 @@ using Deal.Modules.Tenants.Application.Registrars; using Deal.Modules.Tenants.Application.Services; using Deal.SharedKernel.Tenants.Abstractions; using Deal.SharedKernel.Tenants.Models; +using Deal.Tests.Unit.Support; -namespace Deal.Tests.Unit; +namespace Deal.Tests.Unit.Modules.Tenants; /// /// In-memory реализация для тестов пакетной миграции (этап 12, пакет C): diff --git a/src/core/tests/Deal.Tests.Unit/FakeAuditLogStore.cs b/src/core/tests/Deal.Tests.Unit/Modules/Tenants/FakeAuditLogStore.cs similarity index 97% rename from src/core/tests/Deal.Tests.Unit/FakeAuditLogStore.cs rename to src/core/tests/Deal.Tests.Unit/Modules/Tenants/FakeAuditLogStore.cs index 4b84e91..74c835f 100644 --- a/src/core/tests/Deal.Tests.Unit/FakeAuditLogStore.cs +++ b/src/core/tests/Deal.Tests.Unit/Modules/Tenants/FakeAuditLogStore.cs @@ -3,8 +3,9 @@ using Deal.Modules.Tenants.Application.Extensions; using Deal.Modules.Tenants.Application.Models; using Deal.Modules.Tenants.Application.Registrars; using Deal.Modules.Tenants.Application.Services; +using Deal.Tests.Unit.Support; -namespace Deal.Tests.Unit; +namespace Deal.Tests.Unit.Modules.Tenants; /// /// In-memory реализация для юнит/HTTP-тестов аудита (Task 4). diff --git a/src/core/tests/Deal.Tests.Unit/FakeAuthStore.cs b/src/core/tests/Deal.Tests.Unit/Modules/Tenants/FakeAuthStore.cs similarity index 98% rename from src/core/tests/Deal.Tests.Unit/FakeAuthStore.cs rename to src/core/tests/Deal.Tests.Unit/Modules/Tenants/FakeAuthStore.cs index b822edc..ff9041c 100644 --- a/src/core/tests/Deal.Tests.Unit/FakeAuthStore.cs +++ b/src/core/tests/Deal.Tests.Unit/Modules/Tenants/FakeAuthStore.cs @@ -3,8 +3,9 @@ using Deal.Modules.Tenants.Application.Extensions; using Deal.Modules.Tenants.Application.Models; using Deal.Modules.Tenants.Application.Registrars; using Deal.Modules.Tenants.Application.Services; +using Deal.Tests.Unit.Support; -namespace Deal.Tests.Unit; +namespace Deal.Tests.Unit.Modules.Tenants; /// /// In-memory реализация для юнит-тестов AuthService. diff --git a/src/core/tests/Deal.Tests.Unit/FakeInviteStore.cs b/src/core/tests/Deal.Tests.Unit/Modules/Tenants/FakeInviteStore.cs similarity index 98% rename from src/core/tests/Deal.Tests.Unit/FakeInviteStore.cs rename to src/core/tests/Deal.Tests.Unit/Modules/Tenants/FakeInviteStore.cs index 04beb9c..113dd47 100644 --- a/src/core/tests/Deal.Tests.Unit/FakeInviteStore.cs +++ b/src/core/tests/Deal.Tests.Unit/Modules/Tenants/FakeInviteStore.cs @@ -3,8 +3,9 @@ using Deal.Modules.Tenants.Application.Extensions; using Deal.Modules.Tenants.Application.Models; using Deal.Modules.Tenants.Application.Registrars; using Deal.Modules.Tenants.Application.Services; +using Deal.Tests.Unit.Support; -namespace Deal.Tests.Unit; +namespace Deal.Tests.Unit.Modules.Tenants; /// /// In-memory реализация для юнит/HTTP-тестов приглашений (Task 5). diff --git a/src/core/tests/Deal.Tests.Unit/FakeOperatorAuthStore.cs b/src/core/tests/Deal.Tests.Unit/Modules/Tenants/FakeOperatorAuthStore.cs similarity index 97% rename from src/core/tests/Deal.Tests.Unit/FakeOperatorAuthStore.cs rename to src/core/tests/Deal.Tests.Unit/Modules/Tenants/FakeOperatorAuthStore.cs index adce295..d1e3d45 100644 --- a/src/core/tests/Deal.Tests.Unit/FakeOperatorAuthStore.cs +++ b/src/core/tests/Deal.Tests.Unit/Modules/Tenants/FakeOperatorAuthStore.cs @@ -3,8 +3,9 @@ using Deal.Modules.Tenants.Application.Extensions; using Deal.Modules.Tenants.Application.Models; using Deal.Modules.Tenants.Application.Registrars; using Deal.Modules.Tenants.Application.Services; +using Deal.Tests.Unit.Support; -namespace Deal.Tests.Unit; +namespace Deal.Tests.Unit.Modules.Tenants; /// /// In-memory реализация для юнит-тестов OperatorAuthService. diff --git a/src/core/tests/Deal.Tests.Unit/FakePasswordHasher.cs b/src/core/tests/Deal.Tests.Unit/Modules/Tenants/FakePasswordHasher.cs similarity index 92% rename from src/core/tests/Deal.Tests.Unit/FakePasswordHasher.cs rename to src/core/tests/Deal.Tests.Unit/Modules/Tenants/FakePasswordHasher.cs index 76e64b6..ce0ab6c 100644 --- a/src/core/tests/Deal.Tests.Unit/FakePasswordHasher.cs +++ b/src/core/tests/Deal.Tests.Unit/Modules/Tenants/FakePasswordHasher.cs @@ -3,8 +3,9 @@ using Deal.Modules.Tenants.Application.Extensions; using Deal.Modules.Tenants.Application.Models; using Deal.Modules.Tenants.Application.Registrars; using Deal.Modules.Tenants.Application.Services; +using Deal.Tests.Unit.Support; -namespace Deal.Tests.Unit; +namespace Deal.Tests.Unit.Modules.Tenants; /// /// Детерминированный «хэшер» для тестов AuthService (взаимно-однозначное кодирование). diff --git a/src/core/tests/Deal.Tests.Unit/FakeRateLimitCounterStore.cs b/src/core/tests/Deal.Tests.Unit/Modules/Tenants/FakeRateLimitCounterStore.cs similarity index 94% rename from src/core/tests/Deal.Tests.Unit/FakeRateLimitCounterStore.cs rename to src/core/tests/Deal.Tests.Unit/Modules/Tenants/FakeRateLimitCounterStore.cs index 7633af0..b6b11b4 100644 --- a/src/core/tests/Deal.Tests.Unit/FakeRateLimitCounterStore.cs +++ b/src/core/tests/Deal.Tests.Unit/Modules/Tenants/FakeRateLimitCounterStore.cs @@ -3,8 +3,9 @@ using Deal.Modules.Tenants.Application.Extensions; using Deal.Modules.Tenants.Application.Models; using Deal.Modules.Tenants.Application.Registrars; using Deal.Modules.Tenants.Application.Services; +using Deal.Tests.Unit.Support; -namespace Deal.Tests.Unit; +namespace Deal.Tests.Unit.Modules.Tenants; /// /// In-memory реализация для юнит/HTTP-тестов (этап 12, пакет B): diff --git a/src/core/tests/Deal.Tests.Unit/FakeTenantLimitStore.cs b/src/core/tests/Deal.Tests.Unit/Modules/Tenants/FakeTenantLimitStore.cs similarity index 99% rename from src/core/tests/Deal.Tests.Unit/FakeTenantLimitStore.cs rename to src/core/tests/Deal.Tests.Unit/Modules/Tenants/FakeTenantLimitStore.cs index d7af6ff..080494d 100644 --- a/src/core/tests/Deal.Tests.Unit/FakeTenantLimitStore.cs +++ b/src/core/tests/Deal.Tests.Unit/Modules/Tenants/FakeTenantLimitStore.cs @@ -3,8 +3,9 @@ using Deal.Modules.Tenants.Application.Extensions; using Deal.Modules.Tenants.Application.Models; using Deal.Modules.Tenants.Application.Registrars; using Deal.Modules.Tenants.Application.Services; +using Deal.Tests.Unit.Support; -namespace Deal.Tests.Unit; +namespace Deal.Tests.Unit.Modules.Tenants; /// /// In-memory реализация для юнит-тестов recorder'а (Task 8) и diff --git a/src/core/tests/Deal.Tests.Unit/FakeTenantProvisioner.cs b/src/core/tests/Deal.Tests.Unit/Modules/Tenants/FakeTenantProvisioner.cs similarity index 96% rename from src/core/tests/Deal.Tests.Unit/FakeTenantProvisioner.cs rename to src/core/tests/Deal.Tests.Unit/Modules/Tenants/FakeTenantProvisioner.cs index 718d103..6631aab 100644 --- a/src/core/tests/Deal.Tests.Unit/FakeTenantProvisioner.cs +++ b/src/core/tests/Deal.Tests.Unit/Modules/Tenants/FakeTenantProvisioner.cs @@ -5,8 +5,9 @@ using Deal.Modules.Tenants.Application.Registrars; using Deal.Modules.Tenants.Application.Services; using Deal.SharedKernel.Tenants.Abstractions; using Deal.SharedKernel.Tenants.Models; +using Deal.Tests.Unit.Support; -namespace Deal.Tests.Unit; +namespace Deal.Tests.Unit.Modules.Tenants; /// /// In-memory реализация для unit/HTTP-тестов join-потока (Task 6): записывает вызовы. diff --git a/src/core/tests/Deal.Tests.Unit/FakeTenantRegistry.cs b/src/core/tests/Deal.Tests.Unit/Modules/Tenants/FakeTenantRegistry.cs similarity index 96% rename from src/core/tests/Deal.Tests.Unit/FakeTenantRegistry.cs rename to src/core/tests/Deal.Tests.Unit/Modules/Tenants/FakeTenantRegistry.cs index c94cc7f..7a4cb60 100644 --- a/src/core/tests/Deal.Tests.Unit/FakeTenantRegistry.cs +++ b/src/core/tests/Deal.Tests.Unit/Modules/Tenants/FakeTenantRegistry.cs @@ -3,8 +3,9 @@ using Deal.Modules.Tenants.Application.Extensions; using Deal.Modules.Tenants.Application.Models; using Deal.Modules.Tenants.Application.Registrars; using Deal.Modules.Tenants.Application.Services; +using Deal.Tests.Unit.Support; -namespace Deal.Tests.Unit; +namespace Deal.Tests.Unit.Modules.Tenants; /// /// In-memory реализация для тестов gRPC-ингресса (план Task 12). diff --git a/src/core/tests/Deal.Tests.Unit/FakeTenantRepository.cs b/src/core/tests/Deal.Tests.Unit/Modules/Tenants/FakeTenantRepository.cs similarity index 96% rename from src/core/tests/Deal.Tests.Unit/FakeTenantRepository.cs rename to src/core/tests/Deal.Tests.Unit/Modules/Tenants/FakeTenantRepository.cs index 2d830d4..d15ade4 100644 --- a/src/core/tests/Deal.Tests.Unit/FakeTenantRepository.cs +++ b/src/core/tests/Deal.Tests.Unit/Modules/Tenants/FakeTenantRepository.cs @@ -3,8 +3,9 @@ using Deal.Modules.Tenants.Application.Extensions; using Deal.Modules.Tenants.Application.Models; using Deal.Modules.Tenants.Application.Registrars; using Deal.Modules.Tenants.Application.Services; +using Deal.Tests.Unit.Support; -namespace Deal.Tests.Unit; +namespace Deal.Tests.Unit.Modules.Tenants; /// /// In-memory реализация для unit-тестов (реестр тенантов, таблица public.tenants). diff --git a/src/core/tests/Deal.Tests.Unit/FakeTenantStore.cs b/src/core/tests/Deal.Tests.Unit/Modules/Tenants/FakeTenantStore.cs similarity index 97% rename from src/core/tests/Deal.Tests.Unit/FakeTenantStore.cs rename to src/core/tests/Deal.Tests.Unit/Modules/Tenants/FakeTenantStore.cs index 98387ea..0c568f1 100644 --- a/src/core/tests/Deal.Tests.Unit/FakeTenantStore.cs +++ b/src/core/tests/Deal.Tests.Unit/Modules/Tenants/FakeTenantStore.cs @@ -3,8 +3,9 @@ using Deal.Modules.Tenants.Application.Extensions; using Deal.Modules.Tenants.Application.Models; using Deal.Modules.Tenants.Application.Registrars; using Deal.Modules.Tenants.Application.Services; +using Deal.Tests.Unit.Support; -namespace Deal.Tests.Unit; +namespace Deal.Tests.Unit.Modules.Tenants; /// /// In-memory реализация с поддержкой создания тенантов (join-поток, Task 6). diff --git a/src/core/tests/Deal.Tests.Unit/FakeTokenUsageEventStore.cs b/src/core/tests/Deal.Tests.Unit/Modules/Tenants/FakeTokenUsageEventStore.cs similarity index 98% rename from src/core/tests/Deal.Tests.Unit/FakeTokenUsageEventStore.cs rename to src/core/tests/Deal.Tests.Unit/Modules/Tenants/FakeTokenUsageEventStore.cs index 97ecc0c..45a32c9 100644 --- a/src/core/tests/Deal.Tests.Unit/FakeTokenUsageEventStore.cs +++ b/src/core/tests/Deal.Tests.Unit/Modules/Tenants/FakeTokenUsageEventStore.cs @@ -3,8 +3,9 @@ using Deal.Modules.Tenants.Application.Extensions; using Deal.Modules.Tenants.Application.Models; using Deal.Modules.Tenants.Application.Registrars; using Deal.Modules.Tenants.Application.Services; +using Deal.Tests.Unit.Support; -namespace Deal.Tests.Unit; +namespace Deal.Tests.Unit.Modules.Tenants; /// /// In-memory реализация для юнит/HTTP-тестов (этап 10, T2). diff --git a/src/core/tests/Deal.Tests.Unit/InviteCodeGeneratorTests.cs b/src/core/tests/Deal.Tests.Unit/Modules/Tenants/InviteCodeGeneratorTests.cs similarity index 94% rename from src/core/tests/Deal.Tests.Unit/InviteCodeGeneratorTests.cs rename to src/core/tests/Deal.Tests.Unit/Modules/Tenants/InviteCodeGeneratorTests.cs index 9e9be9d..1515eb0 100644 --- a/src/core/tests/Deal.Tests.Unit/InviteCodeGeneratorTests.cs +++ b/src/core/tests/Deal.Tests.Unit/Modules/Tenants/InviteCodeGeneratorTests.cs @@ -3,8 +3,9 @@ using Deal.Modules.Tenants.Application.Extensions; using Deal.Modules.Tenants.Application.Models; using Deal.Modules.Tenants.Application.Registrars; using Deal.Modules.Tenants.Application.Services; +using Deal.Tests.Unit.Support; -namespace Deal.Tests.Unit; +namespace Deal.Tests.Unit.Modules.Tenants; /// /// Юнит-тесты генератора кодов приглашений (Ruling 2: случайный url-safe, 16 симв., без префикса). diff --git a/src/core/tests/Deal.Tests.Unit/InvitesServiceTests.cs b/src/core/tests/Deal.Tests.Unit/Modules/Tenants/InvitesServiceTests.cs similarity index 99% rename from src/core/tests/Deal.Tests.Unit/InvitesServiceTests.cs rename to src/core/tests/Deal.Tests.Unit/Modules/Tenants/InvitesServiceTests.cs index 2a63e70..8a6661b 100644 --- a/src/core/tests/Deal.Tests.Unit/InvitesServiceTests.cs +++ b/src/core/tests/Deal.Tests.Unit/Modules/Tenants/InvitesServiceTests.cs @@ -3,8 +3,9 @@ using Deal.Modules.Tenants.Application.Extensions; using Deal.Modules.Tenants.Application.Models; using Deal.Modules.Tenants.Application.Registrars; using Deal.Modules.Tenants.Application.Services; +using Deal.Tests.Unit.Support; -namespace Deal.Tests.Unit; +namespace Deal.Tests.Unit.Modules.Tenants; /// /// Юнит-тесты InvitesService на fake-хранилище: создание (код/срок/email/антидубль), отзыв, expiry при чтении, список. diff --git a/src/core/tests/Deal.Tests.Unit/JoinFlowTests.cs b/src/core/tests/Deal.Tests.Unit/Modules/Tenants/JoinFlowTests.cs similarity index 99% rename from src/core/tests/Deal.Tests.Unit/JoinFlowTests.cs rename to src/core/tests/Deal.Tests.Unit/Modules/Tenants/JoinFlowTests.cs index 0877001..4a2a820 100644 --- a/src/core/tests/Deal.Tests.Unit/JoinFlowTests.cs +++ b/src/core/tests/Deal.Tests.Unit/Modules/Tenants/JoinFlowTests.cs @@ -3,8 +3,9 @@ using Deal.Modules.Tenants.Application.Extensions; using Deal.Modules.Tenants.Application.Models; using Deal.Modules.Tenants.Application.Registrars; using Deal.Modules.Tenants.Application.Services; +using Deal.Tests.Unit.Support; -namespace Deal.Tests.Unit; +namespace Deal.Tests.Unit.Modules.Tenants; /// /// Юнит-тесты JoinService (активация инвайта, Task 6) на фейк-хранилищах: успех с новым/существующим diff --git a/src/core/tests/Deal.Tests.Unit/OperatorAuthServiceTests.cs b/src/core/tests/Deal.Tests.Unit/Modules/Tenants/OperatorAuthServiceTests.cs similarity index 98% rename from src/core/tests/Deal.Tests.Unit/OperatorAuthServiceTests.cs rename to src/core/tests/Deal.Tests.Unit/Modules/Tenants/OperatorAuthServiceTests.cs index 9b2dc00..81c8088 100644 --- a/src/core/tests/Deal.Tests.Unit/OperatorAuthServiceTests.cs +++ b/src/core/tests/Deal.Tests.Unit/Modules/Tenants/OperatorAuthServiceTests.cs @@ -3,8 +3,9 @@ using Deal.Modules.Tenants.Application.Extensions; using Deal.Modules.Tenants.Application.Models; using Deal.Modules.Tenants.Application.Registrars; using Deal.Modules.Tenants.Application.Services; +using Deal.Tests.Unit.Support; -namespace Deal.Tests.Unit; +namespace Deal.Tests.Unit.Modules.Tenants; /// /// Юнит-тесты OperatorAuthService на fake-хранилище и детерминированном «хэшере». diff --git a/src/core/tests/Deal.Tests.Unit/OperatorBootstrapServiceTests.cs b/src/core/tests/Deal.Tests.Unit/Modules/Tenants/OperatorBootstrapServiceTests.cs similarity index 98% rename from src/core/tests/Deal.Tests.Unit/OperatorBootstrapServiceTests.cs rename to src/core/tests/Deal.Tests.Unit/Modules/Tenants/OperatorBootstrapServiceTests.cs index 76d0d63..5884742 100644 --- a/src/core/tests/Deal.Tests.Unit/OperatorBootstrapServiceTests.cs +++ b/src/core/tests/Deal.Tests.Unit/Modules/Tenants/OperatorBootstrapServiceTests.cs @@ -3,8 +3,9 @@ using Deal.Modules.Tenants.Application.Extensions; using Deal.Modules.Tenants.Application.Models; using Deal.Modules.Tenants.Application.Registrars; using Deal.Modules.Tenants.Application.Services; +using Deal.Tests.Unit.Support; -namespace Deal.Tests.Unit; +namespace Deal.Tests.Unit.Modules.Tenants; /// /// Юнит-тесты bootstrap оператора (Ruling 1 этапа 7): dev-дефолт, prod-skip, идемпотентность. diff --git a/src/core/tests/Deal.Tests.Unit/PasswordHasherTests.cs b/src/core/tests/Deal.Tests.Unit/Modules/Tenants/PasswordHasherTests.cs similarity index 95% rename from src/core/tests/Deal.Tests.Unit/PasswordHasherTests.cs rename to src/core/tests/Deal.Tests.Unit/Modules/Tenants/PasswordHasherTests.cs index 05d2910..c8ab56b 100644 --- a/src/core/tests/Deal.Tests.Unit/PasswordHasherTests.cs +++ b/src/core/tests/Deal.Tests.Unit/Modules/Tenants/PasswordHasherTests.cs @@ -3,8 +3,9 @@ using Deal.Modules.Tenants.Application.Extensions; using Deal.Modules.Tenants.Application.Models; using Deal.Modules.Tenants.Application.Registrars; using Deal.Modules.Tenants.Application.Services; +using Deal.Tests.Unit.Support; -namespace Deal.Tests.Unit; +namespace Deal.Tests.Unit.Modules.Tenants; /// /// Тесты Argon2id-хэшера паролей (Ruling 5, DefaultPasswordHasher). diff --git a/src/core/tests/Deal.Tests.Unit/SessionTokensTests.cs b/src/core/tests/Deal.Tests.Unit/Modules/Tenants/SessionTokensTests.cs similarity index 95% rename from src/core/tests/Deal.Tests.Unit/SessionTokensTests.cs rename to src/core/tests/Deal.Tests.Unit/Modules/Tenants/SessionTokensTests.cs index 2fa8489..481b9a6 100644 --- a/src/core/tests/Deal.Tests.Unit/SessionTokensTests.cs +++ b/src/core/tests/Deal.Tests.Unit/Modules/Tenants/SessionTokensTests.cs @@ -3,8 +3,9 @@ using Deal.Modules.Tenants.Application.Extensions; using Deal.Modules.Tenants.Application.Models; using Deal.Modules.Tenants.Application.Registrars; using Deal.Modules.Tenants.Application.Services; +using Deal.Tests.Unit.Support; -namespace Deal.Tests.Unit; +namespace Deal.Tests.Unit.Modules.Tenants; /// /// Тесты токенов сессий: Base64Url-генерация и детерминированный SHA-256 (Ruling 6). diff --git a/src/core/tests/Deal.Tests.Unit/SuspiciousActivityServiceTests.cs b/src/core/tests/Deal.Tests.Unit/Modules/Tenants/SuspiciousActivityServiceTests.cs similarity index 98% rename from src/core/tests/Deal.Tests.Unit/SuspiciousActivityServiceTests.cs rename to src/core/tests/Deal.Tests.Unit/Modules/Tenants/SuspiciousActivityServiceTests.cs index 50c2f7c..5d0c6c8 100644 --- a/src/core/tests/Deal.Tests.Unit/SuspiciousActivityServiceTests.cs +++ b/src/core/tests/Deal.Tests.Unit/Modules/Tenants/SuspiciousActivityServiceTests.cs @@ -3,8 +3,9 @@ using Deal.Modules.Tenants.Application.Extensions; using Deal.Modules.Tenants.Application.Models; using Deal.Modules.Tenants.Application.Registrars; using Deal.Modules.Tenants.Application.Services; +using Deal.Tests.Unit.Support; -namespace Deal.Tests.Unit; +namespace Deal.Tests.Unit.Modules.Tenants; /// /// Юнит-тесты детектора подозрительной активности (§10.5) на фейковом хранилище аудита. diff --git a/src/core/tests/Deal.Tests.Unit/TenantAdminServiceTests.cs b/src/core/tests/Deal.Tests.Unit/Modules/Tenants/TenantAdminServiceTests.cs similarity index 99% rename from src/core/tests/Deal.Tests.Unit/TenantAdminServiceTests.cs rename to src/core/tests/Deal.Tests.Unit/Modules/Tenants/TenantAdminServiceTests.cs index 1237083..9d3f90c 100644 --- a/src/core/tests/Deal.Tests.Unit/TenantAdminServiceTests.cs +++ b/src/core/tests/Deal.Tests.Unit/Modules/Tenants/TenantAdminServiceTests.cs @@ -3,8 +3,9 @@ using Deal.Modules.Tenants.Application.Extensions; using Deal.Modules.Tenants.Application.Models; using Deal.Modules.Tenants.Application.Registrars; using Deal.Modules.Tenants.Application.Services; +using Deal.Tests.Unit.Support; -namespace Deal.Tests.Unit; +namespace Deal.Tests.Unit.Modules.Tenants; /// /// Юнит-тесты (план Task 7): create (тенант + владелец), список со diff --git a/src/core/tests/Deal.Tests.Unit/TokenBudgetServiceTests.cs b/src/core/tests/Deal.Tests.Unit/Modules/Tenants/TokenBudgetServiceTests.cs similarity index 98% rename from src/core/tests/Deal.Tests.Unit/TokenBudgetServiceTests.cs rename to src/core/tests/Deal.Tests.Unit/Modules/Tenants/TokenBudgetServiceTests.cs index 29fc2ac..23c06ef 100644 --- a/src/core/tests/Deal.Tests.Unit/TokenBudgetServiceTests.cs +++ b/src/core/tests/Deal.Tests.Unit/Modules/Tenants/TokenBudgetServiceTests.cs @@ -3,8 +3,9 @@ using Deal.Modules.Tenants.Application.Extensions; using Deal.Modules.Tenants.Application.Models; using Deal.Modules.Tenants.Application.Registrars; using Deal.Modules.Tenants.Application.Services; +using Deal.Tests.Unit.Support; -namespace Deal.Tests.Unit; +namespace Deal.Tests.Unit.Modules.Tenants; /// /// Тесты период-математики лимитов (Task 8, Ruling 3 этапа 7): конец окна месяца/дня для ленивого diff --git a/src/core/tests/Deal.Tests.Unit/TokenUsageEventServiceTests.cs b/src/core/tests/Deal.Tests.Unit/Modules/Tenants/TokenUsageEventServiceTests.cs similarity index 96% rename from src/core/tests/Deal.Tests.Unit/TokenUsageEventServiceTests.cs rename to src/core/tests/Deal.Tests.Unit/Modules/Tenants/TokenUsageEventServiceTests.cs index 25769e7..a8b5909 100644 --- a/src/core/tests/Deal.Tests.Unit/TokenUsageEventServiceTests.cs +++ b/src/core/tests/Deal.Tests.Unit/Modules/Tenants/TokenUsageEventServiceTests.cs @@ -3,8 +3,9 @@ using Deal.Modules.Tenants.Application.Extensions; using Deal.Modules.Tenants.Application.Models; using Deal.Modules.Tenants.Application.Registrars; using Deal.Modules.Tenants.Application.Services; +using Deal.Tests.Unit.Support; -namespace Deal.Tests.Unit; +namespace Deal.Tests.Unit.Modules.Tenants; /// /// Тесты сервиса истории расхода токенов (этап 10, T2): единая точка записи (At=UTC-now) и чтения агрегатов. diff --git a/src/core/tests/Deal.Tests.Unit/TenantIdTests.cs b/src/core/tests/Deal.Tests.Unit/SharedKernel/TenantIdTests.cs similarity index 95% rename from src/core/tests/Deal.Tests.Unit/TenantIdTests.cs rename to src/core/tests/Deal.Tests.Unit/SharedKernel/TenantIdTests.cs index 9ebca24..0fc22c7 100644 --- a/src/core/tests/Deal.Tests.Unit/TenantIdTests.cs +++ b/src/core/tests/Deal.Tests.Unit/SharedKernel/TenantIdTests.cs @@ -1,7 +1,8 @@ using Deal.SharedKernel.Tenants.Abstractions; using Deal.SharedKernel.Tenants.Models; +using Deal.Tests.Unit.Support; -namespace Deal.Tests.Unit; +namespace Deal.Tests.Unit.SharedKernel; public sealed class TenantIdTests { diff --git a/src/core/tests/Deal.Tests.Unit/AdminTickOrchestratorTests.cs b/src/core/tests/Deal.Tests.Unit/Support/AdminTickOrchestratorTests.cs similarity index 99% rename from src/core/tests/Deal.Tests.Unit/AdminTickOrchestratorTests.cs rename to src/core/tests/Deal.Tests.Unit/Support/AdminTickOrchestratorTests.cs index 6109054..da7a0b6 100644 --- a/src/core/tests/Deal.Tests.Unit/AdminTickOrchestratorTests.cs +++ b/src/core/tests/Deal.Tests.Unit/Support/AdminTickOrchestratorTests.cs @@ -19,8 +19,12 @@ using Deal.Modules.Settings.Application.Models; using Deal.Modules.Settings.Application.Registrars; using Deal.Modules.Settings.Application.Services; using Microsoft.Extensions.Logging.Abstractions; +using Deal.Tests.Unit.Modules.Kanban; +using Deal.Tests.Unit.Modules.Cards; +using Deal.Tests.Unit.Modules.Settings; +using Deal.Tests.Unit.Contracts; -namespace Deal.Tests.Unit; +namespace Deal.Tests.Unit.Support; /// /// Тесты AdminTickOrchestrator — состав POST /api/admin/tick (план Tasks 10–11, Ruling 8/9; dashboard_routes.py diff --git a/src/core/tests/Deal.Tests.Unit/AiConnectionCheckerTests.cs b/src/core/tests/Deal.Tests.Unit/Support/AiConnectionCheckerTests.cs similarity index 99% rename from src/core/tests/Deal.Tests.Unit/AiConnectionCheckerTests.cs rename to src/core/tests/Deal.Tests.Unit/Support/AiConnectionCheckerTests.cs index d8ae286..6adaa64 100644 --- a/src/core/tests/Deal.Tests.Unit/AiConnectionCheckerTests.cs +++ b/src/core/tests/Deal.Tests.Unit/Support/AiConnectionCheckerTests.cs @@ -7,7 +7,7 @@ using Deal.Infrastructure.Integrations.Options; using Deal.Infrastructure.Integrations.Services; using Deal.Modules.Settings.Application.Models; -namespace Deal.Tests.Unit; +namespace Deal.Tests.Unit.Support; /// /// Тесты AiConnectionChecker — ветки ответа POST /api/ai/check (Task 6, Ruling 7). diff --git a/src/core/tests/Deal.Tests.Unit/AiGrpcTestHost.cs b/src/core/tests/Deal.Tests.Unit/Support/AiGrpcTestHost.cs similarity index 96% rename from src/core/tests/Deal.Tests.Unit/AiGrpcTestHost.cs rename to src/core/tests/Deal.Tests.Unit/Support/AiGrpcTestHost.cs index 41dbe89..4a9d30a 100644 --- a/src/core/tests/Deal.Tests.Unit/AiGrpcTestHost.cs +++ b/src/core/tests/Deal.Tests.Unit/Support/AiGrpcTestHost.cs @@ -10,8 +10,9 @@ using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Server.Kestrel.Core; using Microsoft.Extensions.DependencyInjection; +using Deal.Tests.Unit.Grpc; -namespace Deal.Tests.Unit; +namespace Deal.Tests.Unit.Support; // Общий харнесс интеграционных тестов gRPC-адаптеров ядра к ai-service (план Task 15, Ruling 1/2; // эталон MlGrpcTestHost). diff --git a/src/core/tests/Deal.Tests.Unit/AiRawCardMapperTests.cs b/src/core/tests/Deal.Tests.Unit/Support/AiRawCardMapperTests.cs similarity index 99% rename from src/core/tests/Deal.Tests.Unit/AiRawCardMapperTests.cs rename to src/core/tests/Deal.Tests.Unit/Support/AiRawCardMapperTests.cs index 8fb442c..9f839e6 100644 --- a/src/core/tests/Deal.Tests.Unit/AiRawCardMapperTests.cs +++ b/src/core/tests/Deal.Tests.Unit/Support/AiRawCardMapperTests.cs @@ -5,7 +5,7 @@ using Deal.Modules.Pipeline.Application.Models; using Deal.Modules.Pipeline.Application.Registrars; using Deal.Modules.Pipeline.Application.Services; -namespace Deal.Tests.Unit; +namespace Deal.Tests.Unit.Support; /// /// Тесты строгого маппинга JSON-ответа ИИ-классификатора в разбор карточки diff --git a/src/core/tests/Deal.Tests.Unit/BudgetAlertSchedulerTests.cs b/src/core/tests/Deal.Tests.Unit/Support/BudgetAlertSchedulerTests.cs similarity index 99% rename from src/core/tests/Deal.Tests.Unit/BudgetAlertSchedulerTests.cs rename to src/core/tests/Deal.Tests.Unit/Support/BudgetAlertSchedulerTests.cs index 4ca1acb..6ce0bfc 100644 --- a/src/core/tests/Deal.Tests.Unit/BudgetAlertSchedulerTests.cs +++ b/src/core/tests/Deal.Tests.Unit/Support/BudgetAlertSchedulerTests.cs @@ -8,8 +8,9 @@ using Deal.Modules.Tenants.Application.Registrars; using Deal.Modules.Tenants.Application.Services; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging.Abstractions; +using Deal.Tests.Unit.Modules.Tenants; -namespace Deal.Tests.Unit; +namespace Deal.Tests.Unit.Support; /// /// Тесты логики прохода (план Task 9, Ruling 3): фоновая проверка diff --git a/src/core/tests/Deal.Tests.Unit/CardReclassifierTests.cs b/src/core/tests/Deal.Tests.Unit/Support/CardReclassifierTests.cs similarity index 99% rename from src/core/tests/Deal.Tests.Unit/CardReclassifierTests.cs rename to src/core/tests/Deal.Tests.Unit/Support/CardReclassifierTests.cs index 04b8bf4..55e9968 100644 --- a/src/core/tests/Deal.Tests.Unit/CardReclassifierTests.cs +++ b/src/core/tests/Deal.Tests.Unit/Support/CardReclassifierTests.cs @@ -14,8 +14,11 @@ using Deal.Modules.Settings.Application.Abstractions; using Deal.Modules.Settings.Application.Models; using Deal.Modules.Settings.Application.Registrars; using Deal.Modules.Settings.Application.Services; +using Deal.Tests.Unit.Modules.Cards; +using Deal.Tests.Unit.Modules.Settings; +using Deal.Tests.Unit.Contracts; -namespace Deal.Tests.Unit; +namespace Deal.Tests.Unit.Support; /// /// Тесты ручной переклассификации карточек — CardReclassifier (этап 12, пакет D; diff --git a/src/core/tests/Deal.Tests.Unit/CardsServiceFilesTests.cs b/src/core/tests/Deal.Tests.Unit/Support/CardsServiceFilesTests.cs similarity index 98% rename from src/core/tests/Deal.Tests.Unit/CardsServiceFilesTests.cs rename to src/core/tests/Deal.Tests.Unit/Support/CardsServiceFilesTests.cs index b83ae8d..442d004 100644 --- a/src/core/tests/Deal.Tests.Unit/CardsServiceFilesTests.cs +++ b/src/core/tests/Deal.Tests.Unit/Support/CardsServiceFilesTests.cs @@ -4,8 +4,11 @@ using Deal.Modules.Kanban.Application.Extensions; using Deal.Modules.Kanban.Application.Models; using Deal.Modules.Kanban.Application.Registrars; using Deal.Modules.Kanban.Application.Services; +using Deal.Tests.Unit.Modules.Cards; +using Deal.Tests.Unit.Contracts; +using Deal.Tests.Unit.Modules.Settings; -namespace Deal.Tests.Unit; +namespace Deal.Tests.Unit.Support; /// /// Тесты вложений карточки — (единый домен карточки, этап 9): add (детект diff --git a/src/core/tests/Deal.Tests.Unit/CardsServiceSelectedTests.cs b/src/core/tests/Deal.Tests.Unit/Support/CardsServiceSelectedTests.cs similarity index 99% rename from src/core/tests/Deal.Tests.Unit/CardsServiceSelectedTests.cs rename to src/core/tests/Deal.Tests.Unit/Support/CardsServiceSelectedTests.cs index 620e47b..ad36acb 100644 --- a/src/core/tests/Deal.Tests.Unit/CardsServiceSelectedTests.cs +++ b/src/core/tests/Deal.Tests.Unit/Support/CardsServiceSelectedTests.cs @@ -7,8 +7,11 @@ using Deal.Modules.Kanban.Application.Extensions; using Deal.Modules.Kanban.Application.Models; using Deal.Modules.Kanban.Application.Registrars; using Deal.Modules.Kanban.Application.Services; +using Deal.Tests.Unit.Modules.Cards; +using Deal.Tests.Unit.Modules.Settings; +using Deal.Tests.Unit.Contracts; -namespace Deal.Tests.Unit; +namespace Deal.Tests.Unit.Support; /// /// Тесты операций карточек пространства «Выбранные» — (единый домен diff --git a/src/core/tests/Deal.Tests.Unit/CbrRateSourceTests.cs b/src/core/tests/Deal.Tests.Unit/Support/CbrRateSourceTests.cs similarity index 99% rename from src/core/tests/Deal.Tests.Unit/CbrRateSourceTests.cs rename to src/core/tests/Deal.Tests.Unit/Support/CbrRateSourceTests.cs index b4307b3..0b91879 100644 --- a/src/core/tests/Deal.Tests.Unit/CbrRateSourceTests.cs +++ b/src/core/tests/Deal.Tests.Unit/Support/CbrRateSourceTests.cs @@ -8,7 +8,7 @@ using Deal.Infrastructure.Integrations.Options; using Deal.Infrastructure.Integrations.Services; using Microsoft.Extensions.Logging.Abstractions; -namespace Deal.Tests.Unit; +namespace Deal.Tests.Unit.Support; /// /// Тесты CbrRateSource — парсинг daily_json.js и ветки сбоя (Task 8, Ruling 6; rates.py L43–59). diff --git a/src/core/tests/Deal.Tests.Unit/ColumnRulesNewGroupsTests.cs b/src/core/tests/Deal.Tests.Unit/Support/ColumnRulesNewGroupsTests.cs similarity index 97% rename from src/core/tests/Deal.Tests.Unit/ColumnRulesNewGroupsTests.cs rename to src/core/tests/Deal.Tests.Unit/Support/ColumnRulesNewGroupsTests.cs index 1bc9f37..c33a178 100644 --- a/src/core/tests/Deal.Tests.Unit/ColumnRulesNewGroupsTests.cs +++ b/src/core/tests/Deal.Tests.Unit/Support/ColumnRulesNewGroupsTests.cs @@ -2,7 +2,7 @@ using System.Text.Json; using Deal.Modules.Kanban.Application.ColumnRules; using Deal.Modules.Kanban.Application.Models; -namespace Deal.Tests.Unit; +namespace Deal.Tests.Unit.Support; /// /// Тесты новых групп фильтров колонки (§6.3, этап 12): levels/locations/types/prices. diff --git a/src/core/tests/Deal.Tests.Unit/ContainersServiceTests.cs b/src/core/tests/Deal.Tests.Unit/Support/ContainersServiceTests.cs similarity index 99% rename from src/core/tests/Deal.Tests.Unit/ContainersServiceTests.cs rename to src/core/tests/Deal.Tests.Unit/Support/ContainersServiceTests.cs index ed05da8..83649aa 100644 --- a/src/core/tests/Deal.Tests.Unit/ContainersServiceTests.cs +++ b/src/core/tests/Deal.Tests.Unit/Support/ContainersServiceTests.cs @@ -4,8 +4,10 @@ using Deal.Modules.Kanban.Application.Extensions; using Deal.Modules.Kanban.Application.Models; using Deal.Modules.Kanban.Application.Registrars; using Deal.Modules.Kanban.Application.Services; +using Deal.Tests.Unit.Modules.Cards; +using Deal.Tests.Unit.Modules.Settings; -namespace Deal.Tests.Unit; +namespace Deal.Tests.Unit.Support; /// /// Тесты ContainersService — единый реестр контейнеров и colState (этап 9, T4). diff --git a/src/core/tests/Deal.Tests.Unit/ConversionRecomputerTests.cs b/src/core/tests/Deal.Tests.Unit/Support/ConversionRecomputerTests.cs similarity index 99% rename from src/core/tests/Deal.Tests.Unit/ConversionRecomputerTests.cs rename to src/core/tests/Deal.Tests.Unit/Support/ConversionRecomputerTests.cs index b6539bd..52e46f4 100644 --- a/src/core/tests/Deal.Tests.Unit/ConversionRecomputerTests.cs +++ b/src/core/tests/Deal.Tests.Unit/Support/ConversionRecomputerTests.cs @@ -8,8 +8,10 @@ using Deal.Modules.Settings.Application.Abstractions; using Deal.Modules.Settings.Application.Models; using Deal.Modules.Settings.Application.Registrars; using Deal.Modules.Settings.Application.Services; +using Deal.Tests.Unit.Modules.Cards; +using Deal.Tests.Unit.Modules.Settings; -namespace Deal.Tests.Unit; +namespace Deal.Tests.Unit.Support; /// /// Тесты ConversionRecomputer — пересчёт конверсий карточек (Ruling 7, план Task 12 L424–431). diff --git a/src/core/tests/Deal.Tests.Unit/FakeSecretCipher.cs b/src/core/tests/Deal.Tests.Unit/Support/FakeSecretCipher.cs similarity index 98% rename from src/core/tests/Deal.Tests.Unit/FakeSecretCipher.cs rename to src/core/tests/Deal.Tests.Unit/Support/FakeSecretCipher.cs index 255a9f2..0adf2f1 100644 --- a/src/core/tests/Deal.Tests.Unit/FakeSecretCipher.cs +++ b/src/core/tests/Deal.Tests.Unit/Support/FakeSecretCipher.cs @@ -4,7 +4,7 @@ using Deal.Modules.Settings.Application.Models; using Deal.Modules.Settings.Application.Registrars; using Deal.Modules.Settings.Application.Services; -namespace Deal.Tests.Unit; +namespace Deal.Tests.Unit.Support; /// /// Детерминированный шифр для тестов SettingsService: «enc:» + Base64(UTF-8 текст). diff --git a/src/core/tests/Deal.Tests.Unit/FakeTelegramDialogRow.cs b/src/core/tests/Deal.Tests.Unit/Support/FakeTelegramDialogRow.cs similarity index 97% rename from src/core/tests/Deal.Tests.Unit/FakeTelegramDialogRow.cs rename to src/core/tests/Deal.Tests.Unit/Support/FakeTelegramDialogRow.cs index 940d261..fa9e7b0 100644 --- a/src/core/tests/Deal.Tests.Unit/FakeTelegramDialogRow.cs +++ b/src/core/tests/Deal.Tests.Unit/Support/FakeTelegramDialogRow.cs @@ -1,4 +1,4 @@ -namespace Deal.Tests.Unit; +namespace Deal.Tests.Unit.Support; /// /// Срез строки каталога Dialogs фейка (зеркало DialogEntity). diff --git a/src/core/tests/Deal.Tests.Unit/FakeTelegramMessageRow.cs b/src/core/tests/Deal.Tests.Unit/Support/FakeTelegramMessageRow.cs similarity index 95% rename from src/core/tests/Deal.Tests.Unit/FakeTelegramMessageRow.cs rename to src/core/tests/Deal.Tests.Unit/Support/FakeTelegramMessageRow.cs index 2ecfa51..d38ede1 100644 --- a/src/core/tests/Deal.Tests.Unit/FakeTelegramMessageRow.cs +++ b/src/core/tests/Deal.Tests.Unit/Support/FakeTelegramMessageRow.cs @@ -1,4 +1,4 @@ -namespace Deal.Tests.Unit; +namespace Deal.Tests.Unit.Support; /// /// Срез строки превью TgMessages фейка (зеркало TgMessageEntity). diff --git a/src/core/tests/Deal.Tests.Unit/ForwardedHeadersHttpTests.cs b/src/core/tests/Deal.Tests.Unit/Support/ForwardedHeadersHttpTests.cs similarity index 97% rename from src/core/tests/Deal.Tests.Unit/ForwardedHeadersHttpTests.cs rename to src/core/tests/Deal.Tests.Unit/Support/ForwardedHeadersHttpTests.cs index 468389a..3e22512 100644 --- a/src/core/tests/Deal.Tests.Unit/ForwardedHeadersHttpTests.cs +++ b/src/core/tests/Deal.Tests.Unit/Support/ForwardedHeadersHttpTests.cs @@ -6,7 +6,7 @@ using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.HttpOverrides; -namespace Deal.Tests.Unit; +namespace Deal.Tests.Unit.Support; /// /// Тесты обработки прокси-заголовков (план Task 12; замечание ревью T4/T11): за Caddy (compose-prod, diff --git a/src/core/tests/Deal.Tests.Unit/GrpcAiClassifierTests.cs b/src/core/tests/Deal.Tests.Unit/Support/GrpcAiClassifierTests.cs similarity index 99% rename from src/core/tests/Deal.Tests.Unit/GrpcAiClassifierTests.cs rename to src/core/tests/Deal.Tests.Unit/Support/GrpcAiClassifierTests.cs index 9220900..fef85d4 100644 --- a/src/core/tests/Deal.Tests.Unit/GrpcAiClassifierTests.cs +++ b/src/core/tests/Deal.Tests.Unit/Support/GrpcAiClassifierTests.cs @@ -27,8 +27,12 @@ using Deal.Modules.Tenants.Application.Services; using Deal.SharedKernel.Tenants.Abstractions; using Deal.SharedKernel.Tenants.Models; using Microsoft.Extensions.Logging.Abstractions; +using Deal.Tests.Unit.Modules.Settings; +using Deal.Tests.Unit.Modules.Cards; +using Deal.Tests.Unit.Modules.Tenants; +using Deal.Tests.Unit.Grpc; -namespace Deal.Tests.Unit; +namespace Deal.Tests.Unit.Support; /// /// Тесты gRPC-адаптера IAiClassifier к ai-service (план Task 15, Ruling 5/6; Acceptance L433–434). diff --git a/src/core/tests/Deal.Tests.Unit/IncomingRulesTests.cs b/src/core/tests/Deal.Tests.Unit/Support/IncomingRulesTests.cs similarity index 99% rename from src/core/tests/Deal.Tests.Unit/IncomingRulesTests.cs rename to src/core/tests/Deal.Tests.Unit/Support/IncomingRulesTests.cs index 84df39e..1cdb2bb 100644 --- a/src/core/tests/Deal.Tests.Unit/IncomingRulesTests.cs +++ b/src/core/tests/Deal.Tests.Unit/Support/IncomingRulesTests.cs @@ -3,8 +3,9 @@ using Deal.Modules.Settings.Application.Abstractions; using Deal.Modules.Settings.Application.Models; using Deal.Modules.Settings.Application.Registrars; using Deal.Modules.Settings.Application.Services; +using Deal.Tests.Unit.Modules.Settings; -namespace Deal.Tests.Unit; +namespace Deal.Tests.Unit.Support; /// /// Тесты этап-1 правил фильтра входящих — IncomingRules (план Task 10 L370–383, pipeline.py L94–124). diff --git a/src/core/tests/Deal.Tests.Unit/JoinEndpointHttpTests.cs b/src/core/tests/Deal.Tests.Unit/Support/JoinEndpointHttpTests.cs similarity index 99% rename from src/core/tests/Deal.Tests.Unit/JoinEndpointHttpTests.cs rename to src/core/tests/Deal.Tests.Unit/Support/JoinEndpointHttpTests.cs index de92987..503ac35 100644 --- a/src/core/tests/Deal.Tests.Unit/JoinEndpointHttpTests.cs +++ b/src/core/tests/Deal.Tests.Unit/Support/JoinEndpointHttpTests.cs @@ -10,8 +10,9 @@ using Deal.Modules.Tenants.Application.Services; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; using Microsoft.Extensions.DependencyInjection; +using Deal.Tests.Unit.Modules.Tenants; -namespace Deal.Tests.Unit; +namespace Deal.Tests.Unit.Support; /// /// HTTP-тесты POST /api/join (Task 6, Ruling 2/11): эквивалент curl-сценария активации на in-process Kestrel. diff --git a/src/core/tests/Deal.Tests.Unit/LocalColumnSuggesterTests.cs b/src/core/tests/Deal.Tests.Unit/Support/LocalColumnSuggesterTests.cs similarity index 99% rename from src/core/tests/Deal.Tests.Unit/LocalColumnSuggesterTests.cs rename to src/core/tests/Deal.Tests.Unit/Support/LocalColumnSuggesterTests.cs index 5d2087e..d1f4373 100644 --- a/src/core/tests/Deal.Tests.Unit/LocalColumnSuggesterTests.cs +++ b/src/core/tests/Deal.Tests.Unit/Support/LocalColumnSuggesterTests.cs @@ -16,8 +16,10 @@ using Deal.Modules.Settings.Application.Abstractions; using Deal.Modules.Settings.Application.Models; using Deal.Modules.Settings.Application.Registrars; using Deal.Modules.Settings.Application.Services; +using Deal.Tests.Unit.Modules.Cards; +using Deal.Tests.Unit.Modules.Settings; -namespace Deal.Tests.Unit; +namespace Deal.Tests.Unit.Support; /// /// Тесты адаптера ИИ-предложений (Ruling 3, план Task 14 L472–475). diff --git a/src/core/tests/Deal.Tests.Unit/LocalFileStorageTests.cs b/src/core/tests/Deal.Tests.Unit/Support/LocalFileStorageTests.cs similarity index 99% rename from src/core/tests/Deal.Tests.Unit/LocalFileStorageTests.cs rename to src/core/tests/Deal.Tests.Unit/Support/LocalFileStorageTests.cs index a48e54a..da06a73 100644 --- a/src/core/tests/Deal.Tests.Unit/LocalFileStorageTests.cs +++ b/src/core/tests/Deal.Tests.Unit/Support/LocalFileStorageTests.cs @@ -4,7 +4,7 @@ using Deal.Infrastructure.Integrations.Storage.Extensions; using Deal.Infrastructure.Integrations.Storage.Options; using Deal.Infrastructure.Integrations.Storage.Services; -namespace Deal.Tests.Unit; +namespace Deal.Tests.Unit.Support; /// /// Тесты LocalFileStorage — put/get/delete round-trip во временном каталоге (план Task 6 L334–335; object_store.py L54–108; Ruling 4). diff --git a/src/core/tests/Deal.Tests.Unit/LocalMlClientTests.cs b/src/core/tests/Deal.Tests.Unit/Support/LocalMlClientTests.cs similarity index 99% rename from src/core/tests/Deal.Tests.Unit/LocalMlClientTests.cs rename to src/core/tests/Deal.Tests.Unit/Support/LocalMlClientTests.cs index d810922..d02a4f1 100644 --- a/src/core/tests/Deal.Tests.Unit/LocalMlClientTests.cs +++ b/src/core/tests/Deal.Tests.Unit/Support/LocalMlClientTests.cs @@ -15,8 +15,10 @@ using Deal.Modules.Settings.Application.Abstractions; using Deal.Modules.Settings.Application.Models; using Deal.Modules.Settings.Application.Registrars; using Deal.Modules.Settings.Application.Services; +using Deal.Tests.Unit.Modules.Settings; +using Deal.Tests.Unit.Modules.Kanban; -namespace Deal.Tests.Unit; +namespace Deal.Tests.Unit.Support; /// /// Локальная реализация IMlClient без внешнего ML-сервиса (Ruling 4/5, план Task 5 L266–286). diff --git a/src/core/tests/Deal.Tests.Unit/LoginAttemptEndpointHttpTests.cs b/src/core/tests/Deal.Tests.Unit/Support/LoginAttemptEndpointHttpTests.cs similarity index 96% rename from src/core/tests/Deal.Tests.Unit/LoginAttemptEndpointHttpTests.cs rename to src/core/tests/Deal.Tests.Unit/Support/LoginAttemptEndpointHttpTests.cs index 9d1dcaf..75d2599 100644 --- a/src/core/tests/Deal.Tests.Unit/LoginAttemptEndpointHttpTests.cs +++ b/src/core/tests/Deal.Tests.Unit/Support/LoginAttemptEndpointHttpTests.cs @@ -6,8 +6,9 @@ using Deal.Api.Extensions; using Deal.Api.Models; using Deal.Api.Services; using Deal.Modules.Tenants.Application.Models; +using Deal.Tests.Unit.Modules.Tenants; -namespace Deal.Tests.Unit; +namespace Deal.Tests.Unit.Support; /// /// HTTP-тесты защиты входа на ручке POST /api/auth/login (план Task 11, Ruling 5): эндпоинт вызывает diff --git a/src/core/tests/Deal.Tests.Unit/MessageParseCoreTests.cs b/src/core/tests/Deal.Tests.Unit/Support/MessageParseCoreTests.cs similarity index 99% rename from src/core/tests/Deal.Tests.Unit/MessageParseCoreTests.cs rename to src/core/tests/Deal.Tests.Unit/Support/MessageParseCoreTests.cs index 2f6e230..c6ce45d 100644 --- a/src/core/tests/Deal.Tests.Unit/MessageParseCoreTests.cs +++ b/src/core/tests/Deal.Tests.Unit/Support/MessageParseCoreTests.cs @@ -6,8 +6,9 @@ using Deal.Modules.Settings.Application.Abstractions; using Deal.Modules.Settings.Application.Models; using Deal.Modules.Settings.Application.Registrars; using Deal.Modules.Settings.Application.Services; +using Deal.Tests.Unit.Modules.Settings; -namespace Deal.Tests.Unit; +namespace Deal.Tests.Unit.Support; /// /// Тесты чистого ядра разбора сообщения — MessageParseCore (план Task 4 L327–349, Ruling 7; diff --git a/src/core/tests/Deal.Tests.Unit/MlGrpcTestHost.cs b/src/core/tests/Deal.Tests.Unit/Support/MlGrpcTestHost.cs similarity index 96% rename from src/core/tests/Deal.Tests.Unit/MlGrpcTestHost.cs rename to src/core/tests/Deal.Tests.Unit/Support/MlGrpcTestHost.cs index 740789e..20184cd 100644 --- a/src/core/tests/Deal.Tests.Unit/MlGrpcTestHost.cs +++ b/src/core/tests/Deal.Tests.Unit/Support/MlGrpcTestHost.cs @@ -4,8 +4,9 @@ using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Server.Kestrel.Core; using Microsoft.Extensions.DependencyInjection; +using Deal.Tests.Unit.Grpc; -namespace Deal.Tests.Unit; +namespace Deal.Tests.Unit.Support; // Общий харнесс интеграционных тестов gRPC-клиента ядра к ml-service (план Task 16, Ruling 1/2). // Поднимает в процессе теста Kestrel HTTP/2 (plaintext — Ruling 2) на эфемерном порту с фейком diff --git a/src/core/tests/Deal.Tests.Unit/MlGrpcTestsCollection.cs b/src/core/tests/Deal.Tests.Unit/Support/MlGrpcTestsCollection.cs similarity index 92% rename from src/core/tests/Deal.Tests.Unit/MlGrpcTestsCollection.cs rename to src/core/tests/Deal.Tests.Unit/Support/MlGrpcTestsCollection.cs index 9db8264..6521716 100644 --- a/src/core/tests/Deal.Tests.Unit/MlGrpcTestsCollection.cs +++ b/src/core/tests/Deal.Tests.Unit/Support/MlGrpcTestsCollection.cs @@ -1,4 +1,4 @@ -namespace Deal.Tests.Unit; +namespace Deal.Tests.Unit.Support; /// /// Коллекция тестов gRPC-клиента ml-service (план Task 16): сериализует сценарии, меняющие env diff --git a/src/core/tests/Deal.Tests.Unit/MtlsCertificatesTests.cs b/src/core/tests/Deal.Tests.Unit/Support/MtlsCertificatesTests.cs similarity index 97% rename from src/core/tests/Deal.Tests.Unit/MtlsCertificatesTests.cs rename to src/core/tests/Deal.Tests.Unit/Support/MtlsCertificatesTests.cs index e769ad1..1a4dbc5 100644 --- a/src/core/tests/Deal.Tests.Unit/MtlsCertificatesTests.cs +++ b/src/core/tests/Deal.Tests.Unit/Support/MtlsCertificatesTests.cs @@ -8,7 +8,7 @@ using Deal.Infrastructure.Integrations.Models; using Deal.Infrastructure.Integrations.Options; using Deal.Infrastructure.Integrations.Services; -namespace Deal.Tests.Unit; +namespace Deal.Tests.Unit.Support; /// /// Тесты загрузки/проверки сертификатов mTLS (план Task 13, Ruling 6): выбор режима (флаг → null, diff --git a/src/core/tests/Deal.Tests.Unit/OperatorAnalyticsEndpointsHttpTests.cs b/src/core/tests/Deal.Tests.Unit/Support/OperatorAnalyticsEndpointsHttpTests.cs similarity index 99% rename from src/core/tests/Deal.Tests.Unit/OperatorAnalyticsEndpointsHttpTests.cs rename to src/core/tests/Deal.Tests.Unit/Support/OperatorAnalyticsEndpointsHttpTests.cs index 365c969..b08f8ca 100644 --- a/src/core/tests/Deal.Tests.Unit/OperatorAnalyticsEndpointsHttpTests.cs +++ b/src/core/tests/Deal.Tests.Unit/Support/OperatorAnalyticsEndpointsHttpTests.cs @@ -6,8 +6,9 @@ using Deal.Modules.Tenants.Application.Extensions; using Deal.Modules.Tenants.Application.Models; using Deal.Modules.Tenants.Application.Registrars; using Deal.Modules.Tenants.Application.Services; +using Deal.Tests.Unit.Modules.Tenants; -namespace Deal.Tests.Unit; +namespace Deal.Tests.Unit.Support; /// /// HTTP-тесты операторской аналитики (этап 10, T3): /api/operator/analytics/{overview,tokens,activity} diff --git a/src/core/tests/Deal.Tests.Unit/OperatorAuditEndpointsHttpTests.cs b/src/core/tests/Deal.Tests.Unit/Support/OperatorAuditEndpointsHttpTests.cs similarity index 99% rename from src/core/tests/Deal.Tests.Unit/OperatorAuditEndpointsHttpTests.cs rename to src/core/tests/Deal.Tests.Unit/Support/OperatorAuditEndpointsHttpTests.cs index be907eb..4437eec 100644 --- a/src/core/tests/Deal.Tests.Unit/OperatorAuditEndpointsHttpTests.cs +++ b/src/core/tests/Deal.Tests.Unit/Support/OperatorAuditEndpointsHttpTests.cs @@ -6,8 +6,9 @@ using Deal.Modules.Tenants.Application.Extensions; using Deal.Modules.Tenants.Application.Models; using Deal.Modules.Tenants.Application.Registrars; using Deal.Modules.Tenants.Application.Services; +using Deal.Tests.Unit.Modules.Tenants; -namespace Deal.Tests.Unit; +namespace Deal.Tests.Unit.Support; /// /// HTTP-тесты аудита (Task 4, Ruling 4): запись событий входов из login-эндпоинтов и чтение ленты diff --git a/src/core/tests/Deal.Tests.Unit/OperatorAuthEndpointsHttpTests.cs b/src/core/tests/Deal.Tests.Unit/Support/OperatorAuthEndpointsHttpTests.cs similarity index 99% rename from src/core/tests/Deal.Tests.Unit/OperatorAuthEndpointsHttpTests.cs rename to src/core/tests/Deal.Tests.Unit/Support/OperatorAuthEndpointsHttpTests.cs index 4a1db49..7dc483a 100644 --- a/src/core/tests/Deal.Tests.Unit/OperatorAuthEndpointsHttpTests.cs +++ b/src/core/tests/Deal.Tests.Unit/Support/OperatorAuthEndpointsHttpTests.cs @@ -9,8 +9,9 @@ using Deal.Modules.Tenants.Application.Extensions; using Deal.Modules.Tenants.Application.Models; using Deal.Modules.Tenants.Application.Registrars; using Deal.Modules.Tenants.Application.Services; +using Deal.Tests.Unit.Modules.Tenants; -namespace Deal.Tests.Unit; +namespace Deal.Tests.Unit.Support; /// /// HTTP-тесты операторского auth-контура (Task 3): OperatorSessionMiddleware + OperatorAuthEndpoints diff --git a/src/core/tests/Deal.Tests.Unit/OperatorAuthHttpHost.cs b/src/core/tests/Deal.Tests.Unit/Support/OperatorAuthHttpHost.cs similarity index 98% rename from src/core/tests/Deal.Tests.Unit/OperatorAuthHttpHost.cs rename to src/core/tests/Deal.Tests.Unit/Support/OperatorAuthHttpHost.cs index 8ced108..41dd599 100644 --- a/src/core/tests/Deal.Tests.Unit/OperatorAuthHttpHost.cs +++ b/src/core/tests/Deal.Tests.Unit/Support/OperatorAuthHttpHost.cs @@ -31,10 +31,12 @@ using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; +using Deal.Tests.Unit.Modules.Tenants; +using Deal.Tests.Unit.Modules.Settings; using OperatorCookieOptions = Deal.Api.Configuration.OperatorCookieOptions; using TenantCookieOptions = Deal.Api.Configuration.CookieOptions; -namespace Deal.Tests.Unit; +namespace Deal.Tests.Unit.Support; // Харнесс HTTP-тестов операторского контура (Task 3): in-process Kestrel с SessionMiddleware, // OperatorSessionMiddleware и обеими auth-группами (/api/auth и /api/operator/auth) на фейк-хранилищах. diff --git a/src/core/tests/Deal.Tests.Unit/OperatorHealthEndpointsHttpTests.cs b/src/core/tests/Deal.Tests.Unit/Support/OperatorHealthEndpointsHttpTests.cs similarity index 99% rename from src/core/tests/Deal.Tests.Unit/OperatorHealthEndpointsHttpTests.cs rename to src/core/tests/Deal.Tests.Unit/Support/OperatorHealthEndpointsHttpTests.cs index 19ecd2a..f22e1dd 100644 --- a/src/core/tests/Deal.Tests.Unit/OperatorHealthEndpointsHttpTests.cs +++ b/src/core/tests/Deal.Tests.Unit/Support/OperatorHealthEndpointsHttpTests.cs @@ -6,8 +6,9 @@ using Deal.Modules.Tenants.Application.Extensions; using Deal.Modules.Tenants.Application.Models; using Deal.Modules.Tenants.Application.Registrars; using Deal.Modules.Tenants.Application.Services; +using Deal.Tests.Unit.Modules.Tenants; -namespace Deal.Tests.Unit; +namespace Deal.Tests.Unit.Support; /// /// HTTP-тесты операторского health: GET /api/operator/health (план Task 10, Ruling 3/6/9/11). diff --git a/src/core/tests/Deal.Tests.Unit/OperatorInvitesEndpointsHttpTests.cs b/src/core/tests/Deal.Tests.Unit/Support/OperatorInvitesEndpointsHttpTests.cs similarity index 99% rename from src/core/tests/Deal.Tests.Unit/OperatorInvitesEndpointsHttpTests.cs rename to src/core/tests/Deal.Tests.Unit/Support/OperatorInvitesEndpointsHttpTests.cs index b29f27e..93823df 100644 --- a/src/core/tests/Deal.Tests.Unit/OperatorInvitesEndpointsHttpTests.cs +++ b/src/core/tests/Deal.Tests.Unit/Support/OperatorInvitesEndpointsHttpTests.cs @@ -6,8 +6,9 @@ using Deal.Modules.Tenants.Application.Extensions; using Deal.Modules.Tenants.Application.Models; using Deal.Modules.Tenants.Application.Registrars; using Deal.Modules.Tenants.Application.Services; +using Deal.Tests.Unit.Modules.Tenants; -namespace Deal.Tests.Unit; +namespace Deal.Tests.Unit.Support; /// /// HTTP-тесты операторских ручек приглашений (Task 5, Ruling 2/11): create → list → revoke и 401 без оператора. diff --git a/src/core/tests/Deal.Tests.Unit/OperatorLimitsEndpointsHttpTests.cs b/src/core/tests/Deal.Tests.Unit/Support/OperatorLimitsEndpointsHttpTests.cs similarity index 99% rename from src/core/tests/Deal.Tests.Unit/OperatorLimitsEndpointsHttpTests.cs rename to src/core/tests/Deal.Tests.Unit/Support/OperatorLimitsEndpointsHttpTests.cs index 0a277ba..aedf562 100644 --- a/src/core/tests/Deal.Tests.Unit/OperatorLimitsEndpointsHttpTests.cs +++ b/src/core/tests/Deal.Tests.Unit/Support/OperatorLimitsEndpointsHttpTests.cs @@ -6,8 +6,9 @@ using Deal.Modules.Tenants.Application.Extensions; using Deal.Modules.Tenants.Application.Models; using Deal.Modules.Tenants.Application.Registrars; using Deal.Modules.Tenants.Application.Services; +using Deal.Tests.Unit.Modules.Tenants; -namespace Deal.Tests.Unit; +namespace Deal.Tests.Unit.Support; /// /// HTTP-тесты операторских ручек лимитов ИИ-бюджета (план Task 10, Ruling 3/4/11): сводка по всем diff --git a/src/core/tests/Deal.Tests.Unit/OperatorMaintenanceEndpointsHttpTests.cs b/src/core/tests/Deal.Tests.Unit/Support/OperatorMaintenanceEndpointsHttpTests.cs similarity index 98% rename from src/core/tests/Deal.Tests.Unit/OperatorMaintenanceEndpointsHttpTests.cs rename to src/core/tests/Deal.Tests.Unit/Support/OperatorMaintenanceEndpointsHttpTests.cs index 6df90a2..19eb9dd 100644 --- a/src/core/tests/Deal.Tests.Unit/OperatorMaintenanceEndpointsHttpTests.cs +++ b/src/core/tests/Deal.Tests.Unit/Support/OperatorMaintenanceEndpointsHttpTests.cs @@ -6,8 +6,9 @@ using Deal.Modules.Tenants.Application.Extensions; using Deal.Modules.Tenants.Application.Models; using Deal.Modules.Tenants.Application.Registrars; using Deal.Modules.Tenants.Application.Services; +using Deal.Tests.Unit.Modules.Tenants; -namespace Deal.Tests.Unit; +namespace Deal.Tests.Unit.Support; /// /// HTTP-тесты операторской maintenance-ручки пакетной миграции схем тенантов diff --git a/src/core/tests/Deal.Tests.Unit/OperatorSettingsEndpointsHttpTests.cs b/src/core/tests/Deal.Tests.Unit/Support/OperatorSettingsEndpointsHttpTests.cs similarity index 99% rename from src/core/tests/Deal.Tests.Unit/OperatorSettingsEndpointsHttpTests.cs rename to src/core/tests/Deal.Tests.Unit/Support/OperatorSettingsEndpointsHttpTests.cs index 7ee8d92..81c058e 100644 --- a/src/core/tests/Deal.Tests.Unit/OperatorSettingsEndpointsHttpTests.cs +++ b/src/core/tests/Deal.Tests.Unit/Support/OperatorSettingsEndpointsHttpTests.cs @@ -10,8 +10,10 @@ using Deal.Modules.Tenants.Application.Extensions; using Deal.Modules.Tenants.Application.Models; using Deal.Modules.Tenants.Application.Registrars; using Deal.Modules.Tenants.Application.Services; +using Deal.Tests.Unit.Modules.Tenants; +using Deal.Tests.Unit.Modules.Settings; -namespace Deal.Tests.Unit; +namespace Deal.Tests.Unit.Support; /// /// HTTP-тесты операторских ручек глобальных настроек Telegram (ТЗ §4.1/§8.1): diff --git a/src/core/tests/Deal.Tests.Unit/OperatorTenantsEndpointsHttpTests.cs b/src/core/tests/Deal.Tests.Unit/Support/OperatorTenantsEndpointsHttpTests.cs similarity index 99% rename from src/core/tests/Deal.Tests.Unit/OperatorTenantsEndpointsHttpTests.cs rename to src/core/tests/Deal.Tests.Unit/Support/OperatorTenantsEndpointsHttpTests.cs index 43ccb22..478d60f 100644 --- a/src/core/tests/Deal.Tests.Unit/OperatorTenantsEndpointsHttpTests.cs +++ b/src/core/tests/Deal.Tests.Unit/Support/OperatorTenantsEndpointsHttpTests.cs @@ -6,8 +6,9 @@ using Deal.Modules.Tenants.Application.Extensions; using Deal.Modules.Tenants.Application.Models; using Deal.Modules.Tenants.Application.Registrars; using Deal.Modules.Tenants.Application.Services; +using Deal.Tests.Unit.Modules.Tenants; -namespace Deal.Tests.Unit; +namespace Deal.Tests.Unit.Support; /// /// HTTP-тесты операторских ручек тенантов (план Task 7, Ruling 1/4/10/11): эквивалент curl-минимума diff --git a/src/core/tests/Deal.Tests.Unit/OriginGuardHttpTests.cs b/src/core/tests/Deal.Tests.Unit/Support/OriginGuardHttpTests.cs similarity index 97% rename from src/core/tests/Deal.Tests.Unit/OriginGuardHttpTests.cs rename to src/core/tests/Deal.Tests.Unit/Support/OriginGuardHttpTests.cs index 60f1b60..6cdfd02 100644 --- a/src/core/tests/Deal.Tests.Unit/OriginGuardHttpTests.cs +++ b/src/core/tests/Deal.Tests.Unit/Support/OriginGuardHttpTests.cs @@ -7,7 +7,7 @@ using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.DependencyInjection; -namespace Deal.Tests.Unit; +namespace Deal.Tests.Unit.Support; /// /// HTTP-тесты Origin-проверки мутаций (план Task 12, Ruling 10(2)): не-GET/HEAD/OPTIONS запросы /api diff --git a/src/core/tests/Deal.Tests.Unit/PipelineWorkerSchedulerTests.cs b/src/core/tests/Deal.Tests.Unit/Support/PipelineWorkerSchedulerTests.cs similarity index 98% rename from src/core/tests/Deal.Tests.Unit/PipelineWorkerSchedulerTests.cs rename to src/core/tests/Deal.Tests.Unit/Support/PipelineWorkerSchedulerTests.cs index c91a3ac..e371e60 100644 --- a/src/core/tests/Deal.Tests.Unit/PipelineWorkerSchedulerTests.cs +++ b/src/core/tests/Deal.Tests.Unit/Support/PipelineWorkerSchedulerTests.cs @@ -29,8 +29,13 @@ using Deal.SharedKernel.Tenants.Abstractions; using Deal.SharedKernel.Tenants.Models; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; +using Deal.Tests.Unit.Modules.Kanban; +using Deal.Tests.Unit.Modules.Cards; +using Deal.Tests.Unit.Modules.Tenants; +using Deal.Tests.Unit.Modules.Settings; +using Deal.Tests.Unit.Contracts; -namespace Deal.Tests.Unit; +namespace Deal.Tests.Unit.Support; /// /// Тесты PipelineWorkerScheduler — фоновый цикл разбора очереди входящих (план Task 11, Ruling 8; diff --git a/src/core/tests/Deal.Tests.Unit/PipelineWorkerServiceTests.cs b/src/core/tests/Deal.Tests.Unit/Support/PipelineWorkerServiceTests.cs similarity index 99% rename from src/core/tests/Deal.Tests.Unit/PipelineWorkerServiceTests.cs rename to src/core/tests/Deal.Tests.Unit/Support/PipelineWorkerServiceTests.cs index f2b6ee8..2ca45ac 100644 --- a/src/core/tests/Deal.Tests.Unit/PipelineWorkerServiceTests.cs +++ b/src/core/tests/Deal.Tests.Unit/Support/PipelineWorkerServiceTests.cs @@ -14,8 +14,12 @@ using Deal.Modules.Settings.Application.Abstractions; using Deal.Modules.Settings.Application.Models; using Deal.Modules.Settings.Application.Registrars; using Deal.Modules.Settings.Application.Services; +using Deal.Tests.Unit.Modules.Kanban; +using Deal.Tests.Unit.Modules.Cards; +using Deal.Tests.Unit.Modules.Settings; +using Deal.Tests.Unit.Contracts; -namespace Deal.Tests.Unit; +namespace Deal.Tests.Unit.Support; /// /// Тесты воркера pump — PipelineWorkerService (план Task 8 L423–431, Ruling 8; прототип _pump_unlocked L920–1183). diff --git a/src/core/tests/Deal.Tests.Unit/RateLimitHttpTests.cs b/src/core/tests/Deal.Tests.Unit/Support/RateLimitHttpTests.cs similarity index 97% rename from src/core/tests/Deal.Tests.Unit/RateLimitHttpTests.cs rename to src/core/tests/Deal.Tests.Unit/Support/RateLimitHttpTests.cs index e662f4e..eb2988a 100644 --- a/src/core/tests/Deal.Tests.Unit/RateLimitHttpTests.cs +++ b/src/core/tests/Deal.Tests.Unit/Support/RateLimitHttpTests.cs @@ -14,8 +14,9 @@ using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.DependencyInjection; +using Deal.Tests.Unit.Modules.Tenants; -namespace Deal.Tests.Unit; +namespace Deal.Tests.Unit.Support; /// /// HTTP-тесты политик встроенного rate limiter (план Task 11, Ruling 5): превышение окна политики — diff --git a/src/core/tests/Deal.Tests.Unit/RatesServiceTests.cs b/src/core/tests/Deal.Tests.Unit/Support/RatesServiceTests.cs similarity index 99% rename from src/core/tests/Deal.Tests.Unit/RatesServiceTests.cs rename to src/core/tests/Deal.Tests.Unit/Support/RatesServiceTests.cs index ef150da..4a06ba6 100644 --- a/src/core/tests/Deal.Tests.Unit/RatesServiceTests.cs +++ b/src/core/tests/Deal.Tests.Unit/Support/RatesServiceTests.cs @@ -3,8 +3,9 @@ using Deal.Modules.Settings.Application.Abstractions; using Deal.Modules.Settings.Application.Models; using Deal.Modules.Settings.Application.Registrars; using Deal.Modules.Settings.Application.Services; +using Deal.Tests.Unit.Modules.Settings; -namespace Deal.Tests.Unit; +namespace Deal.Tests.Unit.Support; /// /// Тесты RatesService: кэш ratesCache, refresh-семантика, ShouldFetch, ConvertAmount (Task 8, Ruling 6). diff --git a/src/core/tests/Deal.Tests.Unit/ServiceHealthProbeTests.cs b/src/core/tests/Deal.Tests.Unit/Support/ServiceHealthProbeTests.cs similarity index 97% rename from src/core/tests/Deal.Tests.Unit/ServiceHealthProbeTests.cs rename to src/core/tests/Deal.Tests.Unit/Support/ServiceHealthProbeTests.cs index 41edc10..2ccb925 100644 --- a/src/core/tests/Deal.Tests.Unit/ServiceHealthProbeTests.cs +++ b/src/core/tests/Deal.Tests.Unit/Support/ServiceHealthProbeTests.cs @@ -11,7 +11,7 @@ using Microsoft.AspNetCore.Server.Kestrel.Core; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Diagnostics.HealthChecks; -namespace Deal.Tests.Unit; +namespace Deal.Tests.Unit.Support; /// /// Тесты gRPC-health-пробы автономных сервисов (план Task 10, Ruling 3/6): in-proc health-сервер фейк — diff --git a/src/core/tests/Deal.Tests.Unit/SettingsServiceTests.cs b/src/core/tests/Deal.Tests.Unit/Support/SettingsServiceTests.cs similarity index 99% rename from src/core/tests/Deal.Tests.Unit/SettingsServiceTests.cs rename to src/core/tests/Deal.Tests.Unit/Support/SettingsServiceTests.cs index 102f34b..63c889c 100644 --- a/src/core/tests/Deal.Tests.Unit/SettingsServiceTests.cs +++ b/src/core/tests/Deal.Tests.Unit/Support/SettingsServiceTests.cs @@ -3,8 +3,9 @@ using Deal.Modules.Settings.Application.Abstractions; using Deal.Modules.Settings.Application.Models; using Deal.Modules.Settings.Application.Registrars; using Deal.Modules.Settings.Application.Services; +using Deal.Tests.Unit.Modules.Settings; -namespace Deal.Tests.Unit; +namespace Deal.Tests.Unit.Support; /// /// Тесты SettingsService: public-снимок и PATCH-семантика 1:1 (Task 3, Rulings 1/3/9). diff --git a/src/core/tests/Deal.Tests.Unit/SseBrokerTests.cs b/src/core/tests/Deal.Tests.Unit/Support/SseBrokerTests.cs similarity index 97% rename from src/core/tests/Deal.Tests.Unit/SseBrokerTests.cs rename to src/core/tests/Deal.Tests.Unit/Support/SseBrokerTests.cs index 386017f..03ab783 100644 --- a/src/core/tests/Deal.Tests.Unit/SseBrokerTests.cs +++ b/src/core/tests/Deal.Tests.Unit/Support/SseBrokerTests.cs @@ -1,7 +1,7 @@ using System.Text.Json; using Deal.Api.Events; -namespace Deal.Tests.Unit; +namespace Deal.Tests.Unit.Support; /// /// Тесты SSE-брокера (план Task 9 L362–378, Ruling 5): очередь подписчика ≤200 с вытеснением diff --git a/src/core/tests/Deal.Tests.Unit/StorageTickSchedulerTests.cs b/src/core/tests/Deal.Tests.Unit/Support/StorageTickSchedulerTests.cs similarity index 99% rename from src/core/tests/Deal.Tests.Unit/StorageTickSchedulerTests.cs rename to src/core/tests/Deal.Tests.Unit/Support/StorageTickSchedulerTests.cs index 15076f1..296489e 100644 --- a/src/core/tests/Deal.Tests.Unit/StorageTickSchedulerTests.cs +++ b/src/core/tests/Deal.Tests.Unit/Support/StorageTickSchedulerTests.cs @@ -26,8 +26,13 @@ using Deal.SharedKernel.Tenants.Abstractions; using Deal.SharedKernel.Tenants.Models; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging.Abstractions; +using Deal.Tests.Unit.Modules.Tenants; +using Deal.Tests.Unit.Modules.Cards; +using Deal.Tests.Unit.Modules.Settings; +using Deal.Tests.Unit.Modules.Kanban; +using Deal.Tests.Unit.Contracts; -namespace Deal.Tests.Unit; +namespace Deal.Tests.Unit.Support; /// /// Тесты StorageTickScheduler — логика прохода цикла правил хранения (план Task 11, Ruling 8; diff --git a/src/core/tests/Deal.Tests.Unit/StorageToastPublisherTests.cs b/src/core/tests/Deal.Tests.Unit/Support/StorageToastPublisherTests.cs similarity index 97% rename from src/core/tests/Deal.Tests.Unit/StorageToastPublisherTests.cs rename to src/core/tests/Deal.Tests.Unit/Support/StorageToastPublisherTests.cs index e023266..57eff4b 100644 --- a/src/core/tests/Deal.Tests.Unit/StorageToastPublisherTests.cs +++ b/src/core/tests/Deal.Tests.Unit/Support/StorageToastPublisherTests.cs @@ -2,7 +2,7 @@ using System.Text.Json; using Deal.Api.Events; using Deal.Modules.Kanban.Application.Models; -namespace Deal.Tests.Unit; +namespace Deal.Tests.Unit.Support; /// /// Тесты StorageToastPublisher — общий хелпер SSE-тостов статистики тика для POST /api/admin/tick diff --git a/src/core/tests/Deal.Tests.Unit/StubHttpMessageHandler.cs b/src/core/tests/Deal.Tests.Unit/Support/StubHttpMessageHandler.cs similarity index 98% rename from src/core/tests/Deal.Tests.Unit/StubHttpMessageHandler.cs rename to src/core/tests/Deal.Tests.Unit/Support/StubHttpMessageHandler.cs index c03be06..93fb7e2 100644 --- a/src/core/tests/Deal.Tests.Unit/StubHttpMessageHandler.cs +++ b/src/core/tests/Deal.Tests.Unit/Support/StubHttpMessageHandler.cs @@ -1,4 +1,4 @@ -namespace Deal.Tests.Unit; +namespace Deal.Tests.Unit.Support; /// /// Фейковый HttpMessageHandler для юнит-тестов HTTP-адаптеров (запись запросов). diff --git a/src/core/tests/Deal.Tests.Unit/SuggestResultDtosTests.cs b/src/core/tests/Deal.Tests.Unit/Support/SuggestResultDtosTests.cs similarity index 99% rename from src/core/tests/Deal.Tests.Unit/SuggestResultDtosTests.cs rename to src/core/tests/Deal.Tests.Unit/Support/SuggestResultDtosTests.cs index e5de4e5..cbfe505 100644 --- a/src/core/tests/Deal.Tests.Unit/SuggestResultDtosTests.cs +++ b/src/core/tests/Deal.Tests.Unit/Support/SuggestResultDtosTests.cs @@ -1,7 +1,7 @@ using System.Text.Json; using Deal.Contracts.Integrations.Models; -namespace Deal.Tests.Unit; +namespace Deal.Tests.Unit.Support; /// /// Wire-форма ответов ИИ-предложений: сериализация DTO 1:1 с api-map §3.2 L120–121 (camelCase, diff --git a/src/core/tests/Deal.Tests.Unit/TelegramGrpcTestHost.cs b/src/core/tests/Deal.Tests.Unit/Support/TelegramGrpcTestHost.cs similarity index 96% rename from src/core/tests/Deal.Tests.Unit/TelegramGrpcTestHost.cs rename to src/core/tests/Deal.Tests.Unit/Support/TelegramGrpcTestHost.cs index ccb0fff..9ac431f 100644 --- a/src/core/tests/Deal.Tests.Unit/TelegramGrpcTestHost.cs +++ b/src/core/tests/Deal.Tests.Unit/Support/TelegramGrpcTestHost.cs @@ -4,8 +4,9 @@ using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Server.Kestrel.Core; using Microsoft.Extensions.DependencyInjection; +using Deal.Tests.Unit.Grpc; -namespace Deal.Tests.Unit; +namespace Deal.Tests.Unit.Support; // Общий харнесс in-proc тестов gRPC-клиента ядра к telegram-service (план Task 14, Ruling 1/2). // Поднимает в процессе теста Kestrel HTTP/2 (plaintext — Ruling 2) на эфемерном порту с фейком diff --git a/src/core/tests/Deal.Tests.Unit/TelegramIngressServiceTests.cs b/src/core/tests/Deal.Tests.Unit/Support/TelegramIngressServiceTests.cs similarity index 99% rename from src/core/tests/Deal.Tests.Unit/TelegramIngressServiceTests.cs rename to src/core/tests/Deal.Tests.Unit/Support/TelegramIngressServiceTests.cs index ee6b68e..cf4df5c 100644 --- a/src/core/tests/Deal.Tests.Unit/TelegramIngressServiceTests.cs +++ b/src/core/tests/Deal.Tests.Unit/Support/TelegramIngressServiceTests.cs @@ -18,8 +18,12 @@ using Deal.Modules.Tenants.Application.Services; using Grpc.Core; using Grpc.Net.Client; using Microsoft.Extensions.DependencyInjection; +using Deal.Tests.Unit.Modules.Tenants; +using Deal.Tests.Unit.Modules.Settings; +using Deal.Tests.Unit.Modules.Kanban; +using Deal.Tests.Unit.Contracts; -namespace Deal.Tests.Unit; +namespace Deal.Tests.Unit.Support; /// /// In-proc gRPC-тесты входящего потока telegram-service → ядро (план Task 12, L361–377; Ruling 1/7). diff --git a/src/core/tests/Deal.Tests.Unit/TelegramIngressTestHost.cs b/src/core/tests/Deal.Tests.Unit/Support/TelegramIngressTestHost.cs similarity index 96% rename from src/core/tests/Deal.Tests.Unit/TelegramIngressTestHost.cs rename to src/core/tests/Deal.Tests.Unit/Support/TelegramIngressTestHost.cs index a89f8d6..f852c4f 100644 --- a/src/core/tests/Deal.Tests.Unit/TelegramIngressTestHost.cs +++ b/src/core/tests/Deal.Tests.Unit/Support/TelegramIngressTestHost.cs @@ -32,8 +32,11 @@ using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Server.Kestrel.Core; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Diagnostics.HealthChecks; +using Deal.Tests.Unit.Modules.Tenants; +using Deal.Tests.Unit.Modules.Settings; +using Deal.Tests.Unit.Contracts; -namespace Deal.Tests.Unit; +namespace Deal.Tests.Unit.Support; // Общий харнесс интеграционных тестов gRPC-ингресса Deal.Api (план Task 12): поднимает хост в процессе // теста на эфемерном порту (Kestrel HTTP/2, plaintext — Ruling 2) с тем же набором регистраций, что diff --git a/src/core/tests/Deal.Tests.Unit/TestPort.cs b/src/core/tests/Deal.Tests.Unit/Support/TestPort.cs similarity index 97% rename from src/core/tests/Deal.Tests.Unit/TestPort.cs rename to src/core/tests/Deal.Tests.Unit/Support/TestPort.cs index 463bc2f..d980542 100644 --- a/src/core/tests/Deal.Tests.Unit/TestPort.cs +++ b/src/core/tests/Deal.Tests.Unit/Support/TestPort.cs @@ -2,7 +2,7 @@ using System.Collections.Concurrent; using System.Net; using System.Net.Sockets; -namespace Deal.Tests.Unit; +namespace Deal.Tests.Unit.Support; // Единый аллокатор свободных TCP-портов для интеграционных тестов ядра (этап 12, остаток 1). // Заменяет размноженные копии FreeTcpPort() из тест-хостов (AiGrpcTestHost, OperatorAuthHttpHost, diff --git a/src/core/tests/Deal.Tests.Unit/TokenUsageRecorderTests.cs b/src/core/tests/Deal.Tests.Unit/Support/TokenUsageRecorderTests.cs similarity index 98% rename from src/core/tests/Deal.Tests.Unit/TokenUsageRecorderTests.cs rename to src/core/tests/Deal.Tests.Unit/Support/TokenUsageRecorderTests.cs index 0edee9b..4b94c20 100644 --- a/src/core/tests/Deal.Tests.Unit/TokenUsageRecorderTests.cs +++ b/src/core/tests/Deal.Tests.Unit/Support/TokenUsageRecorderTests.cs @@ -18,8 +18,10 @@ using Deal.Modules.Tenants.Application.Registrars; using Deal.Modules.Tenants.Application.Services; using Deal.SharedKernel.Tenants.Abstractions; using Deal.SharedKernel.Tenants.Models; +using Deal.Tests.Unit.Modules.Settings; +using Deal.Tests.Unit.Modules.Tenants; -namespace Deal.Tests.Unit; +namespace Deal.Tests.Unit.Support; /// /// Тесты recorder'а расхода токенов (Ruling 3 этапа 7, Task 8; история — этап 10, T2): успешный usage diff --git a/src/ml-service/Deal.Ml.Tests/MlRpcTests.cs b/src/ml-service/Deal.Ml.Tests/Grpc/MlRpcTests.cs similarity index 97% rename from src/ml-service/Deal.Ml.Tests/MlRpcTests.cs rename to src/ml-service/Deal.Ml.Tests/Grpc/MlRpcTests.cs index e0fbe83..4877414 100644 --- a/src/ml-service/Deal.Ml.Tests/MlRpcTests.cs +++ b/src/ml-service/Deal.Ml.Tests/Grpc/MlRpcTests.cs @@ -1,283 +1,284 @@ -using Deal.Grpc.Ml; -using Grpc.Core; -using Grpc.Net.Client; - -namespace Deal.Ml.Tests; - -/// -/// In-proc gRPC-тесты MlService (план Task 6, Acceptance): Predict/Status/Reset/TrainBatch через -/// реальный хост (Kestrel HTTP/2, интерцептор service-token) с tenant-id из metadata; модель -/// тенанта создаётся лениво (Predict неготовой модели — «не уверен», не ошибка). Без сети. -/// -public sealed class MlRpcTests -{ - /// - /// Свежий тенант: Status — пустой ответ, Predict — фиксированный «не уверен» 1:1. - /// - [Fact] - public async Task FreshTenant_StatusAndPredictReturnNotReadyShape() - { - await MlTestHost.RunAsync(MlTestHost.DefaultToken, async channel => - { - var client = new MlService.MlServiceClient(channel); - CallOptions options = Options(MlTestHost.DefaultTenantId); - - StatusReply status = await client.StatusAsync(new StatusRequest(), options); - - Assert.False(status.Ready); - Assert.Empty(status.Classes); - Assert.Equal(0, status.Learned); - Assert.Equal(0, status.Eval.Count); - Assert.Equal(0.0, status.Eval.Accuracy); - - PredictReply predict = await client.PredictAsync( - new PredictRequest { Text = LearningData.DevMessage }, - options); - - Assert.False(predict.Take); - Assert.False(predict.HasLabel); - Assert.False(predict.Ready); - Assert.Empty(predict.Scores); - Assert.Equal(0, predict.Hits); - Assert.False(predict.HasMargin); - Assert.Empty(predict.Terms); - Assert.Null(predict.Type); - }); - } - - /// - /// TrainBatch батчем из 3 → learned=3; модель остаётся неготовой (ниже MIN_TOTAL). - /// - [Fact] - public async Task TrainBatch_ThreeItems_LearnsThree() - { - await MlTestHost.RunAsync(MlTestHost.DefaultToken, async channel => - { - var client = new MlService.MlServiceClient(channel); - CallOptions options = Options(MlTestHost.DefaultTenantId); - - var request = new TrainBatchRequest(); - request.Items.Add(new TrainExample { Text = LearningData.DevMessage, Label = LearningData.ColumnDev, Delta = 1.0 }); - request.Items.Add(new TrainExample { Text = LearningData.DevMessage, Label = LearningData.ColumnDev, Delta = 1.0 }); - request.Items.Add(new TrainExample { Text = LearningData.SpamMessage, Label = LearningData.SpamLabel, Delta = 1.0 }); - - TrainBatchReply reply = await client.TrainBatchAsync(request, options); - - Assert.Equal(3, reply.Learned); - - StatusReply status = await client.StatusAsync(new StatusRequest(), options); - Assert.False(status.Ready); - Assert.Equal(3, status.Learned); - }); - } - - /// - /// Полный цикл: канонический батч → Status ready/learned → Predict колонки + типа. - /// - [Fact] - public async Task TrainBatch_FullCanonical_ThenStatusAndPredict() - { - await MlTestHost.RunAsync(MlTestHost.DefaultToken, async channel => - { - var client = new MlService.MlServiceClient(channel); - CallOptions options = Options(MlTestHost.DefaultTenantId); - - TrainBatchReply batch = await client.TrainBatchAsync( - new TrainBatchRequest { Items = { LearningData.CanonicalTrainItems().Select(ToExample) } }, - options); - Assert.Equal(28, batch.Learned); - - StatusReply status = await client.StatusAsync(new StatusRequest(), options); - Assert.True(status.Ready); - Assert.Equal(28, status.Learned); - Assert.Equal(6.0, status.Classes[LearningData.SpamLabel]); - - PredictReply dev = await client.PredictAsync( - new PredictRequest { Text = LearningData.DevMessage }, - options); - Assert.True(dev.Take); - Assert.Equal(LearningData.ColumnDev, dev.Label); - Assert.True(dev.HasMargin); - Assert.NotNull(dev.Type); - Assert.Equal("hire", dev.Type.Label); - - PredictReply spam = await client.PredictAsync( - new PredictRequest { Text = LearningData.SpamMessage }, - options); - Assert.True(spam.Take); - Assert.Equal(LearningData.SpamLabel, spam.Label); - Assert.Null(spam.Type); - }); - } - - /// - /// Reset обнуляет модель тенанта: Status снова пуст, Predict «не уверен». - /// - [Fact] - public async Task Reset_AfterTraining_EmptiesModel() - { - await MlTestHost.RunAsync(MlTestHost.DefaultToken, async channel => - { - var client = new MlService.MlServiceClient(channel); - CallOptions options = Options(MlTestHost.DefaultTenantId); - - await client.TrainBatchAsync( - new TrainBatchRequest { Items = { LearningData.CanonicalTrainItems().Select(ToExample) } }, - options); - - ResetReply reset = await client.ResetAsync(new ResetRequest(), options); - - Assert.True(reset.Ok); - Assert.False(reset.HasError); - - StatusReply status = await client.StatusAsync(new StatusRequest(), options); - Assert.False(status.Ready); - Assert.Empty(status.Classes); - Assert.Equal(0, status.Learned); - - PredictReply predict = await client.PredictAsync( - new PredictRequest { Text = LearningData.DevMessage }, - options); - Assert.False(predict.Take); - Assert.False(predict.Ready); - }); - } - - /// - /// Изоляция тенантов: обучение одного не влияет на модель другого (пул per-tenant). - /// - [Fact] - public async Task Tenants_AreIsolated() - { - await MlTestHost.RunAsync(MlTestHost.DefaultToken, async channel => - { - var client = new MlService.MlServiceClient(channel); - - await client.TrainBatchAsync( - new TrainBatchRequest { Items = { LearningData.CanonicalTrainItems().Select(ToExample) } }, - Options("tenant-a")); - - StatusReply trained = await client.StatusAsync(new StatusRequest(), Options("tenant-a")); - Assert.True(trained.Ready); - Assert.Equal(28, trained.Learned); - - StatusReply untouched = await client.StatusAsync(new StatusRequest(), Options("tenant-b")); - Assert.False(untouched.Ready); - Assert.Equal(0, untouched.Learned); - - PredictReply predictB = await client.PredictAsync( - new PredictRequest { Text = LearningData.DevMessage }, - Options("tenant-b")); - Assert.False(predictB.Take); - Assert.False(predictB.Ready); - }); - } - - /// - /// Отсутствующий tenant-id в metadata → UNAUTHENTICATED (Ruling 1; шаблон T5). - /// - [Fact] - public async Task Status_WithoutTenantId_IsUnauthenticated() - { - await MlTestHost.RunAsync(MlTestHost.DefaultToken, async channel => - { - var client = new MlService.MlServiceClient(channel); - Metadata metadata = MlTestHost.CallMetadata(MlTestHost.DefaultToken, tenantId: null); - - AsyncUnaryCall call = client.StatusAsync(new StatusRequest(), MlTestHost.CallOptions(metadata)); - RpcException exception = await Assert.ThrowsAsync(() => call.ResponseAsync); - - Assert.Equal(StatusCode.Unauthenticated, exception.StatusCode); - }); - } - - /// - /// Tenant-id с недопустимыми символами пути → INVALID_ARGUMENT (защита каталога). - /// - [Fact] - public async Task Status_WithPathTraversalTenantId_IsInvalidArgument() - { - await MlTestHost.RunAsync(MlTestHost.DefaultToken, async channel => - { - var client = new MlService.MlServiceClient(channel); - - AsyncUnaryCall call = client.StatusAsync( - new StatusRequest(), - MlTestHost.CallOptions(MlTestHost.CallMetadata(MlTestHost.DefaultToken, tenantId: "../escape"))); - RpcException exception = await Assert.ThrowsAsync(() => call.ResponseAsync); - - Assert.Equal(StatusCode.InvalidArgument, exception.StatusCode); - }); - } - - /// - /// Пустые text/label в батче пропускаются: learned = применённые, ответ не падает. - /// - [Fact] - public async Task TrainBatch_EmptyItems_SkippedQuietly() - { - await MlTestHost.RunAsync(MlTestHost.DefaultToken, async channel => - { - var client = new MlService.MlServiceClient(channel); - var request = new TrainBatchRequest(); - request.Items.Add(new TrainExample { Text = string.Empty, Label = LearningData.ColumnDev, Delta = 1.0 }); - request.Items.Add(new TrainExample { Text = LearningData.DevMessage, Label = " ", Delta = 1.0 }); - request.Items.Add(new TrainExample { Text = LearningData.DevMessage, Label = LearningData.ColumnDev, Delta = 1.0 }); - - TrainBatchReply reply = await client.TrainBatchAsync(request, Options(MlTestHost.DefaultTenantId)); - - Assert.Equal(1, reply.Learned); - }); - } - - /// - /// Серверный лимит батча (ml.proto: ≤100 примеров): 101 → INVALID_ARGUMENT до обучения. - /// - [Fact] - public async Task TrainBatch_OverBatchLimit_IsInvalidArgument() - { - await MlTestHost.RunAsync(MlTestHost.DefaultToken, async channel => - { - var client = new MlService.MlServiceClient(channel); - var request = new TrainBatchRequest(); - for (int i = 0; i < 101; i++) - { - request.Items.Add(new TrainExample { Text = LearningData.DevMessage, Label = LearningData.ColumnDev, Delta = 1.0 }); - } - - RpcException exception = await Assert.ThrowsAsync(() => - client.TrainBatchAsync(request, Options(MlTestHost.DefaultTenantId)).ResponseAsync); - - Assert.Equal(StatusCode.InvalidArgument, exception.StatusCode); - }); - } - - /// - /// Серверный лимит длины текста примера (source_msg ≤4000): превышение → INVALID_ARGUMENT. - /// - [Fact] - public async Task TrainBatch_TooLongExampleText_IsInvalidArgument() - { - await MlTestHost.RunAsync(MlTestHost.DefaultToken, async channel => - { - var client = new MlService.MlServiceClient(channel); - var request = new TrainBatchRequest(); - request.Items.Add(new TrainExample { Text = new string('а', 4001), Label = LearningData.ColumnDev, Delta = 1.0 }); - - RpcException exception = await Assert.ThrowsAsync(() => - client.TrainBatchAsync(request, Options(MlTestHost.DefaultTenantId)).ResponseAsync); - - Assert.Equal(StatusCode.InvalidArgument, exception.StatusCode); - }); - } - - // CallOptions с metadata (service-token + tenant-id) и deadline. - // tenantId: Id тенанта. - private static CallOptions Options(string tenantId) - => MlTestHost.CallOptions(MlTestHost.CallMetadata(MlTestHost.DefaultToken, tenantId)); - - // Маппит пример обучения в wire-форму TrainExample (text/label/delta). - // item: Пример. - private static TrainExample ToExample(Deal.Ml.Model.LearnItem item) - => new() { Text = item.Text, Label = item.Label, Delta = item.Delta }; -} +using Deal.Grpc.Ml; +using Grpc.Core; +using Grpc.Net.Client; +using Deal.Ml.Tests.Ml; + +namespace Deal.Ml.Tests.Grpc; + +/// +/// In-proc gRPC-тесты MlService (план Task 6, Acceptance): Predict/Status/Reset/TrainBatch через +/// реальный хост (Kestrel HTTP/2, интерцептор service-token) с tenant-id из metadata; модель +/// тенанта создаётся лениво (Predict неготовой модели — «не уверен», не ошибка). Без сети. +/// +public sealed class MlRpcTests +{ + /// + /// Свежий тенант: Status — пустой ответ, Predict — фиксированный «не уверен» 1:1. + /// + [Fact] + public async Task FreshTenant_StatusAndPredictReturnNotReadyShape() + { + await MlTestHost.RunAsync(MlTestHost.DefaultToken, async channel => + { + var client = new MlService.MlServiceClient(channel); + CallOptions options = Options(MlTestHost.DefaultTenantId); + + StatusReply status = await client.StatusAsync(new StatusRequest(), options); + + Assert.False(status.Ready); + Assert.Empty(status.Classes); + Assert.Equal(0, status.Learned); + Assert.Equal(0, status.Eval.Count); + Assert.Equal(0.0, status.Eval.Accuracy); + + PredictReply predict = await client.PredictAsync( + new PredictRequest { Text = LearningData.DevMessage }, + options); + + Assert.False(predict.Take); + Assert.False(predict.HasLabel); + Assert.False(predict.Ready); + Assert.Empty(predict.Scores); + Assert.Equal(0, predict.Hits); + Assert.False(predict.HasMargin); + Assert.Empty(predict.Terms); + Assert.Null(predict.Type); + }); + } + + /// + /// TrainBatch батчем из 3 → learned=3; модель остаётся неготовой (ниже MIN_TOTAL). + /// + [Fact] + public async Task TrainBatch_ThreeItems_LearnsThree() + { + await MlTestHost.RunAsync(MlTestHost.DefaultToken, async channel => + { + var client = new MlService.MlServiceClient(channel); + CallOptions options = Options(MlTestHost.DefaultTenantId); + + var request = new TrainBatchRequest(); + request.Items.Add(new TrainExample { Text = LearningData.DevMessage, Label = LearningData.ColumnDev, Delta = 1.0 }); + request.Items.Add(new TrainExample { Text = LearningData.DevMessage, Label = LearningData.ColumnDev, Delta = 1.0 }); + request.Items.Add(new TrainExample { Text = LearningData.SpamMessage, Label = LearningData.SpamLabel, Delta = 1.0 }); + + TrainBatchReply reply = await client.TrainBatchAsync(request, options); + + Assert.Equal(3, reply.Learned); + + StatusReply status = await client.StatusAsync(new StatusRequest(), options); + Assert.False(status.Ready); + Assert.Equal(3, status.Learned); + }); + } + + /// + /// Полный цикл: канонический батч → Status ready/learned → Predict колонки + типа. + /// + [Fact] + public async Task TrainBatch_FullCanonical_ThenStatusAndPredict() + { + await MlTestHost.RunAsync(MlTestHost.DefaultToken, async channel => + { + var client = new MlService.MlServiceClient(channel); + CallOptions options = Options(MlTestHost.DefaultTenantId); + + TrainBatchReply batch = await client.TrainBatchAsync( + new TrainBatchRequest { Items = { LearningData.CanonicalTrainItems().Select(ToExample) } }, + options); + Assert.Equal(28, batch.Learned); + + StatusReply status = await client.StatusAsync(new StatusRequest(), options); + Assert.True(status.Ready); + Assert.Equal(28, status.Learned); + Assert.Equal(6.0, status.Classes[LearningData.SpamLabel]); + + PredictReply dev = await client.PredictAsync( + new PredictRequest { Text = LearningData.DevMessage }, + options); + Assert.True(dev.Take); + Assert.Equal(LearningData.ColumnDev, dev.Label); + Assert.True(dev.HasMargin); + Assert.NotNull(dev.Type); + Assert.Equal("hire", dev.Type.Label); + + PredictReply spam = await client.PredictAsync( + new PredictRequest { Text = LearningData.SpamMessage }, + options); + Assert.True(spam.Take); + Assert.Equal(LearningData.SpamLabel, spam.Label); + Assert.Null(spam.Type); + }); + } + + /// + /// Reset обнуляет модель тенанта: Status снова пуст, Predict «не уверен». + /// + [Fact] + public async Task Reset_AfterTraining_EmptiesModel() + { + await MlTestHost.RunAsync(MlTestHost.DefaultToken, async channel => + { + var client = new MlService.MlServiceClient(channel); + CallOptions options = Options(MlTestHost.DefaultTenantId); + + await client.TrainBatchAsync( + new TrainBatchRequest { Items = { LearningData.CanonicalTrainItems().Select(ToExample) } }, + options); + + ResetReply reset = await client.ResetAsync(new ResetRequest(), options); + + Assert.True(reset.Ok); + Assert.False(reset.HasError); + + StatusReply status = await client.StatusAsync(new StatusRequest(), options); + Assert.False(status.Ready); + Assert.Empty(status.Classes); + Assert.Equal(0, status.Learned); + + PredictReply predict = await client.PredictAsync( + new PredictRequest { Text = LearningData.DevMessage }, + options); + Assert.False(predict.Take); + Assert.False(predict.Ready); + }); + } + + /// + /// Изоляция тенантов: обучение одного не влияет на модель другого (пул per-tenant). + /// + [Fact] + public async Task Tenants_AreIsolated() + { + await MlTestHost.RunAsync(MlTestHost.DefaultToken, async channel => + { + var client = new MlService.MlServiceClient(channel); + + await client.TrainBatchAsync( + new TrainBatchRequest { Items = { LearningData.CanonicalTrainItems().Select(ToExample) } }, + Options("tenant-a")); + + StatusReply trained = await client.StatusAsync(new StatusRequest(), Options("tenant-a")); + Assert.True(trained.Ready); + Assert.Equal(28, trained.Learned); + + StatusReply untouched = await client.StatusAsync(new StatusRequest(), Options("tenant-b")); + Assert.False(untouched.Ready); + Assert.Equal(0, untouched.Learned); + + PredictReply predictB = await client.PredictAsync( + new PredictRequest { Text = LearningData.DevMessage }, + Options("tenant-b")); + Assert.False(predictB.Take); + Assert.False(predictB.Ready); + }); + } + + /// + /// Отсутствующий tenant-id в metadata → UNAUTHENTICATED (Ruling 1; шаблон T5). + /// + [Fact] + public async Task Status_WithoutTenantId_IsUnauthenticated() + { + await MlTestHost.RunAsync(MlTestHost.DefaultToken, async channel => + { + var client = new MlService.MlServiceClient(channel); + Metadata metadata = MlTestHost.CallMetadata(MlTestHost.DefaultToken, tenantId: null); + + AsyncUnaryCall call = client.StatusAsync(new StatusRequest(), MlTestHost.CallOptions(metadata)); + RpcException exception = await Assert.ThrowsAsync(() => call.ResponseAsync); + + Assert.Equal(StatusCode.Unauthenticated, exception.StatusCode); + }); + } + + /// + /// Tenant-id с недопустимыми символами пути → INVALID_ARGUMENT (защита каталога). + /// + [Fact] + public async Task Status_WithPathTraversalTenantId_IsInvalidArgument() + { + await MlTestHost.RunAsync(MlTestHost.DefaultToken, async channel => + { + var client = new MlService.MlServiceClient(channel); + + AsyncUnaryCall call = client.StatusAsync( + new StatusRequest(), + MlTestHost.CallOptions(MlTestHost.CallMetadata(MlTestHost.DefaultToken, tenantId: "../escape"))); + RpcException exception = await Assert.ThrowsAsync(() => call.ResponseAsync); + + Assert.Equal(StatusCode.InvalidArgument, exception.StatusCode); + }); + } + + /// + /// Пустые text/label в батче пропускаются: learned = применённые, ответ не падает. + /// + [Fact] + public async Task TrainBatch_EmptyItems_SkippedQuietly() + { + await MlTestHost.RunAsync(MlTestHost.DefaultToken, async channel => + { + var client = new MlService.MlServiceClient(channel); + var request = new TrainBatchRequest(); + request.Items.Add(new TrainExample { Text = string.Empty, Label = LearningData.ColumnDev, Delta = 1.0 }); + request.Items.Add(new TrainExample { Text = LearningData.DevMessage, Label = " ", Delta = 1.0 }); + request.Items.Add(new TrainExample { Text = LearningData.DevMessage, Label = LearningData.ColumnDev, Delta = 1.0 }); + + TrainBatchReply reply = await client.TrainBatchAsync(request, Options(MlTestHost.DefaultTenantId)); + + Assert.Equal(1, reply.Learned); + }); + } + + /// + /// Серверный лимит батча (ml.proto: ≤100 примеров): 101 → INVALID_ARGUMENT до обучения. + /// + [Fact] + public async Task TrainBatch_OverBatchLimit_IsInvalidArgument() + { + await MlTestHost.RunAsync(MlTestHost.DefaultToken, async channel => + { + var client = new MlService.MlServiceClient(channel); + var request = new TrainBatchRequest(); + for (int i = 0; i < 101; i++) + { + request.Items.Add(new TrainExample { Text = LearningData.DevMessage, Label = LearningData.ColumnDev, Delta = 1.0 }); + } + + RpcException exception = await Assert.ThrowsAsync(() => + client.TrainBatchAsync(request, Options(MlTestHost.DefaultTenantId)).ResponseAsync); + + Assert.Equal(StatusCode.InvalidArgument, exception.StatusCode); + }); + } + + /// + /// Серверный лимит длины текста примера (source_msg ≤4000): превышение → INVALID_ARGUMENT. + /// + [Fact] + public async Task TrainBatch_TooLongExampleText_IsInvalidArgument() + { + await MlTestHost.RunAsync(MlTestHost.DefaultToken, async channel => + { + var client = new MlService.MlServiceClient(channel); + var request = new TrainBatchRequest(); + request.Items.Add(new TrainExample { Text = new string('а', 4001), Label = LearningData.ColumnDev, Delta = 1.0 }); + + RpcException exception = await Assert.ThrowsAsync(() => + client.TrainBatchAsync(request, Options(MlTestHost.DefaultTenantId)).ResponseAsync); + + Assert.Equal(StatusCode.InvalidArgument, exception.StatusCode); + }); + } + + // CallOptions с metadata (service-token + tenant-id) и deadline. + // tenantId: Id тенанта. + private static CallOptions Options(string tenantId) + => MlTestHost.CallOptions(MlTestHost.CallMetadata(MlTestHost.DefaultToken, tenantId)); + + // Маппит пример обучения в wire-форму TrainExample (text/label/delta). + // item: Пример. + private static TrainExample ToExample(Deal.Ml.Model.LearnItem item) + => new() { Text = item.Text, Label = item.Label, Delta = item.Delta }; +} diff --git a/src/ml-service/Deal.Ml.Tests/MlServiceHostTests.cs b/src/ml-service/Deal.Ml.Tests/Grpc/MlServiceHostTests.cs similarity index 97% rename from src/ml-service/Deal.Ml.Tests/MlServiceHostTests.cs rename to src/ml-service/Deal.Ml.Tests/Grpc/MlServiceHostTests.cs index af6bd09..46c8024 100644 --- a/src/ml-service/Deal.Ml.Tests/MlServiceHostTests.cs +++ b/src/ml-service/Deal.Ml.Tests/Grpc/MlServiceHostTests.cs @@ -1,247 +1,247 @@ -using Deal.Grpc.Ml; -using Deal.Ml; -using Microsoft.AspNetCore.Builder; -using Grpc.Core; -using Grpc.Health.V1; -using Grpc.Net.Client; -using System.Net; -using System.Net.Sockets; -using Xunit; - -namespace Deal.Ml.Tests; - -/// -/// Интеграционные тесты хоста ml-service (каркас план Task 3 + Status задач 5–6). -/// -/// Хост поднимается В процессе теста (Kestrel HTTP/2, эфемерный порт) через MlServiceHost.Create — -/// ту же сборку хоста, что использует Program.cs, поэтому тесты покрывают реальную настройку -/// Kestrel/AddGrpc/health, а не её копию. Проверки: gRPC-health → SERVING; ServiceTokenInterceptor -/// (Ruling 1): запрос без токена и с неверным токеном → UNAUTHENTICATED; верный токен проходит к -/// реализованному Status (свежая модель → ready=false); при незаданном DEAL_SERVICE_TOKEN — fail-closed. -/// -/// Токен интерцептор читает из конфигурации (env DEAL_SERVICE_TOKEN), Status создаёт модель тенанта — -/// каталог моделей (DEAL_ML_DATA_DIR) на время сценария направляется во временную папку; тесты -/// выставляют env и восстанавливают исходные значения. Все тесты класса живут в одном процессе/классе, -/// чтобы env и свободные порты не конфликтовали (xunit исполняет методы класса последовательно). -/// -public sealed class MlServiceHostTests -{ - // Env-ключ ожидаемого токена (зеркало ServiceTokenInterceptor). - private const string ServiceTokenEnvKey = "DEAL_SERVICE_TOKEN"; - - // Env-ключ каталога моделей (зеркало MlOptions; Status создаёт модель тенанта). - private const string DataDirEnvKey = "DEAL_ML_DATA_DIR"; - - // Id тенанта сценариев теста (Status реализован задачами 5–6). - private const string TestTenantId = "tenant-test"; - - // Ключ gRPC-metadata с tenant-id (зеркало MlServiceImpl.TenantIdMetadataKey). - private const string TenantIdMetadataKey = "tenant-id"; - - // Ключ gRPC-metadata (зеркало ServiceTokenInterceptor.ServiceTokenMetadataKey). - private const string ServiceTokenMetadataKey = "service-token"; - - // Токен сценариев теста. - private const string ValidToken = "task3-test-token"; - - // Deadline RPC-вызовов теста (сек). - private const int RpcDeadlineSeconds = 10; - - /// - /// HealthCheck (grpc.health.v1.Health/Check) отвечает SERVING — хост поднялся, gRPC-инфраструктура - /// и health-сервис работают (Ruling 12; health освобождён от service-token). - /// - [Fact] - public async Task HealthCheck_ReturnsServing() - { - await RunHostScenarioAsync( - ValidToken, - async channel => - { - var health = new Health.HealthClient(channel); - HealthCheckResponse response = await health.CheckAsync( - new HealthCheckRequest(), - deadline: DateTime.UtcNow.AddSeconds(RpcDeadlineSeconds)); - - Assert.Equal(HealthCheckResponse.Types.ServingStatus.Serving, response.Status); - }); - } - - /// - /// Запрос без metadata «service-token» → UNAUTHENTICATED (Ruling 1). - /// - [Fact] - public async Task Status_WithoutToken_IsUnauthenticated() - { - await AssertDealRpcRejectedAsync( - ValidToken, - tokenHeader: null, - expected: StatusCode.Unauthenticated); - } - - /// - /// Запрос с неверным токеном → UNAUTHENTICATED (Ruling 1). - /// - [Fact] - public async Task Status_WithWrongToken_IsUnauthenticated() - { - await AssertDealRpcRejectedAsync( - ValidToken, - tokenHeader: "wrong-token", - expected: StatusCode.Unauthenticated); - } - - /// - /// Верный токен проходит интерцептор к методу Status (задачи 5–6): кодогенерация и маппинг - /// сервиса работают, свежая модель тенанта отвечает готовым ответом (ready=false), а не UNIMPLEMENTED. - /// - [Fact] - public async Task Status_WithValidToken_ReturnsEmptyStatus() - { - await RunHostScenarioAsync( - ValidToken, - async channel => - { - var client = new MlService.MlServiceClient(channel); - Metadata metadata = new(); - metadata.Add(ServiceTokenMetadataKey, ValidToken); - metadata.Add(TenantIdMetadataKey, TestTenantId); - - StatusReply response = await client.StatusAsync( - new StatusRequest(), - new CallOptions(metadata, deadline: DateTime.UtcNow.AddSeconds(RpcDeadlineSeconds))); - - Assert.False(response.Ready); - Assert.Empty(response.Classes); - Assert.Equal(0, response.Learned); - }); - } - - /// - /// Fail-closed (замечание ревью Task 2): DEAL_SERVICE_TOKEN не задан — Deal-RPC отклоняется всегда, - /// в т.ч. запрос с «пустым» значением metadata (без гарда сравнение «» == «» пропустило бы его); - /// health при этом продолжает отвечать SERVING (инфраструктурный liveness не ломается). - /// - [Fact] - public async Task WithoutConfiguredToken_DealRpcFailsClosed_HealthStillServing() - { - await RunHostScenarioAsync( - serviceToken: null, - async channel => - { - var health = new Health.HealthClient(channel); - HealthCheckResponse healthResponse = await health.CheckAsync( - new HealthCheckRequest(), - deadline: DateTime.UtcNow.AddSeconds(RpcDeadlineSeconds)); - Assert.Equal(HealthCheckResponse.Types.ServingStatus.Serving, healthResponse.Status); - - // Пустое значение metadata — попытка обойти fail-closed (равно незаданному env-токену). - await AssertRejectedAsync(channel, string.Empty, StatusCode.Unauthenticated); - await AssertRejectedAsync(channel, ValidToken, StatusCode.Unauthenticated); - }); - } - - // Прогоняет Status с заданным заголовком «service-token» (null — без заголовка) и проверяет - // ожидаемый код статуса RPC-исключения. - // serviceToken: Ожидаемый токен хоста (env DEAL_SERVICE_TOKEN). - // tokenHeader: Значение metadata «service-token» запроса либо null (нет заголовка). - // expected: Ожидаемый StatusCode ответа. - private static async Task AssertDealRpcRejectedAsync( - string? serviceToken, - string? tokenHeader, - StatusCode expected) - { - await RunHostScenarioAsync( - serviceToken, - channel => AssertRejectedAsync(channel, tokenHeader, expected)); - } - - // Вызывает Status и проверяет, что сервер ответил ожидаемым кодом статуса. - // channel: Канал к хосту ml-service. - // tokenHeader: Значение metadata «service-token» либо null (нет заголовка). - // expected: Ожидаемый StatusCode. - private static async Task AssertRejectedAsync( - GrpcChannel channel, - string? tokenHeader, - StatusCode expected) - { - var client = new MlService.MlServiceClient(channel); - Metadata metadata = new(); - if (tokenHeader is not null) - { - metadata.Add(ServiceTokenMetadataKey, tokenHeader); - } - - var callOptions = new CallOptions( - metadata, - deadline: DateTime.UtcNow.AddSeconds(RpcDeadlineSeconds)); - - AsyncUnaryCall call = client.StatusAsync(new StatusRequest(), callOptions); - RpcException exception = await Assert.ThrowsAsync(() => call.ResponseAsync); - - Assert.Equal(expected, exception.StatusCode); - } - - // Поднимает хост на эфемерном порту с заданным env-токеном, выполняет сценарий и гарантированно - // гасит хост/канал и восстанавливает исходный env. - // serviceToken: Значение env DEAL_SERVICE_TOKEN для сценария (null — убрать). - // scenario: Сценарий с каналом к поднятому хосту. - private static async Task RunHostScenarioAsync(string? serviceToken, Func scenario) - { - string? originalToken = Environment.GetEnvironmentVariable(ServiceTokenEnvKey); - string? originalDataDir = Environment.GetEnvironmentVariable(DataDirEnvKey); - - // Status реализован (задачи 5–6): модель тенанта создаёт SQLite-файл — каталог теста - // направляем во временную папку, чтобы файлы не попали в data/ml репозитория. - string dataDir = Path.Combine(Path.GetTempPath(), "deal-ml-host-tests", Guid.NewGuid().ToString("N")); - Directory.CreateDirectory(dataDir); - - Environment.SetEnvironmentVariable(ServiceTokenEnvKey, serviceToken); - Environment.SetEnvironmentVariable(DataDirEnvKey, dataDir); - - WebApplication? app = null; - GrpcChannel? channel = null; - try - { - int port = FreeTcpPort(); - app = MlServiceHost.Create(port); - await app.StartAsync(); - - channel = GrpcChannel.ForAddress($"http://127.0.0.1:{port}"); - await scenario(channel); - } - finally - { - if (channel is not null) - { - channel.Dispose(); - } - - if (app is not null) - { - await app.StopAsync(); - await app.DisposeAsync(); - } - - Environment.SetEnvironmentVariable(ServiceTokenEnvKey, originalToken); - Environment.SetEnvironmentVariable(DataDirEnvKey, originalDataDir); - - try - { - Directory.Delete(dataDir, recursive: true); - } - catch (IOException) - { - // Каталог мог быть занят на момент удаления — тестовый мусор в temp допустим. - } - } - } - - // Возвращает свободный TCP-порт (127.0.0.1:0 → освобождение перед биндом хоста). - private static int FreeTcpPort() - { - using var listener = new TcpListener(IPAddress.Loopback, 0); - listener.Start(); - return ((IPEndPoint)listener.LocalEndpoint).Port; - } -} +using Deal.Grpc.Ml; +using Deal.Ml; +using Microsoft.AspNetCore.Builder; +using Grpc.Core; +using Grpc.Health.V1; +using Grpc.Net.Client; +using System.Net; +using System.Net.Sockets; +using Xunit; + +namespace Deal.Ml.Tests.Grpc; + +/// +/// Интеграционные тесты хоста ml-service (каркас план Task 3 + Status задач 5–6). +/// +/// Хост поднимается В процессе теста (Kestrel HTTP/2, эфемерный порт) через MlServiceHost.Create — +/// ту же сборку хоста, что использует Program.cs, поэтому тесты покрывают реальную настройку +/// Kestrel/AddGrpc/health, а не её копию. Проверки: gRPC-health → SERVING; ServiceTokenInterceptor +/// (Ruling 1): запрос без токена и с неверным токеном → UNAUTHENTICATED; верный токен проходит к +/// реализованному Status (свежая модель → ready=false); при незаданном DEAL_SERVICE_TOKEN — fail-closed. +/// +/// Токен интерцептор читает из конфигурации (env DEAL_SERVICE_TOKEN), Status создаёт модель тенанта — +/// каталог моделей (DEAL_ML_DATA_DIR) на время сценария направляется во временную папку; тесты +/// выставляют env и восстанавливают исходные значения. Все тесты класса живут в одном процессе/классе, +/// чтобы env и свободные порты не конфликтовали (xunit исполняет методы класса последовательно). +/// +public sealed class MlServiceHostTests +{ + // Env-ключ ожидаемого токена (зеркало ServiceTokenInterceptor). + private const string ServiceTokenEnvKey = "DEAL_SERVICE_TOKEN"; + + // Env-ключ каталога моделей (зеркало MlOptions; Status создаёт модель тенанта). + private const string DataDirEnvKey = "DEAL_ML_DATA_DIR"; + + // Id тенанта сценариев теста (Status реализован задачами 5–6). + private const string TestTenantId = "tenant-test"; + + // Ключ gRPC-metadata с tenant-id (зеркало MlServiceImpl.TenantIdMetadataKey). + private const string TenantIdMetadataKey = "tenant-id"; + + // Ключ gRPC-metadata (зеркало ServiceTokenInterceptor.ServiceTokenMetadataKey). + private const string ServiceTokenMetadataKey = "service-token"; + + // Токен сценариев теста. + private const string ValidToken = "task3-test-token"; + + // Deadline RPC-вызовов теста (сек). + private const int RpcDeadlineSeconds = 10; + + /// + /// HealthCheck (grpc.health.v1.Health/Check) отвечает SERVING — хост поднялся, gRPC-инфраструктура + /// и health-сервис работают (Ruling 12; health освобождён от service-token). + /// + [Fact] + public async Task HealthCheck_ReturnsServing() + { + await RunHostScenarioAsync( + ValidToken, + async channel => + { + var health = new Health.HealthClient(channel); + HealthCheckResponse response = await health.CheckAsync( + new HealthCheckRequest(), + deadline: DateTime.UtcNow.AddSeconds(RpcDeadlineSeconds)); + + Assert.Equal(HealthCheckResponse.Types.ServingStatus.Serving, response.Status); + }); + } + + /// + /// Запрос без metadata «service-token» → UNAUTHENTICATED (Ruling 1). + /// + [Fact] + public async Task Status_WithoutToken_IsUnauthenticated() + { + await AssertDealRpcRejectedAsync( + ValidToken, + tokenHeader: null, + expected: StatusCode.Unauthenticated); + } + + /// + /// Запрос с неверным токеном → UNAUTHENTICATED (Ruling 1). + /// + [Fact] + public async Task Status_WithWrongToken_IsUnauthenticated() + { + await AssertDealRpcRejectedAsync( + ValidToken, + tokenHeader: "wrong-token", + expected: StatusCode.Unauthenticated); + } + + /// + /// Верный токен проходит интерцептор к методу Status (задачи 5–6): кодогенерация и маппинг + /// сервиса работают, свежая модель тенанта отвечает готовым ответом (ready=false), а не UNIMPLEMENTED. + /// + [Fact] + public async Task Status_WithValidToken_ReturnsEmptyStatus() + { + await RunHostScenarioAsync( + ValidToken, + async channel => + { + var client = new MlService.MlServiceClient(channel); + Metadata metadata = new(); + metadata.Add(ServiceTokenMetadataKey, ValidToken); + metadata.Add(TenantIdMetadataKey, TestTenantId); + + StatusReply response = await client.StatusAsync( + new StatusRequest(), + new CallOptions(metadata, deadline: DateTime.UtcNow.AddSeconds(RpcDeadlineSeconds))); + + Assert.False(response.Ready); + Assert.Empty(response.Classes); + Assert.Equal(0, response.Learned); + }); + } + + /// + /// Fail-closed (замечание ревью Task 2): DEAL_SERVICE_TOKEN не задан — Deal-RPC отклоняется всегда, + /// в т.ч. запрос с «пустым» значением metadata (без гарда сравнение «» == «» пропустило бы его); + /// health при этом продолжает отвечать SERVING (инфраструктурный liveness не ломается). + /// + [Fact] + public async Task WithoutConfiguredToken_DealRpcFailsClosed_HealthStillServing() + { + await RunHostScenarioAsync( + serviceToken: null, + async channel => + { + var health = new Health.HealthClient(channel); + HealthCheckResponse healthResponse = await health.CheckAsync( + new HealthCheckRequest(), + deadline: DateTime.UtcNow.AddSeconds(RpcDeadlineSeconds)); + Assert.Equal(HealthCheckResponse.Types.ServingStatus.Serving, healthResponse.Status); + + // Пустое значение metadata — попытка обойти fail-closed (равно незаданному env-токену). + await AssertRejectedAsync(channel, string.Empty, StatusCode.Unauthenticated); + await AssertRejectedAsync(channel, ValidToken, StatusCode.Unauthenticated); + }); + } + + // Прогоняет Status с заданным заголовком «service-token» (null — без заголовка) и проверяет + // ожидаемый код статуса RPC-исключения. + // serviceToken: Ожидаемый токен хоста (env DEAL_SERVICE_TOKEN). + // tokenHeader: Значение metadata «service-token» запроса либо null (нет заголовка). + // expected: Ожидаемый StatusCode ответа. + private static async Task AssertDealRpcRejectedAsync( + string? serviceToken, + string? tokenHeader, + StatusCode expected) + { + await RunHostScenarioAsync( + serviceToken, + channel => AssertRejectedAsync(channel, tokenHeader, expected)); + } + + // Вызывает Status и проверяет, что сервер ответил ожидаемым кодом статуса. + // channel: Канал к хосту ml-service. + // tokenHeader: Значение metadata «service-token» либо null (нет заголовка). + // expected: Ожидаемый StatusCode. + private static async Task AssertRejectedAsync( + GrpcChannel channel, + string? tokenHeader, + StatusCode expected) + { + var client = new MlService.MlServiceClient(channel); + Metadata metadata = new(); + if (tokenHeader is not null) + { + metadata.Add(ServiceTokenMetadataKey, tokenHeader); + } + + var callOptions = new CallOptions( + metadata, + deadline: DateTime.UtcNow.AddSeconds(RpcDeadlineSeconds)); + + AsyncUnaryCall call = client.StatusAsync(new StatusRequest(), callOptions); + RpcException exception = await Assert.ThrowsAsync(() => call.ResponseAsync); + + Assert.Equal(expected, exception.StatusCode); + } + + // Поднимает хост на эфемерном порту с заданным env-токеном, выполняет сценарий и гарантированно + // гасит хост/канал и восстанавливает исходный env. + // serviceToken: Значение env DEAL_SERVICE_TOKEN для сценария (null — убрать). + // scenario: Сценарий с каналом к поднятому хосту. + private static async Task RunHostScenarioAsync(string? serviceToken, Func scenario) + { + string? originalToken = Environment.GetEnvironmentVariable(ServiceTokenEnvKey); + string? originalDataDir = Environment.GetEnvironmentVariable(DataDirEnvKey); + + // Status реализован (задачи 5–6): модель тенанта создаёт SQLite-файл — каталог теста + // направляем во временную папку, чтобы файлы не попали в data/ml репозитория. + string dataDir = Path.Combine(Path.GetTempPath(), "deal-ml-host-tests", Guid.NewGuid().ToString("N")); + Directory.CreateDirectory(dataDir); + + Environment.SetEnvironmentVariable(ServiceTokenEnvKey, serviceToken); + Environment.SetEnvironmentVariable(DataDirEnvKey, dataDir); + + WebApplication? app = null; + GrpcChannel? channel = null; + try + { + int port = FreeTcpPort(); + app = MlServiceHost.Create(port); + await app.StartAsync(); + + channel = GrpcChannel.ForAddress($"http://127.0.0.1:{port}"); + await scenario(channel); + } + finally + { + if (channel is not null) + { + channel.Dispose(); + } + + if (app is not null) + { + await app.StopAsync(); + await app.DisposeAsync(); + } + + Environment.SetEnvironmentVariable(ServiceTokenEnvKey, originalToken); + Environment.SetEnvironmentVariable(DataDirEnvKey, originalDataDir); + + try + { + Directory.Delete(dataDir, recursive: true); + } + catch (IOException) + { + // Каталог мог быть занят на момент удаления — тестовый мусор в temp допустим. + } + } + } + + // Возвращает свободный TCP-порт (127.0.0.1:0 → освобождение перед биндом хоста). + private static int FreeTcpPort() + { + using var listener = new TcpListener(IPAddress.Loopback, 0); + listener.Start(); + return ((IPEndPoint)listener.LocalEndpoint).Port; + } +} diff --git a/src/ml-service/Deal.Ml.Tests/MlTestHost.cs b/src/ml-service/Deal.Ml.Tests/Grpc/MlTestHost.cs similarity index 97% rename from src/ml-service/Deal.Ml.Tests/MlTestHost.cs rename to src/ml-service/Deal.Ml.Tests/Grpc/MlTestHost.cs index dfbd1ef..c4c2a9f 100644 --- a/src/ml-service/Deal.Ml.Tests/MlTestHost.cs +++ b/src/ml-service/Deal.Ml.Tests/Grpc/MlTestHost.cs @@ -1,144 +1,144 @@ -using Deal.Grpc.Hosting; -using Deal.Ml; -using Deal.Ml.Model; -using Deal.Ml.Extensions; -using Grpc.Core; -using Grpc.Net.Client; -using Microsoft.AspNetCore.Builder; -using Microsoft.Extensions.DependencyInjection; -using System.Net; -using System.Net.Sockets; - -namespace Deal.Ml.Tests; - -// Общий харнесс RPC-тестов ml-service (план Task 6): поднимает хост (MlServiceHost.Create) в -// процессе теста на эфемерном порту и направляет каталог моделей (DEAL_ML_DATA_DIR) во временную -// папку — файлы data/ml/<tenant>.sqlite тестов не попадают в репозиторий. Исходные значения -// env восстанавливаются; каталог удаляется после сценария (шаблон TelegramTestHost). -internal static class MlTestHost -{ - /// - /// Env-ключ ожидаемого service-token (зеркало ServiceTokenInterceptor). - /// - public const string ServiceTokenEnvKey = "DEAL_SERVICE_TOKEN"; - - /// - /// Env-ключ каталога моделей (зеркало MlOptions.DataDirEnvVarName). - /// - public const string DataDirEnvKey = MlOptions.DataDirEnvVarName; - - /// - /// Ключ gRPC-metadata с service-token (зеркало ServiceTokenInterceptor). - /// - public const string ServiceTokenMetadataKey = ServiceTokenInterceptor.ServiceTokenMetadataKey; - - /// - /// Ключ gRPC-metadata с tenant-id (зеркало MlServiceImpl.TenantIdMetadataKey). - /// - public const string TenantIdMetadataKey = MlServiceImpl.TenantIdMetadataKey; - - /// - /// Токен сценариев теста. - /// - public const string DefaultToken = "deal-ml-test-token"; - - /// - /// Id тенанта сценариев по умолчанию. - /// - public const string DefaultTenantId = "tenant-test"; - - /// - /// Deadline RPC-вызовов теста (сек). - /// - public const int RpcDeadlineSeconds = 15; - - /// - /// Прогоняет сценарий против поднятого хоста с заданным env-токеном и тестовым каталогом моделей. - /// - /// Значение env DEAL_SERVICE_TOKEN (null — убрать переменную). - /// Сценарий с gRPC-каналом к хосту. - public static async Task RunAsync(string? serviceToken, Func scenario) - { - string? originalToken = Environment.GetEnvironmentVariable(ServiceTokenEnvKey); - string? originalDataDir = Environment.GetEnvironmentVariable(DataDirEnvKey); - - string dataDir = Path.Combine(Path.GetTempPath(), "deal-ml-tests", Guid.NewGuid().ToString("N")); - Directory.CreateDirectory(dataDir); - - Environment.SetEnvironmentVariable(ServiceTokenEnvKey, serviceToken); - Environment.SetEnvironmentVariable(DataDirEnvKey, dataDir); - - WebApplication? app = null; - GrpcChannel? channel = null; - try - { - int port = FreeTcpPort(); - app = MlServiceHost.Create(port); - await app.StartAsync(); - - channel = GrpcChannel.ForAddress($"http://127.0.0.1:{port}"); - await scenario(channel); - } - finally - { - if (channel is not null) - { - channel.Dispose(); - } - - if (app is not null) - { - await app.StopAsync(); - await app.DisposeAsync(); - } - - Environment.SetEnvironmentVariable(ServiceTokenEnvKey, originalToken); - Environment.SetEnvironmentVariable(DataDirEnvKey, originalDataDir); - - try - { - Directory.Delete(dataDir, recursive: true); - } - catch (IOException) - { - // Каталог мог быть занят на момент удаления — тестовый мусор в temp допустим. - } - } - } - - /// - /// Строит metadata вызова: service-token (+ tenant-id, если задан). - /// - /// Значение заголовка service-token. - /// Id тенанта (null — без заголовка tenant-id). - public static Metadata CallMetadata(string? serviceToken, string? tenantId = null) - { - var metadata = new Metadata(); - if (serviceToken is not null) - { - metadata.Add(ServiceTokenMetadataKey, serviceToken); - } - - if (tenantId is not null) - { - metadata.Add(TenantIdMetadataKey, tenantId); - } - - return metadata; - } - - /// - /// CallOptions RPC: metadata + deadline (рекомендации README src/contracts L62–73). - /// - /// Metadata вызова. - public static CallOptions CallOptions(Metadata metadata) - => new(metadata, deadline: DateTime.UtcNow.AddSeconds(RpcDeadlineSeconds)); - - // Возвращает свободный TCP-порт (127.0.0.1:0 → освобождение перед биндом хоста). - private static int FreeTcpPort() - { - using var listener = new TcpListener(IPAddress.Loopback, 0); - listener.Start(); - return ((IPEndPoint)listener.LocalEndpoint).Port; - } -} +using Deal.Grpc.Hosting; +using Deal.Ml; +using Deal.Ml.Model; +using Deal.Ml.Extensions; +using Grpc.Core; +using Grpc.Net.Client; +using Microsoft.AspNetCore.Builder; +using Microsoft.Extensions.DependencyInjection; +using System.Net; +using System.Net.Sockets; + +namespace Deal.Ml.Tests.Grpc; + +// Общий харнесс RPC-тестов ml-service (план Task 6): поднимает хост (MlServiceHost.Create) в +// процессе теста на эфемерном порту и направляет каталог моделей (DEAL_ML_DATA_DIR) во временную +// папку — файлы data/ml/<tenant>.sqlite тестов не попадают в репозиторий. Исходные значения +// env восстанавливаются; каталог удаляется после сценария (шаблон TelegramTestHost). +internal static class MlTestHost +{ + /// + /// Env-ключ ожидаемого service-token (зеркало ServiceTokenInterceptor). + /// + public const string ServiceTokenEnvKey = "DEAL_SERVICE_TOKEN"; + + /// + /// Env-ключ каталога моделей (зеркало MlOptions.DataDirEnvVarName). + /// + public const string DataDirEnvKey = MlOptions.DataDirEnvVarName; + + /// + /// Ключ gRPC-metadata с service-token (зеркало ServiceTokenInterceptor). + /// + public const string ServiceTokenMetadataKey = ServiceTokenInterceptor.ServiceTokenMetadataKey; + + /// + /// Ключ gRPC-metadata с tenant-id (зеркало MlServiceImpl.TenantIdMetadataKey). + /// + public const string TenantIdMetadataKey = MlServiceImpl.TenantIdMetadataKey; + + /// + /// Токен сценариев теста. + /// + public const string DefaultToken = "deal-ml-test-token"; + + /// + /// Id тенанта сценариев по умолчанию. + /// + public const string DefaultTenantId = "tenant-test"; + + /// + /// Deadline RPC-вызовов теста (сек). + /// + public const int RpcDeadlineSeconds = 15; + + /// + /// Прогоняет сценарий против поднятого хоста с заданным env-токеном и тестовым каталогом моделей. + /// + /// Значение env DEAL_SERVICE_TOKEN (null — убрать переменную). + /// Сценарий с gRPC-каналом к хосту. + public static async Task RunAsync(string? serviceToken, Func scenario) + { + string? originalToken = Environment.GetEnvironmentVariable(ServiceTokenEnvKey); + string? originalDataDir = Environment.GetEnvironmentVariable(DataDirEnvKey); + + string dataDir = Path.Combine(Path.GetTempPath(), "deal-ml-tests", Guid.NewGuid().ToString("N")); + Directory.CreateDirectory(dataDir); + + Environment.SetEnvironmentVariable(ServiceTokenEnvKey, serviceToken); + Environment.SetEnvironmentVariable(DataDirEnvKey, dataDir); + + WebApplication? app = null; + GrpcChannel? channel = null; + try + { + int port = FreeTcpPort(); + app = MlServiceHost.Create(port); + await app.StartAsync(); + + channel = GrpcChannel.ForAddress($"http://127.0.0.1:{port}"); + await scenario(channel); + } + finally + { + if (channel is not null) + { + channel.Dispose(); + } + + if (app is not null) + { + await app.StopAsync(); + await app.DisposeAsync(); + } + + Environment.SetEnvironmentVariable(ServiceTokenEnvKey, originalToken); + Environment.SetEnvironmentVariable(DataDirEnvKey, originalDataDir); + + try + { + Directory.Delete(dataDir, recursive: true); + } + catch (IOException) + { + // Каталог мог быть занят на момент удаления — тестовый мусор в temp допустим. + } + } + } + + /// + /// Строит metadata вызова: service-token (+ tenant-id, если задан). + /// + /// Значение заголовка service-token. + /// Id тенанта (null — без заголовка tenant-id). + public static Metadata CallMetadata(string? serviceToken, string? tenantId = null) + { + var metadata = new Metadata(); + if (serviceToken is not null) + { + metadata.Add(ServiceTokenMetadataKey, serviceToken); + } + + if (tenantId is not null) + { + metadata.Add(TenantIdMetadataKey, tenantId); + } + + return metadata; + } + + /// + /// CallOptions RPC: metadata + deadline (рекомендации README src/contracts L62–73). + /// + /// Metadata вызова. + public static CallOptions CallOptions(Metadata metadata) + => new(metadata, deadline: DateTime.UtcNow.AddSeconds(RpcDeadlineSeconds)); + + // Возвращает свободный TCP-порт (127.0.0.1:0 → освобождение перед биндом хоста). + private static int FreeTcpPort() + { + using var listener = new TcpListener(IPAddress.Loopback, 0); + listener.Start(); + return ((IPEndPoint)listener.LocalEndpoint).Port; + } +} diff --git a/src/ml-service/Deal.Ml.Tests/LearningData.cs b/src/ml-service/Deal.Ml.Tests/Ml/LearningData.cs similarity index 97% rename from src/ml-service/Deal.Ml.Tests/LearningData.cs rename to src/ml-service/Deal.Ml.Tests/Ml/LearningData.cs index 6eab0aa..f725b81 100644 --- a/src/ml-service/Deal.Ml.Tests/LearningData.cs +++ b/src/ml-service/Deal.Ml.Tests/Ml/LearningData.cs @@ -1,105 +1,105 @@ -using Deal.Ml.Model; -using Deal.Ml.Extensions; - -namespace Deal.Ml.Tests; - -// Тексты и батчи обучения для тестов модели/RPC (1:1 сценарии приёмки плана Task 5: -// «нужен middle python…» → колонка + тип, «резюме…» → spam после обучения). Словари классов -// намеренно не пересекаются: колонка/тип разработки (Dev), разовая сделка (Order) и спам — -// разные термины, чтобы предсказания были детерминированы порогами, а не шумом пересечений. -internal static class LearningData -{ - /// - /// Id колонки канбана «разработка/найм» (форма b_… как в ядре). - /// - public const string ColumnDev = "b_col_dev"; - - /// - /// Id колонки канбана «разовая сделка/заказ». - /// - public const string ColumnOrder = "b_col_order"; - - /// - /// Внутренний класс типа «найм». - /// - public const string TypeHire = "t:hire"; - - /// - /// Внутренний класс типа «разовая сделка». - /// - public const string TypeOrder = "t:order"; - - /// - /// Сообщение-заявка на разработчика (→ колонка b_col_dev + тип hire). - /// - public const string DevMessage = - "нужен middle python разработчик в команду, удаленная работа, стек django postgres, оффер конкурентный"; - - /// - /// Сообщение-заказ сайта/лендинга (→ колонка b_col_order + тип order). - /// - public const string OrderMessage = - "закажу сайт визитку и лендинг для малого бизнеса, недорого, дизайнер и верстка"; - - /// - /// Спам-резюме/рассылка (→ колонка spam; словарь не пересекается с dev/order). - /// - public const string SpamMessage = - "резюме ищу подработку, разошлю отклик по вакансиям, рассылка кадровым агентствам"; - - /// - /// Пример обучения с заданным весом сигнала (пользователь/ИИ/правила). - /// - /// Метка класса. - /// Текст примера. - /// Вес сигнала. - public static LearnItem Signal( - string label, - string text, - double delta) => new(text, label, delta); - - /// - /// Пример обучения с дельтой 1.0 (действие пользователя). - /// - /// Метка класса. - /// Текст примера. - public static LearnItem User(string label, string text) => new(text, label, 1.0); - - /// - /// Канонический батч, доводящий модель до готовности и уверенных предсказаний (28 примеров): - /// колонки 8/6/6 (dev/order/spam), типы t:hire/t:order по 4, после «включения» — 4 реальных - /// действия (delta=1) для журнала самооценки. Баланс 1:1 со сценарием приёмки Task 5. - /// - public static List CanonicalTrainItems() - { - var items = new List(); - AddRepeated(items, User(ColumnDev, DevMessage), 6); - AddRepeated(items, User(ColumnOrder, OrderMessage), 6); - AddRepeated(items, User(LearningData.SpamLabel, SpamMessage), 4); - AddRepeated(items, User(TypeHire, DevMessage), 4); - AddRepeated(items, User(TypeOrder, OrderMessage), 4); - AddRepeated(items, User(ColumnDev, DevMessage), 2); - AddRepeated(items, User(LearningData.SpamLabel, SpamMessage), 2); - return items; - } - - /// - /// Метка spam (внутренняя константа модели; зеркало для читаемости батчей). - /// - public static string SpamLabel => ModelConstants.SpamLabel; - - // Добавляет count копий примера в список. - // target: Список-приёмник. - // item: Пример. - // count: Число копий. - private static void AddRepeated( - List target, - LearnItem item, - int count) - { - for (int i = 0; i < count; i++) - { - target.Add(item); - } - } -} +using Deal.Ml.Model; +using Deal.Ml.Extensions; + +namespace Deal.Ml.Tests.Ml; + +// Тексты и батчи обучения для тестов модели/RPC (1:1 сценарии приёмки плана Task 5: +// «нужен middle python…» → колонка + тип, «резюме…» → spam после обучения). Словари классов +// намеренно не пересекаются: колонка/тип разработки (Dev), разовая сделка (Order) и спам — +// разные термины, чтобы предсказания были детерминированы порогами, а не шумом пересечений. +internal static class LearningData +{ + /// + /// Id колонки канбана «разработка/найм» (форма b_… как в ядре). + /// + public const string ColumnDev = "b_col_dev"; + + /// + /// Id колонки канбана «разовая сделка/заказ». + /// + public const string ColumnOrder = "b_col_order"; + + /// + /// Внутренний класс типа «найм». + /// + public const string TypeHire = "t:hire"; + + /// + /// Внутренний класс типа «разовая сделка». + /// + public const string TypeOrder = "t:order"; + + /// + /// Сообщение-заявка на разработчика (→ колонка b_col_dev + тип hire). + /// + public const string DevMessage = + "нужен middle python разработчик в команду, удаленная работа, стек django postgres, оффер конкурентный"; + + /// + /// Сообщение-заказ сайта/лендинга (→ колонка b_col_order + тип order). + /// + public const string OrderMessage = + "закажу сайт визитку и лендинг для малого бизнеса, недорого, дизайнер и верстка"; + + /// + /// Спам-резюме/рассылка (→ колонка spam; словарь не пересекается с dev/order). + /// + public const string SpamMessage = + "резюме ищу подработку, разошлю отклик по вакансиям, рассылка кадровым агентствам"; + + /// + /// Пример обучения с заданным весом сигнала (пользователь/ИИ/правила). + /// + /// Метка класса. + /// Текст примера. + /// Вес сигнала. + public static LearnItem Signal( + string label, + string text, + double delta) => new(text, label, delta); + + /// + /// Пример обучения с дельтой 1.0 (действие пользователя). + /// + /// Метка класса. + /// Текст примера. + public static LearnItem User(string label, string text) => new(text, label, 1.0); + + /// + /// Канонический батч, доводящий модель до готовности и уверенных предсказаний (28 примеров): + /// колонки 8/6/6 (dev/order/spam), типы t:hire/t:order по 4, после «включения» — 4 реальных + /// действия (delta=1) для журнала самооценки. Баланс 1:1 со сценарием приёмки Task 5. + /// + public static List CanonicalTrainItems() + { + var items = new List(); + AddRepeated(items, User(ColumnDev, DevMessage), 6); + AddRepeated(items, User(ColumnOrder, OrderMessage), 6); + AddRepeated(items, User(LearningData.SpamLabel, SpamMessage), 4); + AddRepeated(items, User(TypeHire, DevMessage), 4); + AddRepeated(items, User(TypeOrder, OrderMessage), 4); + AddRepeated(items, User(ColumnDev, DevMessage), 2); + AddRepeated(items, User(LearningData.SpamLabel, SpamMessage), 2); + return items; + } + + /// + /// Метка spam (внутренняя константа модели; зеркало для читаемости батчей). + /// + public static string SpamLabel => ModelConstants.SpamLabel; + + // Добавляет count копий примера в список. + // target: Список-приёмник. + // item: Пример. + // count: Число копий. + private static void AddRepeated( + List target, + LearnItem item, + int count) + { + for (int i = 0; i < count; i++) + { + target.Add(item); + } + } +} diff --git a/src/ml-service/Deal.Ml.Tests/MlTokenizerTests.cs b/src/ml-service/Deal.Ml.Tests/Ml/MlTokenizerTests.cs similarity index 96% rename from src/ml-service/Deal.Ml.Tests/MlTokenizerTests.cs rename to src/ml-service/Deal.Ml.Tests/Ml/MlTokenizerTests.cs index 01a5422..1bcd8bc 100644 --- a/src/ml-service/Deal.Ml.Tests/MlTokenizerTests.cs +++ b/src/ml-service/Deal.Ml.Tests/Ml/MlTokenizerTests.cs @@ -1,78 +1,78 @@ -using Deal.Ml.Model; -using Deal.Ml.Extensions; - -namespace Deal.Ml.Tests; - -/// -/// Тесты токенизации (план Task 5; 1:1 mlservice/model.py tokenize L78–87): удаление ссылок -/// (включая markdown), lowercase, отбрасывание слов короче 3, «хвостовые» ~prefix-термины для -/// слов длиной ≥ 6, разрешённый алфавит [a-zа-яё0-9@+.#]. -/// -public sealed class MlTokenizerTests -{ - /// - /// Базовое: lowercase, дефис разделяет термины, длинные слова дают ~prefix. - /// - [Fact] - public void Tokenize_LowercasesAndAddsPrefixes() - { - string[] tokens = MlTokenizer.Tokenize("Нужен Python-разработчик"); - - Assert.Equal(new[] { "нужен", "python", "~pyth", "разработчик", "~разр" }, tokens); - } - - /// - /// Ссылки (http/www/markdown) не дают терминов и не ломают соседние слова. - /// - [Fact] - public void Tokenize_RemovesLinksAndMarkdownLinks() - { - string text = "Ссылка https://example.com/page?utm_source=x и www.example.org [текст](https://x.ru/a) конец"; - - string[] tokens = MlTokenizer.Tokenize(text); - - Assert.Equal(new[] { "ссылка", "~ссыл", "конец" }, tokens); - } - - /// - /// Слова короче 3 символов отбрасываются (пустой результат для «а б вг»). - /// - [Fact] - public void Tokenize_DropsShortWords() - { - Assert.Empty(MlTokenizer.Tokenize("а б вг")); - } - - /// - /// Кириллица/ё обрабатываются как в python: lower + префикс первых 4 символов. - /// - [Fact] - public void Tokenize_CyrillicWithYo() - { - string[] tokens = MlTokenizer.Tokenize("Программист Ёжик"); - - Assert.Equal(new[] { "программист", "~прог", "ёжик" }, tokens); - } - - /// - /// Разрешённый алфавит включает @ + . # — email остаётся одним термином (1:1 python). - /// - [Fact] - public void Tokenize_KeepsEmailAsSingleTerm() - { - string[] tokens = MlTokenizer.Tokenize("привет@mail.ru"); - - Assert.Equal(new[] { "привет@mail.ru", "~прив" }, tokens); - } - - /// - /// Null/пустой текст — пустой набор терминов. - /// - [Fact] - public void Tokenize_NullOrEmpty_ReturnsEmpty() - { - Assert.Empty(MlTokenizer.Tokenize(null)); - Assert.Empty(MlTokenizer.Tokenize(string.Empty)); - Assert.Empty(MlTokenizer.Tokenize(" ")); - } -} +using Deal.Ml.Model; +using Deal.Ml.Extensions; + +namespace Deal.Ml.Tests.Ml; + +/// +/// Тесты токенизации (план Task 5; 1:1 mlservice/model.py tokenize L78–87): удаление ссылок +/// (включая markdown), lowercase, отбрасывание слов короче 3, «хвостовые» ~prefix-термины для +/// слов длиной ≥ 6, разрешённый алфавит [a-zа-яё0-9@+.#]. +/// +public sealed class MlTokenizerTests +{ + /// + /// Базовое: lowercase, дефис разделяет термины, длинные слова дают ~prefix. + /// + [Fact] + public void Tokenize_LowercasesAndAddsPrefixes() + { + string[] tokens = MlTokenizer.Tokenize("Нужен Python-разработчик"); + + Assert.Equal(new[] { "нужен", "python", "~pyth", "разработчик", "~разр" }, tokens); + } + + /// + /// Ссылки (http/www/markdown) не дают терминов и не ломают соседние слова. + /// + [Fact] + public void Tokenize_RemovesLinksAndMarkdownLinks() + { + string text = "Ссылка https://example.com/page?utm_source=x и www.example.org [текст](https://x.ru/a) конец"; + + string[] tokens = MlTokenizer.Tokenize(text); + + Assert.Equal(new[] { "ссылка", "~ссыл", "конец" }, tokens); + } + + /// + /// Слова короче 3 символов отбрасываются (пустой результат для «а б вг»). + /// + [Fact] + public void Tokenize_DropsShortWords() + { + Assert.Empty(MlTokenizer.Tokenize("а б вг")); + } + + /// + /// Кириллица/ё обрабатываются как в python: lower + префикс первых 4 символов. + /// + [Fact] + public void Tokenize_CyrillicWithYo() + { + string[] tokens = MlTokenizer.Tokenize("Программист Ёжик"); + + Assert.Equal(new[] { "программист", "~прог", "ёжик" }, tokens); + } + + /// + /// Разрешённый алфавит включает @ + . # — email остаётся одним термином (1:1 python). + /// + [Fact] + public void Tokenize_KeepsEmailAsSingleTerm() + { + string[] tokens = MlTokenizer.Tokenize("привет@mail.ru"); + + Assert.Equal(new[] { "привет@mail.ru", "~прив" }, tokens); + } + + /// + /// Null/пустой текст — пустой набор терминов. + /// + [Fact] + public void Tokenize_NullOrEmpty_ReturnsEmpty() + { + Assert.Empty(MlTokenizer.Tokenize(null)); + Assert.Empty(MlTokenizer.Tokenize(string.Empty)); + Assert.Empty(MlTokenizer.Tokenize(" ")); + } +} diff --git a/src/ml-service/Deal.Ml.Tests/ModelPersistenceTests.cs b/src/ml-service/Deal.Ml.Tests/Ml/ModelPersistenceTests.cs similarity index 97% rename from src/ml-service/Deal.Ml.Tests/ModelPersistenceTests.cs rename to src/ml-service/Deal.Ml.Tests/Ml/ModelPersistenceTests.cs index 8175786..27358fa 100644 --- a/src/ml-service/Deal.Ml.Tests/ModelPersistenceTests.cs +++ b/src/ml-service/Deal.Ml.Tests/Ml/ModelPersistenceTests.cs @@ -1,160 +1,160 @@ -using Deal.Ml.Model; -using Deal.Ml.Extensions; -using Microsoft.Data.Sqlite; - -namespace Deal.Ml.Tests; - -/// -/// Тесты персистентности модели (план Task 5): веса переживают перезапуск пула (второй инстанс -/// на тот же SQLite-файл), журнал самооценки прунится до EVAL_KEEP=200, статус отдаёт окно -/// EVAL_WINDOW=50 последних решений. -/// -public sealed class ModelPersistenceTests -{ - /// - /// Перезапуск пула сохраняет веса: обучение на первом пуле → закрытие → второй пул на тот же - /// каталог отдаёт те же классы/learned/eval и те же предсказания. - /// - [Fact] - public void SecondPoolOnSameFile_ReloadsWeightsAndEval() - { - string dataDir = NewDataDir(); - try - { - MlStatusResult firstStatus; - MlPredictResult firstPredict; - using (var firstPool = new ModelPool(MlOptions.Create(dataDir))) - { - TenantModel model = firstPool.GetOrCreate("tenant-persist"); - List items = LearningData.CanonicalTrainItems(); - model.LearnBatch(items.Take(24).ToList()); - model.LearnBatch(items.Skip(24).ToList()); - - firstStatus = model.Status(); - firstPredict = model.Predict(LearningData.DevMessage); - } - - using var secondPool = new ModelPool(MlOptions.Create(dataDir)); - TenantModel reloaded = secondPool.GetOrCreate("tenant-persist"); - - MlStatusResult secondStatus = reloaded.Status(); - MlPredictResult secondPredict = reloaded.Predict(LearningData.DevMessage); - - Assert.True(secondStatus.Ready); - Assert.Equal(firstStatus.Learned, secondStatus.Learned); - Assert.Equal(firstStatus.Classes, secondStatus.Classes); - Assert.Equal(firstStatus.Eval, secondStatus.Eval); - Assert.True(secondPredict.Take); - Assert.Equal(firstPredict.Label, secondPredict.Label); - Assert.Equal(firstPredict.Scores, secondPredict.Scores); - } - finally - { - TryDelete(dataDir); - } - } - - /// - /// Окно самооценки: журнал хранит не больше EVAL_KEEP (200) строк (проверка в файле), статус - /// считает последние EVAL_WINDOW (50) решений. Сценарий — 250 дополнительных «реальных» - /// действий поверх канонического батча. - /// - [Fact] - public void EvalLog_PrunesToKeepAndWindowIsFifty() - { - string dataDir = NewDataDir(); - try - { - TenantModel model; - using (var pool = new ModelPool(MlOptions.Create(dataDir))) - { - model = pool.GetOrCreate("tenant-eval-window"); - List items = LearningData.CanonicalTrainItems(); - model.LearnBatch(items.Take(24).ToList()); - model.LearnBatch(items.Skip(24).ToList()); - - // 250 реальных действий (delta=1, метка совпадает с предсказанием) — самооценка пишется. - model.LearnBatch( - Enumerable.Repeat(LearningData.User(LearningData.ColumnDev, LearningData.DevMessage), 250).ToList()); - - MlStatusResult status = model.Status(); - - Assert.Equal(50, status.Eval.Count); // окно — последние EVAL_WINDOW - Assert.Equal(50, status.Eval.Correct); - Assert.Equal(1.0, status.Eval.Accuracy); - - Assert.Equal(200, CountEvalRows(model.DatabasePath)); // журнал прунится до EVAL_KEEP - } - } - finally - { - TryDelete(dataDir); - } - } - - /// - /// Прунинг переживает перезапуск: reload держит последние EVAL_KEEP и окно 50. - /// - [Fact] - public void EvalLog_PrunePersistedAcrossPoolRestart() - { - string dataDir = NewDataDir(); - try - { - using (var pool = new ModelPool(MlOptions.Create(dataDir))) - { - TenantModel model = pool.GetOrCreate("tenant-eval-restart"); - List items = LearningData.CanonicalTrainItems(); - model.LearnBatch(items.Take(24).ToList()); - model.LearnBatch(items.Skip(24).ToList()); - model.LearnBatch( - Enumerable.Repeat(LearningData.User(LearningData.ColumnDev, LearningData.DevMessage), 250).ToList()); - } - - using var secondPool = new ModelPool(MlOptions.Create(dataDir)); - TenantModel reloaded = secondPool.GetOrCreate("tenant-eval-restart"); - - MlStatusResult status = reloaded.Status(); - Assert.Equal(50, status.Eval.Count); - Assert.Equal(1.0, status.Eval.Accuracy); - Assert.Equal(200, CountEvalRows(reloaded.DatabasePath)); - } - finally - { - TryDelete(dataDir); - } - } - - // Читает число строк eval_log прямо из SQLite-файла модели. - // dbPath: Путь к файлу модели. - private static long CountEvalRows(string dbPath) - { - using var connection = new SqliteConnection($"Data Source={dbPath}"); - connection.Open(); - using SqliteCommand command = connection.CreateCommand(); - command.CommandText = "SELECT count(*) FROM eval_log"; - return (long)command.ExecuteScalar()!; - } - - // Новая временная папка данных моделей. - private static string NewDataDir() - { - string dataDir = Path.Combine(Path.GetTempPath(), "deal-ml-persist-tests", Guid.NewGuid().ToString("N")); - Directory.CreateDirectory(dataDir); - return dataDir; - } - - // Удаляет временную папку (мусор в temp допустим при сбое). - // dataDir: Папка. - private static void TryDelete(string dataDir) - { - try - { - Directory.Delete(dataDir, recursive: true); - } - catch (IOException) - { - // Занятый каталог — тестовый мусор в temp допустим. - } - } -} +using Deal.Ml.Model; +using Deal.Ml.Extensions; +using Microsoft.Data.Sqlite; + +namespace Deal.Ml.Tests.Ml; + +/// +/// Тесты персистентности модели (план Task 5): веса переживают перезапуск пула (второй инстанс +/// на тот же SQLite-файл), журнал самооценки прунится до EVAL_KEEP=200, статус отдаёт окно +/// EVAL_WINDOW=50 последних решений. +/// +public sealed class ModelPersistenceTests +{ + /// + /// Перезапуск пула сохраняет веса: обучение на первом пуле → закрытие → второй пул на тот же + /// каталог отдаёт те же классы/learned/eval и те же предсказания. + /// + [Fact] + public void SecondPoolOnSameFile_ReloadsWeightsAndEval() + { + string dataDir = NewDataDir(); + try + { + MlStatusResult firstStatus; + MlPredictResult firstPredict; + using (var firstPool = new ModelPool(MlOptions.Create(dataDir))) + { + TenantModel model = firstPool.GetOrCreate("tenant-persist"); + List items = LearningData.CanonicalTrainItems(); + model.LearnBatch(items.Take(24).ToList()); + model.LearnBatch(items.Skip(24).ToList()); + + firstStatus = model.Status(); + firstPredict = model.Predict(LearningData.DevMessage); + } + + using var secondPool = new ModelPool(MlOptions.Create(dataDir)); + TenantModel reloaded = secondPool.GetOrCreate("tenant-persist"); + + MlStatusResult secondStatus = reloaded.Status(); + MlPredictResult secondPredict = reloaded.Predict(LearningData.DevMessage); + + Assert.True(secondStatus.Ready); + Assert.Equal(firstStatus.Learned, secondStatus.Learned); + Assert.Equal(firstStatus.Classes, secondStatus.Classes); + Assert.Equal(firstStatus.Eval, secondStatus.Eval); + Assert.True(secondPredict.Take); + Assert.Equal(firstPredict.Label, secondPredict.Label); + Assert.Equal(firstPredict.Scores, secondPredict.Scores); + } + finally + { + TryDelete(dataDir); + } + } + + /// + /// Окно самооценки: журнал хранит не больше EVAL_KEEP (200) строк (проверка в файле), статус + /// считает последние EVAL_WINDOW (50) решений. Сценарий — 250 дополнительных «реальных» + /// действий поверх канонического батча. + /// + [Fact] + public void EvalLog_PrunesToKeepAndWindowIsFifty() + { + string dataDir = NewDataDir(); + try + { + TenantModel model; + using (var pool = new ModelPool(MlOptions.Create(dataDir))) + { + model = pool.GetOrCreate("tenant-eval-window"); + List items = LearningData.CanonicalTrainItems(); + model.LearnBatch(items.Take(24).ToList()); + model.LearnBatch(items.Skip(24).ToList()); + + // 250 реальных действий (delta=1, метка совпадает с предсказанием) — самооценка пишется. + model.LearnBatch( + Enumerable.Repeat(LearningData.User(LearningData.ColumnDev, LearningData.DevMessage), 250).ToList()); + + MlStatusResult status = model.Status(); + + Assert.Equal(50, status.Eval.Count); // окно — последние EVAL_WINDOW + Assert.Equal(50, status.Eval.Correct); + Assert.Equal(1.0, status.Eval.Accuracy); + + Assert.Equal(200, CountEvalRows(model.DatabasePath)); // журнал прунится до EVAL_KEEP + } + } + finally + { + TryDelete(dataDir); + } + } + + /// + /// Прунинг переживает перезапуск: reload держит последние EVAL_KEEP и окно 50. + /// + [Fact] + public void EvalLog_PrunePersistedAcrossPoolRestart() + { + string dataDir = NewDataDir(); + try + { + using (var pool = new ModelPool(MlOptions.Create(dataDir))) + { + TenantModel model = pool.GetOrCreate("tenant-eval-restart"); + List items = LearningData.CanonicalTrainItems(); + model.LearnBatch(items.Take(24).ToList()); + model.LearnBatch(items.Skip(24).ToList()); + model.LearnBatch( + Enumerable.Repeat(LearningData.User(LearningData.ColumnDev, LearningData.DevMessage), 250).ToList()); + } + + using var secondPool = new ModelPool(MlOptions.Create(dataDir)); + TenantModel reloaded = secondPool.GetOrCreate("tenant-eval-restart"); + + MlStatusResult status = reloaded.Status(); + Assert.Equal(50, status.Eval.Count); + Assert.Equal(1.0, status.Eval.Accuracy); + Assert.Equal(200, CountEvalRows(reloaded.DatabasePath)); + } + finally + { + TryDelete(dataDir); + } + } + + // Читает число строк eval_log прямо из SQLite-файла модели. + // dbPath: Путь к файлу модели. + private static long CountEvalRows(string dbPath) + { + using var connection = new SqliteConnection($"Data Source={dbPath}"); + connection.Open(); + using SqliteCommand command = connection.CreateCommand(); + command.CommandText = "SELECT count(*) FROM eval_log"; + return (long)command.ExecuteScalar()!; + } + + // Новая временная папка данных моделей. + private static string NewDataDir() + { + string dataDir = Path.Combine(Path.GetTempPath(), "deal-ml-persist-tests", Guid.NewGuid().ToString("N")); + Directory.CreateDirectory(dataDir); + return dataDir; + } + + // Удаляет временную папку (мусор в temp допустим при сбое). + // dataDir: Папка. + private static void TryDelete(string dataDir) + { + try + { + Directory.Delete(dataDir, recursive: true); + } + catch (IOException) + { + // Занятый каталог — тестовый мусор в temp допустим. + } + } +} diff --git a/src/ml-service/Deal.Ml.Tests/ModelPoolTests.cs b/src/ml-service/Deal.Ml.Tests/Ml/ModelPoolTests.cs similarity index 96% rename from src/ml-service/Deal.Ml.Tests/ModelPoolTests.cs rename to src/ml-service/Deal.Ml.Tests/Ml/ModelPoolTests.cs index e224906..ea49d21 100644 --- a/src/ml-service/Deal.Ml.Tests/ModelPoolTests.cs +++ b/src/ml-service/Deal.Ml.Tests/Ml/ModelPoolTests.cs @@ -1,86 +1,86 @@ -using Deal.Ml.Model; -using Deal.Ml.Extensions; - -namespace Deal.Ml.Tests; - -/// -/// Тесты пула моделей (план Task 5, Ruling 4): ленивое создание по тенанту (один инстанс на -/// тенанта), файл data/ml/<tenantId>.sqlite, защита tenant-id от выхода из каталога. -/// -public sealed class ModelPoolTests -{ - /// - /// Один тенант → один инстанс модели; файл создаётся только по первому обращению. - /// - [Fact] - public void GetOrCreate_ReturnsSameModelPerTenant() - { - RunWithPool(pool => - { - TenantModel first = pool.GetOrCreate("tenant-a"); - TenantModel second = pool.GetOrCreate("tenant-a"); - TenantModel other = pool.GetOrCreate("tenant-b"); - - Assert.Same(first, second); - Assert.NotSame(first, other); - Assert.False(File.Exists(first.DatabasePath)); // лениво: файла ещё нет - }); - } - - /// - /// Имя файла модели — <tenantId>.sqlite в каталоге данных (Ruling 4). - /// - [Fact] - public void GetOrCreate_DbPathIsTenantFileUnderDataDir() - { - RunWithPool(pool => - { - TenantModel model = pool.GetOrCreate("t1"); - model.Status(); // первое обращение создаёт файл - - string expected = Path.Combine(pool.DataDirectory, "t1.sqlite"); - Assert.Equal(expected, model.DatabasePath); - Assert.True(File.Exists(expected)); - }); - } - - /// - /// Некорректный tenant-id (путь/«..») не может вывести файл за каталог данных. - /// - [Fact] - public void GetOrCreate_InvalidTenantId_Throws() - { - RunWithPool(pool => - { - Assert.Throws(() => pool.GetOrCreate(string.Empty)); - Assert.Throws(() => pool.GetOrCreate(" ")); - Assert.Throws(() => pool.GetOrCreate("../outside")); - Assert.Throws(() => pool.GetOrCreate("a\\b")); - Assert.Throws(() => pool.GetOrCreate("..")); - }); - } - - // Прогоняет сценарий над свежим пулом во временной папке (очистка после). - // scenario: Сценарий с пулом. - private static void RunWithPool(Action scenario) - { - string dataDir = Path.Combine(Path.GetTempPath(), "deal-ml-pool-tests", Guid.NewGuid().ToString("N")); - Directory.CreateDirectory(dataDir); - try - { - using var pool = new ModelPool(MlOptions.Create(dataDir)); - scenario(pool); - } - finally - { - try - { - Directory.Delete(dataDir, recursive: true); - } - catch (IOException) - { - // Занятый каталог — тестовый мусор в temp допустим. - } - } - } -} +using Deal.Ml.Model; +using Deal.Ml.Extensions; + +namespace Deal.Ml.Tests.Ml; + +/// +/// Тесты пула моделей (план Task 5, Ruling 4): ленивое создание по тенанту (один инстанс на +/// тенанта), файл data/ml/<tenantId>.sqlite, защита tenant-id от выхода из каталога. +/// +public sealed class ModelPoolTests +{ + /// + /// Один тенант → один инстанс модели; файл создаётся только по первому обращению. + /// + [Fact] + public void GetOrCreate_ReturnsSameModelPerTenant() + { + RunWithPool(pool => + { + TenantModel first = pool.GetOrCreate("tenant-a"); + TenantModel second = pool.GetOrCreate("tenant-a"); + TenantModel other = pool.GetOrCreate("tenant-b"); + + Assert.Same(first, second); + Assert.NotSame(first, other); + Assert.False(File.Exists(first.DatabasePath)); // лениво: файла ещё нет + }); + } + + /// + /// Имя файла модели — <tenantId>.sqlite в каталоге данных (Ruling 4). + /// + [Fact] + public void GetOrCreate_DbPathIsTenantFileUnderDataDir() + { + RunWithPool(pool => + { + TenantModel model = pool.GetOrCreate("t1"); + model.Status(); // первое обращение создаёт файл + + string expected = Path.Combine(pool.DataDirectory, "t1.sqlite"); + Assert.Equal(expected, model.DatabasePath); + Assert.True(File.Exists(expected)); + }); + } + + /// + /// Некорректный tenant-id (путь/«..») не может вывести файл за каталог данных. + /// + [Fact] + public void GetOrCreate_InvalidTenantId_Throws() + { + RunWithPool(pool => + { + Assert.Throws(() => pool.GetOrCreate(string.Empty)); + Assert.Throws(() => pool.GetOrCreate(" ")); + Assert.Throws(() => pool.GetOrCreate("../outside")); + Assert.Throws(() => pool.GetOrCreate("a\\b")); + Assert.Throws(() => pool.GetOrCreate("..")); + }); + } + + // Прогоняет сценарий над свежим пулом во временной папке (очистка после). + // scenario: Сценарий с пулом. + private static void RunWithPool(Action scenario) + { + string dataDir = Path.Combine(Path.GetTempPath(), "deal-ml-pool-tests", Guid.NewGuid().ToString("N")); + Directory.CreateDirectory(dataDir); + try + { + using var pool = new ModelPool(MlOptions.Create(dataDir)); + scenario(pool); + } + finally + { + try + { + Directory.Delete(dataDir, recursive: true); + } + catch (IOException) + { + // Занятый каталог — тестовый мусор в temp допустим. + } + } + } +} diff --git a/src/ml-service/Deal.Ml.Tests/TenantModelLearningTests.cs b/src/ml-service/Deal.Ml.Tests/Ml/TenantModelLearningTests.cs similarity index 97% rename from src/ml-service/Deal.Ml.Tests/TenantModelLearningTests.cs rename to src/ml-service/Deal.Ml.Tests/Ml/TenantModelLearningTests.cs index e410705..75395a0 100644 --- a/src/ml-service/Deal.Ml.Tests/TenantModelLearningTests.cs +++ b/src/ml-service/Deal.Ml.Tests/Ml/TenantModelLearningTests.cs @@ -1,379 +1,379 @@ -using Deal.Ml.Model; -using Deal.Ml.Extensions; - -namespace Deal.Ml.Tests; - -/// -/// Тесты движка модели (план Task 5): обучение → предсказание спама/колонки/типа на русских -/// примерах, ready-пороги (20/6/4/2), адаптивный margin, delta<0 «разучивание», дробные веса -/// ИИ-сигналов, журнал самооценки. Модель — поверх реального SQLite-файла во временной папке. -/// -public sealed class TenantModelLearningTests -{ - /// - /// Пустая модель: predict — фиксированный «не уверен» 1:1 (take:false, ready:false). - /// - [Fact] - public void EmptyModel_PredictReturnsNotReadyShape() - { - RunWithPool(pool => - { - TenantModel model = pool.GetOrCreate("tenant-empty"); - - MlPredictResult predict = model.Predict(LearningData.DevMessage); - - Assert.False(predict.Take); - Assert.Null(predict.Label); - Assert.False(predict.Ready); - Assert.Empty(predict.Scores); - Assert.Equal(0, predict.Hits); - Assert.Null(predict.Margin); - Assert.Empty(predict.Terms); - Assert.Null(predict.Type); - - MlStatusResult status = model.Status(); - Assert.False(status.Ready); - Assert.Empty(status.Classes); - Assert.Equal(0, status.Learned); - Assert.Equal(0, status.Eval.Count); - Assert.Equal(0, status.Eval.Correct); - Assert.Equal(0.0, status.Eval.Accuracy); - }); - } - - /// - /// Пока суммарно примеров < MIN_TOTAL (20), модель не готова и ничего не решает - /// (ready=false, «не уверен») — не может ошибочно удалить заявку как спам. - /// - [Fact] - public void BelowMinTotal_ModelNotReadyAndDoesNotDecide() - { - RunWithPool(pool => - { - TenantModel model = pool.GetOrCreate("tenant-below-threshold"); - Train(model, BelowThresholdItems()); - - MlStatusResult status = model.Status(); - - Assert.False(status.Ready); - Assert.Equal(16, status.Learned); - - MlPredictResult predict = model.Predict(LearningData.SpamMessage); - Assert.False(predict.Take); - Assert.Null(predict.Label); - Assert.False(predict.Ready); - }); - } - - /// - /// Порог ready (суммарно ≥ 20, spam ≥ 4, не-спам ≥ 6): после добавления t:hire модель - /// «включается» и начинает уверенно решать (приёмка Task 5: «нужен middle python…» → колонка). - /// - [Fact] - public void ReachingReadyThreshold_StartsDeciding() - { - RunWithPool(pool => - { - TenantModel model = pool.GetOrCreate("tenant-ready-threshold"); - Train(model, BelowThresholdItems()); - Assert.False(model.Status().Ready); - - Train(model, TypeHireItems()); - MlStatusResult status = model.Status(); - - Assert.True(status.Ready); - - MlPredictResult predict = model.Predict(LearningData.DevMessage); - Assert.True(predict.Take); - Assert.Equal(LearningData.ColumnDev, predict.Label); - Assert.Null(predict.Type); // только один t:-класс — тип не определяется - }); - } - - /// - /// Полный сценарий приёмки Task 5: обучение колонок dev/order + spam + типы t:hire/t:order → - /// предсказания: Dev → колонка b_col_dev + тип hire, Order → b_col_order + тип order, - /// Spam → spam без типа. Параллельно проверяются статус (learned/classes) и журнал самооценки. - /// - [Fact] - public void CanonicalTraining_LearnsColumnsSpamAndTypes() - { - RunWithPool(pool => - { - TenantModel model = pool.GetOrCreate("tenant-canonical"); - List items = LearningData.CanonicalTrainItems(); - - // Батчами, как флашер ядра (Ruling 6): сначала доводим до готовности (24 примера), - // затем «реальные действия» (4) — они и дают строки самооценки. - int learnedFirst = model.LearnBatch(items.Take(24).ToList()); - int learnedSecond = model.LearnBatch(items.Skip(24).ToList()); - - Assert.Equal(24, learnedFirst); - Assert.Equal(4, learnedSecond); - - MlStatusResult status = model.Status(); - Assert.True(status.Ready); - Assert.Equal(28, status.Learned); - Assert.Equal(8.0, status.Classes[LearningData.ColumnDev]); - Assert.Equal(6.0, status.Classes[LearningData.ColumnOrder]); - Assert.Equal(6.0, status.Classes[LearningData.SpamLabel]); - Assert.Equal(4.0, status.Classes[LearningData.TypeHire]); - Assert.Equal(4.0, status.Classes[LearningData.TypeOrder]); - Assert.Equal(4, status.Eval.Count); // 2 dev + 2 spam «реальных» действия после ready - Assert.Equal(4, status.Eval.Correct); - Assert.Equal(1.0, status.Eval.Accuracy); - - MlPredictResult dev = model.Predict(LearningData.DevMessage); - Assert.True(dev.Take); - Assert.Equal(LearningData.ColumnDev, dev.Label); - Assert.Equal(0.9, dev.Margin); - Assert.Equal(21, dev.Hits); // паритет с python (mlservice/model.py, контрольный прогон) - Assert.Equal(37.333, dev.Scores[LearningData.ColumnDev]); - Assert.Equal(33.6, dev.Scores[LearningData.TypeHire]); - Assert.NotNull(dev.Type); - Assert.True(dev.Type.Take); - Assert.Equal("hire", dev.Type.Label); - Assert.Equal(LearningData.TypeHire, dev.Type.Value); - Assert.NotEmpty(dev.Terms); - - MlPredictResult order = model.Predict(LearningData.OrderMessage); - Assert.True(order.Take); - Assert.Equal(LearningData.ColumnOrder, order.Label); - Assert.Equal(18, order.Hits); - Assert.Equal(30.857, order.Scores[LearningData.ColumnOrder]); - Assert.Equal(28.8, order.Scores[LearningData.TypeOrder]); - Assert.NotNull(order.Type); - Assert.Equal("order", order.Type.Label); - - MlPredictResult spam = model.Predict(LearningData.SpamMessage); - Assert.True(spam.Take); - Assert.Equal(LearningData.SpamLabel, spam.Label); - Assert.Equal(17, spam.Hits); - Assert.Equal(29.143, spam.Scores[LearningData.SpamLabel]); - Assert.Null(spam.Type); - - // Минимальный отклик одного термина (паритет с python): weight=6 → score 1.714. - MlPredictResult oneTerm = model.Predict("ищу"); - Assert.True(oneTerm.Ready); - Assert.False(oneTerm.Take); - Assert.Equal(1, oneTerm.Hits); - Assert.Equal(1.714, oneTerm.Scores[LearningData.SpamLabel]); - - MlPredictResult twoTerms = model.Predict("ищу подработку"); - Assert.True(twoTerms.Take); - Assert.Equal(LearningData.SpamLabel, twoTerms.Label); - Assert.Equal(5.143, twoTerms.Scores[LearningData.SpamLabel]); - }); - } - - /// - /// MIN_HITS=2: одно совпадение у победителя ещё не «взятие» (take=false, hits=1), двух — уже да. - /// - [Fact] - public void MinHitsThreshold_NeedsTwoMatchedTerms() - { - RunWithPool(pool => - { - TenantModel model = pool.GetOrCreate("tenant-min-hits"); - Train(model, BelowThresholdItems()); - Train(model, TypeHireItems()); // суммарно 20 → ready - - MlPredictResult single = model.Predict("ищу"); - - Assert.True(single.Ready); - Assert.False(single.Take); - Assert.Null(single.Label); - Assert.Equal(1, single.Hits); - Assert.NotNull(single.Margin); - - MlPredictResult two = model.Predict("ищу подработку"); - Assert.True(two.Take); - Assert.Equal(LearningData.SpamLabel, two.Label); - }); - } - - /// - /// Незнакомый текст готовой модели: «не уверен», но ready=true и margin пуст. - /// - [Fact] - public void UnknownText_NotTakenButReady() - { - RunWithPool(pool => - { - TenantModel model = pool.GetOrCreate("tenant-unknown"); - Train(model, LearningData.CanonicalTrainItems()); - - MlPredictResult predict = model.Predict("совершенно посторонний текст без общих терминов"); - - Assert.True(predict.Ready); - Assert.False(predict.Take); - Assert.Null(predict.Label); - Assert.Empty(predict.Scores); - Assert.Null(predict.Margin); - Assert.Null(predict.Type); - }); - } - - /// - /// Адаптивный margin: 0.9 на старте, 0.7 после ≥ 60 суммарных примеров (L42–55). - /// - [Fact] - public void AdaptiveMargin_DropsAfter60Examples() - { - RunWithPool(pool => - { - TenantModel model = pool.GetOrCreate("tenant-margin"); - Train(model, LearningData.CanonicalTrainItems()); - - Assert.Equal(0.9, model.Predict(LearningData.DevMessage).Margin); - - Train(model, Enumerable.Repeat(LearningData.User(LearningData.ColumnDev, LearningData.DevMessage), 40).ToList()); - - double? margin = model.Predict(LearningData.DevMessage).Margin; - Assert.Equal(0.7, margin); - }); - } - - /// - /// delta < 0 «разучивает»: вес класса и его термины уменьшаются до удаления. - /// - [Fact] - public void NegativeDelta_UnlearnsUntilClassRemoved() - { - RunWithPool(pool => - { - TenantModel model = pool.GetOrCreate("tenant-unlearn"); - Train(model, Enumerable.Repeat(LearningData.User(LearningData.SpamLabel, LearningData.SpamMessage), 3).ToList()); - - Assert.Equal(3.0, model.Status().Classes[LearningData.SpamLabel]); - Assert.Equal(3, model.Status().Learned); - - Train(model, Enumerable.Repeat(LearningData.Signal(LearningData.SpamLabel, LearningData.SpamMessage, -1.0), 1).ToList()); - Assert.Equal(2.0, model.Status().Classes[LearningData.SpamLabel]); - - Train(model, Enumerable.Repeat(LearningData.Signal(LearningData.SpamLabel, LearningData.SpamMessage, -1.0), 2).ToList()); - - MlStatusResult status = model.Status(); - Assert.False(status.Classes.ContainsKey(LearningData.SpamLabel)); - Assert.Equal(0, status.Learned); - Assert.False(model.Predict(LearningData.SpamMessage).Take); - }); - } - - /// - /// Дробные веса ИИ-сигналов (delta 0.4): класс копится, но не кратен 1 (гипотезы ИИ). - /// - [Fact] - public void FractionalDelta_TrainsAiHypotheses() - { - RunWithPool(pool => - { - TenantModel model = pool.GetOrCreate("tenant-ai-signal"); - Train(model, [LearningData.Signal(LearningData.TypeHire, LearningData.DevMessage, 0.4)]); - - MlStatusResult status = model.Status(); - - Assert.False(status.Ready); - Assert.Equal(0.4, status.Classes[LearningData.TypeHire]); - Assert.Equal(0, status.Learned); // int(0.4) — как python int(sum) - }); - } - - /// - /// Пустые/пробельные text или label пропускаются тихо (1:1 _upsert_one L112–114). - /// - [Fact] - public void LearnBatch_SkipsEmptyItems() - { - RunWithPool(pool => - { - TenantModel model = pool.GetOrCreate("tenant-empty-items"); - var items = new List - { - LearningData.User(LearningData.ColumnDev, string.Empty), - LearningData.User(" ", LearningData.DevMessage), - LearningData.User(LearningData.ColumnOrder, LearningData.OrderMessage), - }; - - int learned = model.LearnBatch(items); - - Assert.Equal(1, learned); - MlStatusResult status = model.Status(); - Assert.Equal(1, status.Learned); - Assert.True(status.Classes.ContainsKey(LearningData.ColumnOrder)); - Assert.False(status.Classes.ContainsKey(LearningData.ColumnDev)); - }); - } - - /// - /// Reset очищает модель и пересоздаёт файл при следующем обучении (план Task 6). - /// - [Fact] - public void Reset_ClearsWeightsAndFileRecreatedOnNextLearn() - { - RunWithPool(pool => - { - TenantModel model = pool.GetOrCreate("tenant-reset"); - Train(model, LearningData.CanonicalTrainItems()); - string dbPath = model.DatabasePath; - Assert.True(File.Exists(dbPath)); - - model.Reset(); - - Assert.False(File.Exists(dbPath)); - Assert.False(model.Status().Ready); - Assert.Empty(model.Status().Classes); - Assert.Equal(0, model.Status().Learned); - Assert.False(model.Predict(LearningData.DevMessage).Take); - - Train(model, Enumerable.Repeat(LearningData.User(LearningData.ColumnDev, LearningData.DevMessage), 3).ToList()); - - Assert.True(File.Exists(dbPath)); - Assert.Equal(3, model.Status().Learned); - }); - } - - // Батч: 6 колонок dev, 6 колонок order, 4 спама (суммарно 16 — ниже MIN_TOTAL). - private static List BelowThresholdItems() - { - var items = new List(); - items.AddRange(Enumerable.Repeat(LearningData.User(LearningData.ColumnDev, LearningData.DevMessage), 6)); - items.AddRange(Enumerable.Repeat(LearningData.User(LearningData.ColumnOrder, LearningData.OrderMessage), 6)); - items.AddRange(Enumerable.Repeat(LearningData.User(LearningData.SpamLabel, LearningData.SpamMessage), 4)); - return items; - } - - // 4 примера типа t:hire (доводят суммарный счёт до MIN_TOTAL). - private static List TypeHireItems() - => Enumerable.Repeat(LearningData.User(LearningData.TypeHire, LearningData.DevMessage), 4).ToList(); - - // Обучает модель батчем (один вызов LearnBatch). - // model: Модель. - // items: Примеры. - private static void Train(TenantModel model, List items) - => model.LearnBatch(items); - - // Прогоняет сценарий над свежим пулом во временной папке (очистка после). - // scenario: Сценарий с пулом. - private static void RunWithPool(Action scenario) - { - string dataDir = Path.Combine(Path.GetTempPath(), "deal-ml-model-tests", Guid.NewGuid().ToString("N")); - Directory.CreateDirectory(dataDir); - try - { - using var pool = new ModelPool(MlOptions.Create(dataDir)); - scenario(pool); - } - finally - { - try - { - Directory.Delete(dataDir, recursive: true); - } - catch (IOException) - { - // Мусор в temp допустим, если каталог занят (Windows). - } - } - } -} +using Deal.Ml.Model; +using Deal.Ml.Extensions; + +namespace Deal.Ml.Tests.Ml; + +/// +/// Тесты движка модели (план Task 5): обучение → предсказание спама/колонки/типа на русских +/// примерах, ready-пороги (20/6/4/2), адаптивный margin, delta<0 «разучивание», дробные веса +/// ИИ-сигналов, журнал самооценки. Модель — поверх реального SQLite-файла во временной папке. +/// +public sealed class TenantModelLearningTests +{ + /// + /// Пустая модель: predict — фиксированный «не уверен» 1:1 (take:false, ready:false). + /// + [Fact] + public void EmptyModel_PredictReturnsNotReadyShape() + { + RunWithPool(pool => + { + TenantModel model = pool.GetOrCreate("tenant-empty"); + + MlPredictResult predict = model.Predict(LearningData.DevMessage); + + Assert.False(predict.Take); + Assert.Null(predict.Label); + Assert.False(predict.Ready); + Assert.Empty(predict.Scores); + Assert.Equal(0, predict.Hits); + Assert.Null(predict.Margin); + Assert.Empty(predict.Terms); + Assert.Null(predict.Type); + + MlStatusResult status = model.Status(); + Assert.False(status.Ready); + Assert.Empty(status.Classes); + Assert.Equal(0, status.Learned); + Assert.Equal(0, status.Eval.Count); + Assert.Equal(0, status.Eval.Correct); + Assert.Equal(0.0, status.Eval.Accuracy); + }); + } + + /// + /// Пока суммарно примеров < MIN_TOTAL (20), модель не готова и ничего не решает + /// (ready=false, «не уверен») — не может ошибочно удалить заявку как спам. + /// + [Fact] + public void BelowMinTotal_ModelNotReadyAndDoesNotDecide() + { + RunWithPool(pool => + { + TenantModel model = pool.GetOrCreate("tenant-below-threshold"); + Train(model, BelowThresholdItems()); + + MlStatusResult status = model.Status(); + + Assert.False(status.Ready); + Assert.Equal(16, status.Learned); + + MlPredictResult predict = model.Predict(LearningData.SpamMessage); + Assert.False(predict.Take); + Assert.Null(predict.Label); + Assert.False(predict.Ready); + }); + } + + /// + /// Порог ready (суммарно ≥ 20, spam ≥ 4, не-спам ≥ 6): после добавления t:hire модель + /// «включается» и начинает уверенно решать (приёмка Task 5: «нужен middle python…» → колонка). + /// + [Fact] + public void ReachingReadyThreshold_StartsDeciding() + { + RunWithPool(pool => + { + TenantModel model = pool.GetOrCreate("tenant-ready-threshold"); + Train(model, BelowThresholdItems()); + Assert.False(model.Status().Ready); + + Train(model, TypeHireItems()); + MlStatusResult status = model.Status(); + + Assert.True(status.Ready); + + MlPredictResult predict = model.Predict(LearningData.DevMessage); + Assert.True(predict.Take); + Assert.Equal(LearningData.ColumnDev, predict.Label); + Assert.Null(predict.Type); // только один t:-класс — тип не определяется + }); + } + + /// + /// Полный сценарий приёмки Task 5: обучение колонок dev/order + spam + типы t:hire/t:order → + /// предсказания: Dev → колонка b_col_dev + тип hire, Order → b_col_order + тип order, + /// Spam → spam без типа. Параллельно проверяются статус (learned/classes) и журнал самооценки. + /// + [Fact] + public void CanonicalTraining_LearnsColumnsSpamAndTypes() + { + RunWithPool(pool => + { + TenantModel model = pool.GetOrCreate("tenant-canonical"); + List items = LearningData.CanonicalTrainItems(); + + // Батчами, как флашер ядра (Ruling 6): сначала доводим до готовности (24 примера), + // затем «реальные действия» (4) — они и дают строки самооценки. + int learnedFirst = model.LearnBatch(items.Take(24).ToList()); + int learnedSecond = model.LearnBatch(items.Skip(24).ToList()); + + Assert.Equal(24, learnedFirst); + Assert.Equal(4, learnedSecond); + + MlStatusResult status = model.Status(); + Assert.True(status.Ready); + Assert.Equal(28, status.Learned); + Assert.Equal(8.0, status.Classes[LearningData.ColumnDev]); + Assert.Equal(6.0, status.Classes[LearningData.ColumnOrder]); + Assert.Equal(6.0, status.Classes[LearningData.SpamLabel]); + Assert.Equal(4.0, status.Classes[LearningData.TypeHire]); + Assert.Equal(4.0, status.Classes[LearningData.TypeOrder]); + Assert.Equal(4, status.Eval.Count); // 2 dev + 2 spam «реальных» действия после ready + Assert.Equal(4, status.Eval.Correct); + Assert.Equal(1.0, status.Eval.Accuracy); + + MlPredictResult dev = model.Predict(LearningData.DevMessage); + Assert.True(dev.Take); + Assert.Equal(LearningData.ColumnDev, dev.Label); + Assert.Equal(0.9, dev.Margin); + Assert.Equal(21, dev.Hits); // паритет с python (mlservice/model.py, контрольный прогон) + Assert.Equal(37.333, dev.Scores[LearningData.ColumnDev]); + Assert.Equal(33.6, dev.Scores[LearningData.TypeHire]); + Assert.NotNull(dev.Type); + Assert.True(dev.Type.Take); + Assert.Equal("hire", dev.Type.Label); + Assert.Equal(LearningData.TypeHire, dev.Type.Value); + Assert.NotEmpty(dev.Terms); + + MlPredictResult order = model.Predict(LearningData.OrderMessage); + Assert.True(order.Take); + Assert.Equal(LearningData.ColumnOrder, order.Label); + Assert.Equal(18, order.Hits); + Assert.Equal(30.857, order.Scores[LearningData.ColumnOrder]); + Assert.Equal(28.8, order.Scores[LearningData.TypeOrder]); + Assert.NotNull(order.Type); + Assert.Equal("order", order.Type.Label); + + MlPredictResult spam = model.Predict(LearningData.SpamMessage); + Assert.True(spam.Take); + Assert.Equal(LearningData.SpamLabel, spam.Label); + Assert.Equal(17, spam.Hits); + Assert.Equal(29.143, spam.Scores[LearningData.SpamLabel]); + Assert.Null(spam.Type); + + // Минимальный отклик одного термина (паритет с python): weight=6 → score 1.714. + MlPredictResult oneTerm = model.Predict("ищу"); + Assert.True(oneTerm.Ready); + Assert.False(oneTerm.Take); + Assert.Equal(1, oneTerm.Hits); + Assert.Equal(1.714, oneTerm.Scores[LearningData.SpamLabel]); + + MlPredictResult twoTerms = model.Predict("ищу подработку"); + Assert.True(twoTerms.Take); + Assert.Equal(LearningData.SpamLabel, twoTerms.Label); + Assert.Equal(5.143, twoTerms.Scores[LearningData.SpamLabel]); + }); + } + + /// + /// MIN_HITS=2: одно совпадение у победителя ещё не «взятие» (take=false, hits=1), двух — уже да. + /// + [Fact] + public void MinHitsThreshold_NeedsTwoMatchedTerms() + { + RunWithPool(pool => + { + TenantModel model = pool.GetOrCreate("tenant-min-hits"); + Train(model, BelowThresholdItems()); + Train(model, TypeHireItems()); // суммарно 20 → ready + + MlPredictResult single = model.Predict("ищу"); + + Assert.True(single.Ready); + Assert.False(single.Take); + Assert.Null(single.Label); + Assert.Equal(1, single.Hits); + Assert.NotNull(single.Margin); + + MlPredictResult two = model.Predict("ищу подработку"); + Assert.True(two.Take); + Assert.Equal(LearningData.SpamLabel, two.Label); + }); + } + + /// + /// Незнакомый текст готовой модели: «не уверен», но ready=true и margin пуст. + /// + [Fact] + public void UnknownText_NotTakenButReady() + { + RunWithPool(pool => + { + TenantModel model = pool.GetOrCreate("tenant-unknown"); + Train(model, LearningData.CanonicalTrainItems()); + + MlPredictResult predict = model.Predict("совершенно посторонний текст без общих терминов"); + + Assert.True(predict.Ready); + Assert.False(predict.Take); + Assert.Null(predict.Label); + Assert.Empty(predict.Scores); + Assert.Null(predict.Margin); + Assert.Null(predict.Type); + }); + } + + /// + /// Адаптивный margin: 0.9 на старте, 0.7 после ≥ 60 суммарных примеров (L42–55). + /// + [Fact] + public void AdaptiveMargin_DropsAfter60Examples() + { + RunWithPool(pool => + { + TenantModel model = pool.GetOrCreate("tenant-margin"); + Train(model, LearningData.CanonicalTrainItems()); + + Assert.Equal(0.9, model.Predict(LearningData.DevMessage).Margin); + + Train(model, Enumerable.Repeat(LearningData.User(LearningData.ColumnDev, LearningData.DevMessage), 40).ToList()); + + double? margin = model.Predict(LearningData.DevMessage).Margin; + Assert.Equal(0.7, margin); + }); + } + + /// + /// delta < 0 «разучивает»: вес класса и его термины уменьшаются до удаления. + /// + [Fact] + public void NegativeDelta_UnlearnsUntilClassRemoved() + { + RunWithPool(pool => + { + TenantModel model = pool.GetOrCreate("tenant-unlearn"); + Train(model, Enumerable.Repeat(LearningData.User(LearningData.SpamLabel, LearningData.SpamMessage), 3).ToList()); + + Assert.Equal(3.0, model.Status().Classes[LearningData.SpamLabel]); + Assert.Equal(3, model.Status().Learned); + + Train(model, Enumerable.Repeat(LearningData.Signal(LearningData.SpamLabel, LearningData.SpamMessage, -1.0), 1).ToList()); + Assert.Equal(2.0, model.Status().Classes[LearningData.SpamLabel]); + + Train(model, Enumerable.Repeat(LearningData.Signal(LearningData.SpamLabel, LearningData.SpamMessage, -1.0), 2).ToList()); + + MlStatusResult status = model.Status(); + Assert.False(status.Classes.ContainsKey(LearningData.SpamLabel)); + Assert.Equal(0, status.Learned); + Assert.False(model.Predict(LearningData.SpamMessage).Take); + }); + } + + /// + /// Дробные веса ИИ-сигналов (delta 0.4): класс копится, но не кратен 1 (гипотезы ИИ). + /// + [Fact] + public void FractionalDelta_TrainsAiHypotheses() + { + RunWithPool(pool => + { + TenantModel model = pool.GetOrCreate("tenant-ai-signal"); + Train(model, [LearningData.Signal(LearningData.TypeHire, LearningData.DevMessage, 0.4)]); + + MlStatusResult status = model.Status(); + + Assert.False(status.Ready); + Assert.Equal(0.4, status.Classes[LearningData.TypeHire]); + Assert.Equal(0, status.Learned); // int(0.4) — как python int(sum) + }); + } + + /// + /// Пустые/пробельные text или label пропускаются тихо (1:1 _upsert_one L112–114). + /// + [Fact] + public void LearnBatch_SkipsEmptyItems() + { + RunWithPool(pool => + { + TenantModel model = pool.GetOrCreate("tenant-empty-items"); + var items = new List + { + LearningData.User(LearningData.ColumnDev, string.Empty), + LearningData.User(" ", LearningData.DevMessage), + LearningData.User(LearningData.ColumnOrder, LearningData.OrderMessage), + }; + + int learned = model.LearnBatch(items); + + Assert.Equal(1, learned); + MlStatusResult status = model.Status(); + Assert.Equal(1, status.Learned); + Assert.True(status.Classes.ContainsKey(LearningData.ColumnOrder)); + Assert.False(status.Classes.ContainsKey(LearningData.ColumnDev)); + }); + } + + /// + /// Reset очищает модель и пересоздаёт файл при следующем обучении (план Task 6). + /// + [Fact] + public void Reset_ClearsWeightsAndFileRecreatedOnNextLearn() + { + RunWithPool(pool => + { + TenantModel model = pool.GetOrCreate("tenant-reset"); + Train(model, LearningData.CanonicalTrainItems()); + string dbPath = model.DatabasePath; + Assert.True(File.Exists(dbPath)); + + model.Reset(); + + Assert.False(File.Exists(dbPath)); + Assert.False(model.Status().Ready); + Assert.Empty(model.Status().Classes); + Assert.Equal(0, model.Status().Learned); + Assert.False(model.Predict(LearningData.DevMessage).Take); + + Train(model, Enumerable.Repeat(LearningData.User(LearningData.ColumnDev, LearningData.DevMessage), 3).ToList()); + + Assert.True(File.Exists(dbPath)); + Assert.Equal(3, model.Status().Learned); + }); + } + + // Батч: 6 колонок dev, 6 колонок order, 4 спама (суммарно 16 — ниже MIN_TOTAL). + private static List BelowThresholdItems() + { + var items = new List(); + items.AddRange(Enumerable.Repeat(LearningData.User(LearningData.ColumnDev, LearningData.DevMessage), 6)); + items.AddRange(Enumerable.Repeat(LearningData.User(LearningData.ColumnOrder, LearningData.OrderMessage), 6)); + items.AddRange(Enumerable.Repeat(LearningData.User(LearningData.SpamLabel, LearningData.SpamMessage), 4)); + return items; + } + + // 4 примера типа t:hire (доводят суммарный счёт до MIN_TOTAL). + private static List TypeHireItems() + => Enumerable.Repeat(LearningData.User(LearningData.TypeHire, LearningData.DevMessage), 4).ToList(); + + // Обучает модель батчем (один вызов LearnBatch). + // model: Модель. + // items: Примеры. + private static void Train(TenantModel model, List items) + => model.LearnBatch(items); + + // Прогоняет сценарий над свежим пулом во временной папке (очистка после). + // scenario: Сценарий с пулом. + private static void RunWithPool(Action scenario) + { + string dataDir = Path.Combine(Path.GetTempPath(), "deal-ml-model-tests", Guid.NewGuid().ToString("N")); + Directory.CreateDirectory(dataDir); + try + { + using var pool = new ModelPool(MlOptions.Create(dataDir)); + scenario(pool); + } + finally + { + try + { + Directory.Delete(dataDir, recursive: true); + } + catch (IOException) + { + // Мусор в temp допустим, если каталог занят (Windows). + } + } + } +} diff --git a/src/telegram-service/Deal.Telegram.Tests/CoreIngressClientTests.cs b/src/telegram-service/Deal.Telegram.Tests/Grpc/CoreIngressClientTests.cs similarity index 96% rename from src/telegram-service/Deal.Telegram.Tests/CoreIngressClientTests.cs rename to src/telegram-service/Deal.Telegram.Tests/Grpc/CoreIngressClientTests.cs index 7814543..154e2a8 100644 --- a/src/telegram-service/Deal.Telegram.Tests/CoreIngressClientTests.cs +++ b/src/telegram-service/Deal.Telegram.Tests/Grpc/CoreIngressClientTests.cs @@ -5,8 +5,9 @@ using Grpc.Core; using Microsoft.AspNetCore.Builder; using Microsoft.Extensions.Logging.Abstractions; using Xunit; +using Deal.Telegram.Tests.Support; -namespace Deal.Telegram.Tests; +namespace Deal.Telegram.Tests.Grpc; /// /// Тесты исходящего gRPC-канала в ядро (план Task 10 Acceptance: «PushMessage-клиент к in-proc diff --git a/src/telegram-service/Deal.Telegram.Tests/DialogRpcTests.cs b/src/telegram-service/Deal.Telegram.Tests/Grpc/DialogRpcTests.cs similarity index 97% rename from src/telegram-service/Deal.Telegram.Tests/DialogRpcTests.cs rename to src/telegram-service/Deal.Telegram.Tests/Grpc/DialogRpcTests.cs index e35b069..20a7c6c 100644 --- a/src/telegram-service/Deal.Telegram.Tests/DialogRpcTests.cs +++ b/src/telegram-service/Deal.Telegram.Tests/Grpc/DialogRpcTests.cs @@ -8,8 +8,10 @@ using Microsoft.AspNetCore.Builder; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; using Xunit; +using Deal.Telegram.Tests.Support; +using Deal.Telegram.Tests.Telegram; -namespace Deal.Telegram.Tests; +namespace Deal.Telegram.Tests.Grpc; /// /// RPC-тесты каталога/мониторинга поверх реального gRPC-хоста (план Task 10: RefreshDialogs/ diff --git a/src/telegram-service/Deal.Telegram.Tests/DiscoveryProtoMapperTests.cs b/src/telegram-service/Deal.Telegram.Tests/Grpc/DiscoveryProtoMapperTests.cs similarity index 96% rename from src/telegram-service/Deal.Telegram.Tests/DiscoveryProtoMapperTests.cs rename to src/telegram-service/Deal.Telegram.Tests/Grpc/DiscoveryProtoMapperTests.cs index 2d90db5..88ffca4 100644 --- a/src/telegram-service/Deal.Telegram.Tests/DiscoveryProtoMapperTests.cs +++ b/src/telegram-service/Deal.Telegram.Tests/Grpc/DiscoveryProtoMapperTests.cs @@ -4,8 +4,9 @@ using Deal.Telegram.Dialogs; using Deal.Telegram.Telegram; using Deal.Telegram.Extensions; using Xunit; +using Deal.Telegram.Tests.Support; -namespace Deal.Telegram.Tests; +namespace Deal.Telegram.Tests.Grpc; /// /// Unit-тесты маппера ответов discovery (план Task 11 Acceptance: «формат ответов — тесты на чистых diff --git a/src/telegram-service/Deal.Telegram.Tests/DiscoveryRpcTests.cs b/src/telegram-service/Deal.Telegram.Tests/Grpc/DiscoveryRpcTests.cs similarity index 97% rename from src/telegram-service/Deal.Telegram.Tests/DiscoveryRpcTests.cs rename to src/telegram-service/Deal.Telegram.Tests/Grpc/DiscoveryRpcTests.cs index 6ce6eff..ef4f998 100644 --- a/src/telegram-service/Deal.Telegram.Tests/DiscoveryRpcTests.cs +++ b/src/telegram-service/Deal.Telegram.Tests/Grpc/DiscoveryRpcTests.cs @@ -8,8 +8,10 @@ using Microsoft.AspNetCore.Builder; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; using Xunit; +using Deal.Telegram.Tests.Support; +using Deal.Telegram.Tests.Telegram; -namespace Deal.Telegram.Tests; +namespace Deal.Telegram.Tests.Grpc; /// /// RPC-тесты discovery-операций поверх реального gRPC-хоста (план Task 11: Search/GetInfo/ReadForEval/ diff --git a/src/telegram-service/Deal.Telegram.Tests/FakeIngressServer.cs b/src/telegram-service/Deal.Telegram.Tests/Grpc/FakeIngressServer.cs similarity index 96% rename from src/telegram-service/Deal.Telegram.Tests/FakeIngressServer.cs rename to src/telegram-service/Deal.Telegram.Tests/Grpc/FakeIngressServer.cs index 82bc272..672ef2a 100644 --- a/src/telegram-service/Deal.Telegram.Tests/FakeIngressServer.cs +++ b/src/telegram-service/Deal.Telegram.Tests/Grpc/FakeIngressServer.cs @@ -6,8 +6,9 @@ using Microsoft.AspNetCore.Server.Kestrel.Core; using Microsoft.Extensions.DependencyInjection; using System.Net; using System.Net.Sockets; +using Deal.Telegram.Tests.Support; -namespace Deal.Telegram.Tests; +namespace Deal.Telegram.Tests.Grpc; // Фейковая реализация IngressService (сервер ядра) для in-proc проверки клиента ингресса // (план Task 10: «PushMessage-клиент к in-proc fake-серверу ингресса»). Записывает вызовы и metadata diff --git a/src/telegram-service/Deal.Telegram.Tests/RealtimeListenerTests.cs b/src/telegram-service/Deal.Telegram.Tests/Grpc/RealtimeListenerTests.cs similarity index 95% rename from src/telegram-service/Deal.Telegram.Tests/RealtimeListenerTests.cs rename to src/telegram-service/Deal.Telegram.Tests/Grpc/RealtimeListenerTests.cs index 0960f4d..717a22a 100644 --- a/src/telegram-service/Deal.Telegram.Tests/RealtimeListenerTests.cs +++ b/src/telegram-service/Deal.Telegram.Tests/Grpc/RealtimeListenerTests.cs @@ -6,8 +6,10 @@ using Deal.Telegram.Extensions; using Grpc.Core; using Microsoft.Extensions.Logging.Abstractions; using Xunit; +using Deal.Telegram.Tests.Support; +using Deal.Telegram.Tests.Telegram; -namespace Deal.Telegram.Tests; +namespace Deal.Telegram.Tests.Grpc; /// /// Unit-тесты realtime-listener'а (план Task 10 Acceptance: фильтр мониторинга; «новые сообщения → diff --git a/src/telegram-service/Deal.Telegram.Tests/TelegramServiceHostTests.cs b/src/telegram-service/Deal.Telegram.Tests/Grpc/TelegramServiceHostTests.cs similarity index 97% rename from src/telegram-service/Deal.Telegram.Tests/TelegramServiceHostTests.cs rename to src/telegram-service/Deal.Telegram.Tests/Grpc/TelegramServiceHostTests.cs index dc2ef7c..92ab83c 100644 --- a/src/telegram-service/Deal.Telegram.Tests/TelegramServiceHostTests.cs +++ b/src/telegram-service/Deal.Telegram.Tests/Grpc/TelegramServiceHostTests.cs @@ -3,8 +3,9 @@ using Grpc.Core; using Grpc.Health.V1; using Grpc.Net.Client; using Xunit; +using Deal.Telegram.Tests.Support; -namespace Deal.Telegram.Tests; +namespace Deal.Telegram.Tests.Grpc; /// /// Интеграционные тесты каркаса telegram-service (план Task 2, Acceptance + задача сессий). diff --git a/src/telegram-service/Deal.Telegram.Tests/TelegramSessionRpcTests.cs b/src/telegram-service/Deal.Telegram.Tests/Grpc/TelegramSessionRpcTests.cs similarity index 96% rename from src/telegram-service/Deal.Telegram.Tests/TelegramSessionRpcTests.cs rename to src/telegram-service/Deal.Telegram.Tests/Grpc/TelegramSessionRpcTests.cs index d546163..f9406b2 100644 --- a/src/telegram-service/Deal.Telegram.Tests/TelegramSessionRpcTests.cs +++ b/src/telegram-service/Deal.Telegram.Tests/Grpc/TelegramSessionRpcTests.cs @@ -5,8 +5,10 @@ using Grpc.Core; using Grpc.Net.Client; using Microsoft.Extensions.DependencyInjection; using Xunit; +using Deal.Telegram.Tests.Support; +using Deal.Telegram.Tests.Telegram; -namespace Deal.Telegram.Tests; +namespace Deal.Telegram.Tests.Grpc; /// /// RPC-тесты подключения аккаунта поверх реального gRPC-хоста (план Task 9 Acceptance: ветки RPC — diff --git a/src/telegram-service/Deal.Telegram.Tests/TelegramTestHost.cs b/src/telegram-service/Deal.Telegram.Tests/Grpc/TelegramTestHost.cs similarity index 96% rename from src/telegram-service/Deal.Telegram.Tests/TelegramTestHost.cs rename to src/telegram-service/Deal.Telegram.Tests/Grpc/TelegramTestHost.cs index 4e6e745..f522363 100644 --- a/src/telegram-service/Deal.Telegram.Tests/TelegramTestHost.cs +++ b/src/telegram-service/Deal.Telegram.Tests/Grpc/TelegramTestHost.cs @@ -8,8 +8,9 @@ using Microsoft.AspNetCore.Builder; using Microsoft.Extensions.DependencyInjection; using System.Net; using System.Net.Sockets; +using Deal.Telegram.Tests.Support; -namespace Deal.Telegram.Tests; +namespace Deal.Telegram.Tests.Grpc; // Общий харнесс интеграционных тестов telegram-service: поднимает хост (TelegramServiceHost.Create) // в процессе теста на эфемерном порту с управляемым окружением env и каталогом сессий в temp. diff --git a/src/telegram-service/Deal.Telegram.Tests/TestDoubles.cs b/src/telegram-service/Deal.Telegram.Tests/Grpc/TestDoubles.cs similarity index 95% rename from src/telegram-service/Deal.Telegram.Tests/TestDoubles.cs rename to src/telegram-service/Deal.Telegram.Tests/Grpc/TestDoubles.cs index 986f686..5c862c0 100644 --- a/src/telegram-service/Deal.Telegram.Tests/TestDoubles.cs +++ b/src/telegram-service/Deal.Telegram.Tests/Grpc/TestDoubles.cs @@ -2,8 +2,9 @@ using Deal.Grpc.Telegram; using Deal.Telegram.Core; using Deal.Telegram.Dialogs; using Grpc.Core; +using Deal.Telegram.Tests.Support; -namespace Deal.Telegram.Tests; +namespace Deal.Telegram.Tests.Grpc; // Фейк исходящего канала в ядро (ICoreIngressClient) для unit-тестов служб каталога: записывает // PushMessage/SyncDialogs без сети; SyncDialogs возвращает настраиваемый monitored-набор (роль ядра, diff --git a/src/telegram-service/Deal.Telegram.Tests/SessionStorageTests.cs b/src/telegram-service/Deal.Telegram.Tests/Support/SessionStorageTests.cs similarity index 96% rename from src/telegram-service/Deal.Telegram.Tests/SessionStorageTests.cs rename to src/telegram-service/Deal.Telegram.Tests/Support/SessionStorageTests.cs index ca4ec5e..7e6e7ac 100644 --- a/src/telegram-service/Deal.Telegram.Tests/SessionStorageTests.cs +++ b/src/telegram-service/Deal.Telegram.Tests/Support/SessionStorageTests.cs @@ -2,8 +2,9 @@ using System.Security.Cryptography; using Deal.Telegram.Sessions; using Grpc.Core; using Xunit; +using Deal.Telegram.Tests.Telegram; -namespace Deal.Telegram.Tests; +namespace Deal.Telegram.Tests.Support; /// /// Тесты файлового хранилища сессий и AES-GCM-обёртки (план Task 9 Acceptance: roundtrip/шифрование/ diff --git a/src/telegram-service/Deal.Telegram.Tests/TenantSessionTests.cs b/src/telegram-service/Deal.Telegram.Tests/Support/TenantSessionTests.cs similarity index 97% rename from src/telegram-service/Deal.Telegram.Tests/TenantSessionTests.cs rename to src/telegram-service/Deal.Telegram.Tests/Support/TenantSessionTests.cs index b6a896d..f5e80d3 100644 --- a/src/telegram-service/Deal.Telegram.Tests/TenantSessionTests.cs +++ b/src/telegram-service/Deal.Telegram.Tests/Support/TenantSessionTests.cs @@ -3,8 +3,9 @@ using Deal.Telegram.Sessions; using Grpc.Core; using Microsoft.Extensions.Logging.Abstractions; using Xunit; +using Deal.Telegram.Tests.Telegram; -namespace Deal.Telegram.Tests; +namespace Deal.Telegram.Tests.Support; /// /// Тесты фазовой машины сессии тенанта (план Task 9 Acceptance: фазовые переходы на fake-клиенте diff --git a/src/telegram-service/Deal.Telegram.Tests/BackfillServiceTests.cs b/src/telegram-service/Deal.Telegram.Tests/Telegram/BackfillServiceTests.cs similarity index 96% rename from src/telegram-service/Deal.Telegram.Tests/BackfillServiceTests.cs rename to src/telegram-service/Deal.Telegram.Tests/Telegram/BackfillServiceTests.cs index b5aaf07..2f379bf 100644 --- a/src/telegram-service/Deal.Telegram.Tests/BackfillServiceTests.cs +++ b/src/telegram-service/Deal.Telegram.Tests/Telegram/BackfillServiceTests.cs @@ -5,8 +5,10 @@ using Deal.Telegram.Extensions; using Grpc.Core; using Microsoft.Extensions.Logging.Abstractions; using Xunit; +using Deal.Telegram.Tests.Support; +using Deal.Telegram.Tests.Grpc; -namespace Deal.Telegram.Tests; +namespace Deal.Telegram.Tests.Telegram; /// /// Unit-тесты backfill диалога (план Task 10 Acceptance: паузы fake-пейсером, processed, mark-as-read diff --git a/src/telegram-service/Deal.Telegram.Tests/DialogCatalogTests.cs b/src/telegram-service/Deal.Telegram.Tests/Telegram/DialogCatalogTests.cs similarity index 96% rename from src/telegram-service/Deal.Telegram.Tests/DialogCatalogTests.cs rename to src/telegram-service/Deal.Telegram.Tests/Telegram/DialogCatalogTests.cs index 92b0380..1a18604 100644 --- a/src/telegram-service/Deal.Telegram.Tests/DialogCatalogTests.cs +++ b/src/telegram-service/Deal.Telegram.Tests/Telegram/DialogCatalogTests.cs @@ -1,7 +1,8 @@ using Deal.Telegram.Dialogs; using Xunit; +using Deal.Telegram.Tests.Support; -namespace Deal.Telegram.Tests; +namespace Deal.Telegram.Tests.Telegram; /// /// Unit-тесты зеркала каталога/мониторинга диалогов (план Task 10: DialogCatalog; Ruling 7 — ядро diff --git a/src/telegram-service/Deal.Telegram.Tests/DialogHueTests.cs b/src/telegram-service/Deal.Telegram.Tests/Telegram/DialogHueTests.cs similarity index 94% rename from src/telegram-service/Deal.Telegram.Tests/DialogHueTests.cs rename to src/telegram-service/Deal.Telegram.Tests/Telegram/DialogHueTests.cs index 058ab24..ae41df2 100644 --- a/src/telegram-service/Deal.Telegram.Tests/DialogHueTests.cs +++ b/src/telegram-service/Deal.Telegram.Tests/Telegram/DialogHueTests.cs @@ -1,7 +1,8 @@ using Deal.Telegram.Dialogs; using Xunit; +using Deal.Telegram.Tests.Support; -namespace Deal.Telegram.Tests; +namespace Deal.Telegram.Tests.Telegram; /// /// Unit-тесты цвета диалога (план Task 10: DialogHue — 1:1 dialog_hue прототипа L876–880 и палитры diff --git a/src/telegram-service/Deal.Telegram.Tests/DiscoveryOpsTests.cs b/src/telegram-service/Deal.Telegram.Tests/Telegram/DiscoveryOpsTests.cs similarity index 96% rename from src/telegram-service/Deal.Telegram.Tests/DiscoveryOpsTests.cs rename to src/telegram-service/Deal.Telegram.Tests/Telegram/DiscoveryOpsTests.cs index 84179f5..a318f9d 100644 --- a/src/telegram-service/Deal.Telegram.Tests/DiscoveryOpsTests.cs +++ b/src/telegram-service/Deal.Telegram.Tests/Telegram/DiscoveryOpsTests.cs @@ -5,8 +5,10 @@ using Deal.Telegram.Extensions; using Grpc.Core; using Microsoft.Extensions.Logging.Abstractions; using Xunit; +using Deal.Telegram.Tests.Support; +using Deal.Telegram.Tests.Grpc; -namespace Deal.Telegram.Tests; +namespace Deal.Telegram.Tests.Telegram; /// /// Unit-тесты discovery-операций (план Task 11): поиск с паузой 2–4 с и дедупом/обрезкой, join по diff --git a/src/telegram-service/Deal.Telegram.Tests/FakeSessionClient.cs b/src/telegram-service/Deal.Telegram.Tests/Telegram/FakeSessionClient.cs similarity index 96% rename from src/telegram-service/Deal.Telegram.Tests/FakeSessionClient.cs rename to src/telegram-service/Deal.Telegram.Tests/Telegram/FakeSessionClient.cs index bb0e59e..25d3d2c 100644 --- a/src/telegram-service/Deal.Telegram.Tests/FakeSessionClient.cs +++ b/src/telegram-service/Deal.Telegram.Tests/Telegram/FakeSessionClient.cs @@ -1,8 +1,9 @@ using Deal.Telegram.Sessions; using Deal.Telegram.Telegram; using Deal.Telegram.Extensions; +using Deal.Telegram.Tests.Support; -namespace Deal.Telegram.Tests; +namespace Deal.Telegram.Tests.Telegram; // Фейковый клиент Telegram для unit/in-proc тестов (план Task 9: фазовые переходы на fake-клиенте // абстракции ISessionClient; без сети). Поведение задаётся скриптом: ошибки/результаты кода/пароля, diff --git a/src/telegram-service/Deal.Telegram.Tests/FarmHarness.cs b/src/telegram-service/Deal.Telegram.Tests/Telegram/FarmHarness.cs similarity index 95% rename from src/telegram-service/Deal.Telegram.Tests/FarmHarness.cs rename to src/telegram-service/Deal.Telegram.Tests/Telegram/FarmHarness.cs index 15c4598..4ff574c 100644 --- a/src/telegram-service/Deal.Telegram.Tests/FarmHarness.cs +++ b/src/telegram-service/Deal.Telegram.Tests/Telegram/FarmHarness.cs @@ -1,8 +1,9 @@ using Deal.Telegram.Sessions; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; +using Deal.Telegram.Tests.Support; -namespace Deal.Telegram.Tests; +namespace Deal.Telegram.Tests.Telegram; // Харнесс пула сессий (SessionFarm) с готовой ready-сессией тенанта для unit-тестов служб каталога // (BackfillService/RealtimeSweep/RealtimeListener ходят в сессию через SessionFarm, как в проде). diff --git a/src/telegram-service/Deal.Telegram.Tests/LruCacheTests.cs b/src/telegram-service/Deal.Telegram.Tests/Telegram/LruCacheTests.cs similarity index 95% rename from src/telegram-service/Deal.Telegram.Tests/LruCacheTests.cs rename to src/telegram-service/Deal.Telegram.Tests/Telegram/LruCacheTests.cs index 39b0786..8edbcec 100644 --- a/src/telegram-service/Deal.Telegram.Tests/LruCacheTests.cs +++ b/src/telegram-service/Deal.Telegram.Tests/Telegram/LruCacheTests.cs @@ -1,7 +1,8 @@ using Deal.Telegram.Caching; using Xunit; +using Deal.Telegram.Tests.Support; -namespace Deal.Telegram.Tests; +namespace Deal.Telegram.Tests.Telegram; /// /// Unit-тесты LRU-кэша (этап 12, пакет C): вытеснение least-recently-used при переполнении, diff --git a/src/telegram-service/Deal.Telegram.Tests/RealtimeSweepTests.cs b/src/telegram-service/Deal.Telegram.Tests/Telegram/RealtimeSweepTests.cs similarity index 96% rename from src/telegram-service/Deal.Telegram.Tests/RealtimeSweepTests.cs rename to src/telegram-service/Deal.Telegram.Tests/Telegram/RealtimeSweepTests.cs index 065bb5b..0ab8a0e 100644 --- a/src/telegram-service/Deal.Telegram.Tests/RealtimeSweepTests.cs +++ b/src/telegram-service/Deal.Telegram.Tests/Telegram/RealtimeSweepTests.cs @@ -5,8 +5,10 @@ using Deal.Telegram.Extensions; using Grpc.Core; using Microsoft.Extensions.Logging.Abstractions; using Xunit; +using Deal.Telegram.Tests.Support; +using Deal.Telegram.Tests.Grpc; -namespace Deal.Telegram.Tests; +namespace Deal.Telegram.Tests.Telegram; /// /// Unit-тесты догона realtime (план Task 10 Acceptance: фильтр мониторинга; RealtimeSweep L392–456). diff --git a/src/telegram-service/Deal.Telegram.Tests/SessionHarness.cs b/src/telegram-service/Deal.Telegram.Tests/Telegram/SessionHarness.cs similarity index 95% rename from src/telegram-service/Deal.Telegram.Tests/SessionHarness.cs rename to src/telegram-service/Deal.Telegram.Tests/Telegram/SessionHarness.cs index 2730d75..4a41fb0 100644 --- a/src/telegram-service/Deal.Telegram.Tests/SessionHarness.cs +++ b/src/telegram-service/Deal.Telegram.Tests/Telegram/SessionHarness.cs @@ -1,7 +1,8 @@ using Deal.Telegram.Sessions; using Microsoft.Extensions.Logging.Abstractions; +using Deal.Telegram.Tests.Support; -namespace Deal.Telegram.Tests; +namespace Deal.Telegram.Tests.Telegram; // Харнесс ready-сессии для unit-тестов каталога (план Task 10): реальное хранилище в temp-каталоге, // фейковая фабрика клиентов и сессия, возобновлённая из «сохранённой авторизованной сессии» diff --git a/src/telegram-service/Deal.Telegram.Tests/TestSessionFactory.cs b/src/telegram-service/Deal.Telegram.Tests/Telegram/TestSessionFactory.cs similarity index 95% rename from src/telegram-service/Deal.Telegram.Tests/TestSessionFactory.cs rename to src/telegram-service/Deal.Telegram.Tests/Telegram/TestSessionFactory.cs index 2ef464e..a6b5d44 100644 --- a/src/telegram-service/Deal.Telegram.Tests/TestSessionFactory.cs +++ b/src/telegram-service/Deal.Telegram.Tests/Telegram/TestSessionFactory.cs @@ -1,7 +1,8 @@ using Deal.Telegram.Sessions; using Microsoft.Extensions.Logging.Abstractions; +using Deal.Telegram.Tests.Support; -namespace Deal.Telegram.Tests; +namespace Deal.Telegram.Tests.Telegram; // Фабрика компонентов хранилища сессий для unit-тестов: уникальный temp-каталог на сценарий, // тестовый ключ AES-256 и реальные SessionFileCipher/SessionStore (без сети). diff --git a/src/telegram-service/Deal.Telegram.Tests/TlMessageMapperTests.cs b/src/telegram-service/Deal.Telegram.Tests/Telegram/TlMessageMapperTests.cs similarity index 96% rename from src/telegram-service/Deal.Telegram.Tests/TlMessageMapperTests.cs rename to src/telegram-service/Deal.Telegram.Tests/Telegram/TlMessageMapperTests.cs index 41788ad..5b90c9f 100644 --- a/src/telegram-service/Deal.Telegram.Tests/TlMessageMapperTests.cs +++ b/src/telegram-service/Deal.Telegram.Tests/Telegram/TlMessageMapperTests.cs @@ -2,8 +2,9 @@ using Deal.Telegram.Telegram; using Deal.Telegram.Extensions; using TL; using Xunit; +using Deal.Telegram.Tests.Support; -namespace Deal.Telegram.Tests; +namespace Deal.Telegram.Tests.Telegram; /// /// Unit-тесты веток realtime-разбора на фейковых TL-объектах (без сети; план Task 10, ревью-фикс: