diff --git a/scripts/make_explicit.py b/scripts/make_explicit.py new file mode 100644 index 0000000..6765151 --- /dev/null +++ b/scripts/make_explicit.py @@ -0,0 +1,234 @@ +#!/usr/bin/env python3 +"""Codemod: неявные реализации интерфейсов -> явные (§11, вариант A — прод). + +Только production-код (/tests/ не трогает). Card (DTO) пропускается. +Логика: таблицы членов интерфейсов -> маппинг класс -> интерфейсы (включая partial) -> +конвертация public-члена с совпавшим именем и сигнатурой в `Тип IFoo.Член`. +Ошибочный конверсионный файл откатывается компиляторным циклом (git checkout). + +Запуск: python scripts/make_explicit.py [--apply] [--file <путь>] +""" +from __future__ import annotations + +import re +import sys +from pathlib import Path + +REPO = Path(__file__).resolve().parent.parent +ROOTS = [REPO / "src" / "core", REPO / "src" / "telegram-service", REPO / "src" / "ai-service", + REPO / "src" / "ml-service", REPO / "src" / "storage-service", REPO / "src" / "grpc-hosting"] +# файлы-исключения: DTO/модели, члены которых — собственный публичный API +EXCLUDE_FILES = {"src/core/Deal.Modules.Cards/Application/Models/Card.cs"} + +IFACE_DECL = re.compile( + r"^\s*(?:public\s+|internal\s+)?(?:partial\s+|sealed\s+|static\s+|unsafe\s+)*interface\s+(\w+)", re.M) +CLASS_DECL = re.compile( + r"^\s*(?:public\s+|internal\s+)?(?:sealed\s+|abstract\s+|static\s+|partial\s+|unsafe\s+)*" + r"class\s+(\w+)(?:<[^>]*>)?[^{;]*:\s*([^{;]+)", re.M) +PARTIAL_CLASS = re.compile( + r"^\s*(?:public\s+|internal\s+)?(?:sealed\s+|abstract\s+|static\s+|partial\s+|unsafe\s+)*" + r"class\s+(\w+)(?:<[^>]*>)?\s*(?::[^{;]+)?\{", re.M) +IFNAME = re.compile(r"\bI[A-Z]\w*") +METHOD_DECL = re.compile( + r"^(?P\s*)(?P(?:public\s+|internal\s+|protected\s+|private\s+|static\s+|" + r"virtual\s+|override\s+|sealed\s+|async\s+|new\s+)*)" + r"(?P[\w<>\[\],\s.?]+?)\s(?P\w+)\s*(?:<[^>]*>)?\s*\(") +PROP_DECL = re.compile( + r"^(?P\s*)(?P(?:public\s+|internal\s+|protected\s+|private\s+|static\s+|" + r"virtual\s+|override\s+|sealed\s+|new\s+)*)" + r"(?P[\w<>\[\],\s.?]+?)\s(?P\w+)\s*\{\s*(?P(?:public\s+)?(?:get|set|init)[^}]*)\}") + + +def src_files() -> list[Path]: + out = [] + for root in ROOTS: + if not root.exists(): + continue + for p in root.rglob("*.cs"): + s = p.as_posix().lower() + if "/bin/" in s or "/obj/" in s or "/tests/" in s or ".tests/" in s: + continue + out.append(p) + return out + + +def interface_body(text: str, start: int) -> str: + b = text.find("{", start) + if b == -1: + return "" + depth, end = 0, b + for i in range(b, len(text)): + if text[i] == "{": + depth += 1 + elif text[i] == "}": + depth -= 1 + if depth == 0: + end = i + break + return text[b:end] + + +def norm_types(params: str) -> tuple[str, ...]: + if not params.strip(): + return () + out, depth, cur = [], 0, "" + for ch in params: + if ch in "<([": + depth += 1 + elif ch in ">)]": + depth -= 1 + if ch == "," and depth == 0: + out.append(cur) + cur = "" + else: + cur += ch + out.append(cur) + res = [] + for raw in out: + t = re.sub(r"\s+", " ", raw.strip()) + parts = t.rsplit(" ", 1) + if len(parts) == 2 and not re.search(r"[<>\[\](),]", parts[1]) and parts[1] not in ("*", "&"): + t = parts[0] + res.append(t.replace("?", "").replace(" ", "")) + return tuple(res) + + +def collect_interfaces(files: list[Path]) -> dict[str, dict[str, dict]]: + tables: dict[str, dict[str, dict]] = {} + for p in files: + text = p.read_text(encoding="utf-8") + for m in IFACE_DECL.finditer(text): + name = m.group(1) + body = interface_body(text, m.start()) + lines = body.splitlines() + i = 0 + while i < len(lines): + line = lines[i] + mm = METHOD_DECL.match(line) + if mm and "(" in line: + # сигнатура может быть многострочной (§7: параметры на отдельных строках) + sig_text = line + while sig_text.count("(") > sig_text.count(")") and i + 1 < len(lines): + i += 1 + sig_text += " " + lines[i].strip() + if sig_text.count("(") > sig_text.count(")"): + i += 1 + continue + params = sig_text.split("(", 1)[1].rsplit(")", 1)[0] + tables.setdefault(name, {})[mm.group("name")] = {"kind": "method", "sig": norm_types(params)} + i += 1 + continue + pm = PROP_DECL.match(line) + if pm: + tables.setdefault(name, {})[pm.group("name")] = { + "kind": "prop", "sig": pm.group("type").replace("?", "").replace(" ", "")} + i += 1 + return tables + + +def collect_class_ifaces(files: list[Path]) -> dict[str, set[str]]: + result: dict[str, set[str]] = {} + for p in files: + rel = p.relative_to(REPO).as_posix() + if rel in EXCLUDE_FILES: + continue + text = p.read_text(encoding="utf-8") + for m in CLASS_DECL.finditer(text): + ifaces = set(IFNAME.findall(m.group(2))) + if ifaces: + result.setdefault(m.group(1), set()).update(ifaces) + return result + + +def convert_file(path: Path, cls_ifaces: dict[str, set[str]], itables: dict[str, dict[str, dict]]): + text = path.read_text(encoding="utf-8") + lines = text.splitlines(keepends=True) + local: set[str] = set() + for m in PARTIAL_CLASS.finditer(text): + if m.group(1) in cls_ifaces: + local.add(m.group(1)) + if not local: + return 0, None, [] + allowed = {i for cls in local for i in cls_ifaces[cls] if i in itables} + + changed, notes = 0, [] + i = 0 + while i < len(lines): + line = lines[i] + stripped = line.lstrip() + if stripped.startswith("//") or stripped.startswith("#"): + i += 1 + continue + mm = METHOD_DECL.match(line.rstrip("\r\n")) + pm = None if mm else PROP_DECL.match(line.rstrip("\r\n")) + if not mm and not pm: + i += 1 + continue + kind = "method" if mm else "prop" + decl = mm or pm + mods = decl.group("mods") + if "static" in mods or "public" not in mods: + i += 1 + continue + name = decl.group("name") + if kind == "method": + sig_text, j = line, i + while sig_text.count("(") > sig_text.count(")") and j + 1 < len(lines): + j += 1 + sig_text += lines[j] + if "(" not in sig_text: + i += 1 + continue + sig = norm_types(sig_text.split("(", 1)[1].rsplit(")", 1)[0]) + else: + sig = decl.group("type").replace("?", "").replace(" ", "") + j = i + cands = [ifc for ifc in allowed + if itables[ifc].get(name, {}).get("kind") == kind and itables[ifc][name]["sig"] == sig] + if not cands: + i += 1 + continue + ifc = cands[0] + head = line.rstrip("\r\n") + eol = line[len(head):] + # убрать модификаторы (оставить отступ), затем квалифицировать имя + new_head = re.sub(r"^(\s*)(?:public\s+|internal\s+|protected\s+|private\s+|virtual\s+|" + r"override\s+|sealed\s+|new\s+)+", r"\1", head) + m2 = re.search(r"\s" + re.escape(name) + r"(?![\w])", new_head) + if not m2: + i += 1 + continue + new_head = new_head[:m2.start()] + " " + ifc + "." + name + new_head[m2.end():] + lines[i] = new_head + eol + changed += 1 + notes.append(f"{path.relative_to(REPO).as_posix()}:{i + 1} -> {ifc}.{name}") + i = j + 1 + return changed, ("".join(lines) if changed else None), notes + + +def main() -> int: + apply = "--apply" in sys.argv + only = sys.argv[sys.argv.index("--file") + 1] if "--file" in sys.argv else None + files = src_files() + itables = collect_interfaces(files) + cls_ifaces = collect_class_ifaces(files) + total, per_file = 0, 0 + for p in files: + if only and p.as_posix() != only: + continue + changed, new_text, notes = convert_file(p, cls_ifaces, itables) + if not changed: + continue + total += changed + per_file += 1 + if apply and new_text is not None: + p.write_bytes(new_text.replace("\r\n", "\n").encode("utf-8")) + if not apply: + for n in notes[:3]: + print(" ", n) + print(f"{'apply' if apply else 'dry-run'}: конверсий {total} в {per_file} файлах") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/src/ai-service/Deal.Ai.Tests/Support/LlmHttpClientTests.cs b/src/ai-service/Deal.Ai.Tests/Support/LlmHttpClientTests.cs index 02669d6..58d2b33 100644 --- a/src/ai-service/Deal.Ai.Tests/Support/LlmHttpClientTests.cs +++ b/src/ai-service/Deal.Ai.Tests/Support/LlmHttpClientTests.cs @@ -53,7 +53,7 @@ public sealed class LlmHttpClientTests public async Task ChatAsync_OpenAiStyle_BuildsWireRequest() { var handler = new StubHttpMessageHandler(StubHttpMessageHandler.JsonOk(OpenAiJsonReply)); - LlmHttpClient client = CreateClient(handler); + IProviderClient client = CreateClient(handler); ProviderChatResult result = await client.ChatAsync( OpenAiConfig(apiKey: "secret-key"), @@ -87,7 +87,7 @@ public sealed class LlmHttpClientTests public async Task ChatAsync_OpenAiStyleWithoutApiKey_SkipsAuthorization() { var handler = new StubHttpMessageHandler(StubHttpMessageHandler.JsonOk(OpenAiJsonReply)); - LlmHttpClient client = CreateClient(handler); + IProviderClient client = CreateClient(handler); await client.ChatAsync(OpenAiConfig(apiKey: null), "Система", "Сообщение", CancellationToken.None); @@ -103,7 +103,7 @@ public sealed class LlmHttpClientTests const string reasoningOnlyReply = """{ "choices": [ { "message": { "role": "assistant", "reasoning_content": "хм, подумаю" } } ] }"""; var handler = new StubHttpMessageHandler(StubHttpMessageHandler.JsonOk(reasoningOnlyReply)); - LlmHttpClient client = CreateClient(handler); + IProviderClient client = CreateClient(handler); LlmHttpException exception = await Assert.ThrowsAsync(() => client.ChatAsync(OpenAiConfig(), "Система", "Сообщение", CancellationToken.None)); @@ -118,7 +118,7 @@ public sealed class LlmHttpClientTests public async Task ChatAsync_AnthropicStyle_BuildsWireRequest() { var handler = new StubHttpMessageHandler(StubHttpMessageHandler.JsonOk(AnthropicJsonReply)); - LlmHttpClient client = CreateClient(handler); + IProviderClient client = CreateClient(handler); ProviderChatResult result = await client.ChatAsync( AnthropicConfig(), @@ -153,7 +153,7 @@ public sealed class LlmHttpClientTests public async Task ChatAsync_HttpError_Throws() { var handler = new StubHttpMessageHandler(StubHttpMessageHandler.Status(HttpStatusCode.InternalServerError)); - LlmHttpClient client = CreateClient(handler); + IProviderClient client = CreateClient(handler); LlmHttpException exception = await Assert.ThrowsAsync(() => client.ChatAsync(OpenAiConfig(), "Система", "Сообщение", CancellationToken.None)); @@ -171,7 +171,7 @@ public sealed class LlmHttpClientTests { Content = new StringContent("upstream error", System.Text.Encoding.UTF8, "text/html"), }); - LlmHttpClient client = CreateClient(handler); + IProviderClient client = CreateClient(handler); await Assert.ThrowsAsync(() => client.ChatAsync(OpenAiConfig(), "Система", "Сообщение", CancellationToken.None)); @@ -186,7 +186,7 @@ public sealed class LlmHttpClientTests var handler = new StubHttpMessageHandler( StubHttpMessageHandler.JsonOk(OpenAiJsonReply), delay: TestHandlerDelay); - LlmHttpClient client = new( + IProviderClient client = new LlmHttpClient( TestHttpClient(handler), TestCallTimeout, TestCallTimeout); diff --git a/src/ai-service/Deal.Ai/Llm/LlmHttpClient.cs b/src/ai-service/Deal.Ai/Llm/LlmHttpClient.cs index 6dcc7f4..04e3072 100644 --- a/src/ai-service/Deal.Ai/Llm/LlmHttpClient.cs +++ b/src/ai-service/Deal.Ai/Llm/LlmHttpClient.cs @@ -65,7 +65,7 @@ public sealed class LlmHttpClient : IProviderClient /// Системный промпт. /// Пользовательское сообщение/контекст. /// Текст ответа и usage API-ответа (null при его отсутствии). - public async Task ChatAsync( + async Task IProviderClient.ChatAsync( LlmConfig config, string systemPrompt, string userText, diff --git a/src/core/Deal.Infrastructure/Data/TenantContext.cs b/src/core/Deal.Infrastructure/Data/TenantContext.cs index e65eb93..65eed6f 100644 --- a/src/core/Deal.Infrastructure/Data/TenantContext.cs +++ b/src/core/Deal.Infrastructure/Data/TenantContext.cs @@ -17,8 +17,8 @@ public sealed class TenantContext : ITenantContext public string? SchemaName => Current.Value?.SchemaName; /// - public void SetTenant(TenantId tenantId) => Current.Value = tenantId; + void ITenantContext.SetTenant(TenantId tenantId) => Current.Value = tenantId; /// - public void Reset() => Current.Value = null; + void ITenantContext.Reset() => Current.Value = null; } diff --git a/src/core/Deal.Infrastructure/Integrations/Services/AiConnectionChecker.cs b/src/core/Deal.Infrastructure/Integrations/Services/AiConnectionChecker.cs index 5d4021a..78cf24b 100644 --- a/src/core/Deal.Infrastructure/Integrations/Services/AiConnectionChecker.cs +++ b/src/core/Deal.Infrastructure/Integrations/Services/AiConnectionChecker.cs @@ -79,7 +79,7 @@ public sealed class AiConnectionChecker : IAiConnectionChecker } /// - public async Task CheckAsync(AiCheckRequest request, CancellationToken ct) + async Task IAiConnectionChecker.CheckAsync(AiCheckRequest request, CancellationToken ct) { ArgumentNullException.ThrowIfNull(request); diff --git a/src/core/Deal.Infrastructure/Integrations/Services/BudgetedAiClassifier.cs b/src/core/Deal.Infrastructure/Integrations/Services/BudgetedAiClassifier.cs index e42a5ab..c4a7807 100644 --- a/src/core/Deal.Infrastructure/Integrations/Services/BudgetedAiClassifier.cs +++ b/src/core/Deal.Infrastructure/Integrations/Services/BudgetedAiClassifier.cs @@ -55,7 +55,7 @@ public sealed class BudgetedAiClassifier : IAiClassifier } /// - public async Task FilterAsync(string text, CancellationToken ct) + async Task IAiClassifier.FilterAsync(string text, CancellationToken ct) { if (await IsPaidAllowedAsync(ct)) { @@ -68,7 +68,7 @@ public sealed class BudgetedAiClassifier : IAiClassifier } /// - public async Task ClassifyAsync(string text, CancellationToken ct) + async Task IAiClassifier.ClassifyAsync(string text, CancellationToken ct) { if (await IsPaidAllowedAsync(ct)) { diff --git a/src/core/Deal.Infrastructure/Integrations/Services/BudgetedAiTools.cs b/src/core/Deal.Infrastructure/Integrations/Services/BudgetedAiTools.cs index 16a867a..381775d 100644 --- a/src/core/Deal.Infrastructure/Integrations/Services/BudgetedAiTools.cs +++ b/src/core/Deal.Infrastructure/Integrations/Services/BudgetedAiTools.cs @@ -51,7 +51,7 @@ public sealed class BudgetedAiTools : IAiTools } /// - public async Task GenerateKeywordsAsync(string description, CancellationToken ct) + async Task IAiTools.GenerateKeywordsAsync(string description, CancellationToken ct) { BudgetStateDto state = await GateStateAsync(ct); if (state.Allowed) @@ -70,7 +70,7 @@ public sealed class BudgetedAiTools : IAiTools } /// - public async Task EvaluateFitAsync( + async Task IAiTools.EvaluateFitAsync( string text, string description, IReadOnlyCollection keywords, diff --git a/src/core/Deal.Infrastructure/Integrations/Services/CbrRateSource.cs b/src/core/Deal.Infrastructure/Integrations/Services/CbrRateSource.cs index 93ef35d..3dce510 100644 --- a/src/core/Deal.Infrastructure/Integrations/Services/CbrRateSource.cs +++ b/src/core/Deal.Infrastructure/Integrations/Services/CbrRateSource.cs @@ -48,7 +48,7 @@ public sealed class CbrRateSource : IRatesSource } /// - public async Task?> FetchAsync(CancellationToken ct) + async Task?> IRatesSource.FetchAsync(CancellationToken ct) { try { diff --git a/src/core/Deal.Infrastructure/Integrations/Services/GrpcAiClassifier.cs b/src/core/Deal.Infrastructure/Integrations/Services/GrpcAiClassifier.cs index c21cd9a..c9bd692 100644 --- a/src/core/Deal.Infrastructure/Integrations/Services/GrpcAiClassifier.cs +++ b/src/core/Deal.Infrastructure/Integrations/Services/GrpcAiClassifier.cs @@ -71,7 +71,7 @@ public sealed class GrpcAiClassifier : IAiClassifier } /// - public async Task FilterAsync(string text, CancellationToken ct) + async Task IAiClassifier.FilterAsync(string text, CancellationToken ct) { TenantId tenantId = RequireTenant(); try @@ -107,7 +107,7 @@ public sealed class GrpcAiClassifier : IAiClassifier } /// - public async Task ClassifyAsync(string text, CancellationToken ct) + async Task IAiClassifier.ClassifyAsync(string text, CancellationToken ct) { TenantId tenantId = RequireTenant(); string systemPrompt = await _contextBuilder.BuildClassifySystemPromptAsync(ct); diff --git a/src/core/Deal.Infrastructure/Integrations/Services/GrpcAiTools.cs b/src/core/Deal.Infrastructure/Integrations/Services/GrpcAiTools.cs index a82cd19..c0c92ae 100644 --- a/src/core/Deal.Infrastructure/Integrations/Services/GrpcAiTools.cs +++ b/src/core/Deal.Infrastructure/Integrations/Services/GrpcAiTools.cs @@ -71,7 +71,7 @@ public sealed class GrpcAiTools : IAiTools } /// - public async Task GenerateKeywordsAsync(string description, CancellationToken ct) + async Task IAiTools.GenerateKeywordsAsync(string description, CancellationToken ct) { TenantId tenantId = RequireTenant(); try @@ -105,7 +105,7 @@ public sealed class GrpcAiTools : IAiTools } /// - public async Task EvaluateFitAsync( + async Task IAiTools.EvaluateFitAsync( string text, string description, IReadOnlyCollection keywords, diff --git a/src/core/Deal.Infrastructure/Integrations/Services/GrpcMlClient.cs b/src/core/Deal.Infrastructure/Integrations/Services/GrpcMlClient.cs index 11ec1f4..2b53c3a 100644 --- a/src/core/Deal.Infrastructure/Integrations/Services/GrpcMlClient.cs +++ b/src/core/Deal.Infrastructure/Integrations/Services/GrpcMlClient.cs @@ -98,7 +98,7 @@ public sealed class GrpcMlClient : IMlClient, IMlTrainClient } /// - public async Task StatusAsync(CancellationToken ct) + async Task IMlClient.StatusAsync(CancellationToken ct) { TenantId tenantId = RequireTenant(); MlStatusCache.Snapshot snapshot = await GetServiceSnapshotAsync(tenantId, ct); @@ -123,7 +123,7 @@ public sealed class GrpcMlClient : IMlClient, IMlTrainClient } /// - public async Task PredictAsync(string text, CancellationToken ct) + async Task IMlClient.PredictAsync(string text, CancellationToken ct) { TenantId tenantId = RequireTenant(); try @@ -144,7 +144,7 @@ public sealed class GrpcMlClient : IMlClient, IMlTrainClient } /// - public async Task ResetAsync(CancellationToken ct) + async Task IMlClient.ResetAsync(CancellationToken ct) { TenantId tenantId = RequireTenant(); ResetReply reply; @@ -172,7 +172,7 @@ public sealed class GrpcMlClient : IMlClient, IMlTrainClient } /// - public async Task PushAsync( + async Task IMlClient.PushAsync( string text, string label, double delta, @@ -182,7 +182,7 @@ public sealed class GrpcMlClient : IMlClient, IMlTrainClient } /// - public async Task TrainBatchAsync(IReadOnlyList items, CancellationToken ct) + async Task IMlTrainClient.TrainBatchAsync(IReadOnlyList items, CancellationToken ct) { TenantId tenantId = RequireTenant(); var request = new TrainBatchRequest(); diff --git a/src/core/Deal.Infrastructure/Integrations/Services/GrpcTelegramClient.cs b/src/core/Deal.Infrastructure/Integrations/Services/GrpcTelegramClient.cs index b2ce470..d7465bf 100644 --- a/src/core/Deal.Infrastructure/Integrations/Services/GrpcTelegramClient.cs +++ b/src/core/Deal.Infrastructure/Integrations/Services/GrpcTelegramClient.cs @@ -59,7 +59,7 @@ public sealed class GrpcTelegramClient : ITelegramGateway } /// - public async Task StatusAsync(CancellationToken ct) + async Task ITelegramGateway.StatusAsync(CancellationToken ct) { TenantId tenantId = RequireTenant(); try @@ -82,7 +82,7 @@ public sealed class GrpcTelegramClient : ITelegramGateway } /// - public async Task StartPhoneAsync( + async Task ITelegramGateway.StartPhoneAsync( string phone, int apiId, string apiHash, @@ -105,7 +105,7 @@ public sealed class GrpcTelegramClient : ITelegramGateway } /// - public async Task StartQrAsync( + async Task ITelegramGateway.StartQrAsync( int apiId, string apiHash, CancellationToken ct) @@ -129,7 +129,7 @@ public sealed class GrpcTelegramClient : ITelegramGateway } /// - public async Task SendCodeAsync(string code, CancellationToken ct) + async Task ITelegramGateway.SendCodeAsync(string code, CancellationToken ct) { TenantId tenantId = RequireTenant(); try @@ -147,7 +147,7 @@ public sealed class GrpcTelegramClient : ITelegramGateway } /// - public async Task SendPasswordAsync(string password, CancellationToken ct) + async Task ITelegramGateway.SendPasswordAsync(string password, CancellationToken ct) { TenantId tenantId = RequireTenant(); try @@ -165,7 +165,7 @@ public sealed class GrpcTelegramClient : ITelegramGateway } /// - public async Task LogoutAsync(CancellationToken ct) + async Task ITelegramGateway.LogoutAsync(CancellationToken ct) { TenantId tenantId = RequireTenant(); try @@ -181,7 +181,7 @@ public sealed class GrpcTelegramClient : ITelegramGateway } /// - public async Task> RefreshDialogsAsync(CancellationToken ct) + async Task> ITelegramGateway.RefreshDialogsAsync(CancellationToken ct) { TenantId tenantId = RequireTenant(); try @@ -198,7 +198,7 @@ public sealed class GrpcTelegramClient : ITelegramGateway } /// - public async Task SetMonitorAsync( + async Task ITelegramGateway.SetMonitorAsync( string dialogId, bool enabled, CancellationToken ct) @@ -218,7 +218,7 @@ public sealed class GrpcTelegramClient : ITelegramGateway } /// - public async Task SetMonitorAllAsync(bool enabled, CancellationToken ct) + async Task ITelegramGateway.SetMonitorAllAsync(bool enabled, CancellationToken ct) { TenantId tenantId = RequireTenant(); try @@ -235,7 +235,7 @@ public sealed class GrpcTelegramClient : ITelegramGateway } /// - public async Task BackfillAsync( + async Task ITelegramGateway.BackfillAsync( string dialogId, bool force, CancellationToken ct) @@ -256,7 +256,7 @@ public sealed class GrpcTelegramClient : ITelegramGateway } /// - public async Task> ReadRecentAsync( + async Task> ITelegramGateway.ReadRecentAsync( string dialogId, int limit, CancellationToken ct) @@ -279,7 +279,7 @@ public sealed class GrpcTelegramClient : ITelegramGateway } /// - public async Task ReadSourceAsync( + async Task ITelegramGateway.ReadSourceAsync( string dialogId, long msgId, CancellationToken ct) @@ -303,7 +303,7 @@ public sealed class GrpcTelegramClient : ITelegramGateway } /// - public async Task> SearchAsync( + async Task> ITelegramGateway.SearchAsync( string query, int limit, CancellationToken ct) @@ -324,7 +324,7 @@ public sealed class GrpcTelegramClient : ITelegramGateway } /// - public async Task InfoAsync(string dialogId, CancellationToken ct) + async Task ITelegramGateway.InfoAsync(string dialogId, CancellationToken ct) { TenantId tenantId = RequireTenant(); try @@ -350,7 +350,7 @@ public sealed class GrpcTelegramClient : ITelegramGateway } /// - public async Task ReadForEvalAsync( + async Task ITelegramGateway.ReadForEvalAsync( string dialogId, int limit, CancellationToken ct) @@ -381,7 +381,7 @@ public sealed class GrpcTelegramClient : ITelegramGateway } /// - public async Task JoinAsync(string username, CancellationToken ct) + async Task ITelegramGateway.JoinAsync(string username, CancellationToken ct) { TenantId tenantId = RequireTenant(); try @@ -398,7 +398,7 @@ public sealed class GrpcTelegramClient : ITelegramGateway } /// - public async Task LeaveAsync(string dialogId, CancellationToken ct) + async Task ITelegramGateway.LeaveAsync(string dialogId, CancellationToken ct) { TenantId tenantId = RequireTenant(); try diff --git a/src/core/Deal.Infrastructure/Integrations/Services/LocalAiTools.cs b/src/core/Deal.Infrastructure/Integrations/Services/LocalAiTools.cs index 33e42fc..a67f332 100644 --- a/src/core/Deal.Infrastructure/Integrations/Services/LocalAiTools.cs +++ b/src/core/Deal.Infrastructure/Integrations/Services/LocalAiTools.cs @@ -13,11 +13,11 @@ public sealed class LocalAiTools : IAiTools "ИИ-инструменты доступны только при подключённом ai-service (Services:Ai:UseLocal=false)."; /// - public Task GenerateKeywordsAsync(string description, CancellationToken ct) + Task IAiTools.GenerateKeywordsAsync(string description, CancellationToken ct) => throw new NotSupportedException(NotSupportedMessage); /// - public Task EvaluateFitAsync( + Task IAiTools.EvaluateFitAsync( string text, string description, IReadOnlyCollection keywords, diff --git a/src/core/Deal.Infrastructure/Integrations/Services/LocalTelegramGateway.cs b/src/core/Deal.Infrastructure/Integrations/Services/LocalTelegramGateway.cs index e024c3d..1af1518 100644 --- a/src/core/Deal.Infrastructure/Integrations/Services/LocalTelegramGateway.cs +++ b/src/core/Deal.Infrastructure/Integrations/Services/LocalTelegramGateway.cs @@ -12,13 +12,13 @@ public sealed class LocalTelegramGateway : ITelegramGateway private const string IdlePhase = "idle"; /// - public Task StatusAsync(CancellationToken ct) + Task ITelegramGateway.StatusAsync(CancellationToken ct) { return Task.FromResult(new TelegramAccountStatusDto(IdlePhase, false, false, string.Empty, null, null)); } /// - public Task StartPhoneAsync( + Task ITelegramGateway.StartPhoneAsync( string phone, int apiId, string apiHash, @@ -26,7 +26,7 @@ public sealed class LocalTelegramGateway : ITelegramGateway => Task.FromResult(new TelegramAuthResultDto(IdlePhase, null)); /// - public Task StartQrAsync( + Task ITelegramGateway.StartQrAsync( int apiId, string apiHash, CancellationToken ct) @@ -39,20 +39,20 @@ public sealed class LocalTelegramGateway : ITelegramGateway public Task SendPasswordAsync(string password, CancellationToken ct) => Task.FromResult(IdlePhase); /// - public Task LogoutAsync(CancellationToken ct) => Task.CompletedTask; + Task ITelegramGateway.LogoutAsync(CancellationToken ct) => Task.CompletedTask; /// - public Task> RefreshDialogsAsync(CancellationToken ct) + Task> ITelegramGateway.RefreshDialogsAsync(CancellationToken ct) => Task.FromResult>([]); /// - public Task SetMonitorAsync( + Task ITelegramGateway.SetMonitorAsync( string dialogId, bool enabled, CancellationToken ct) => Task.CompletedTask; /// - public Task SetMonitorAllAsync(bool enabled, CancellationToken ct) => Task.CompletedTask; + Task ITelegramGateway.SetMonitorAllAsync(bool enabled, CancellationToken ct) => Task.CompletedTask; /// public Task BackfillAsync( @@ -61,40 +61,40 @@ public sealed class LocalTelegramGateway : ITelegramGateway CancellationToken ct) => Task.FromResult(0); /// - public Task> ReadRecentAsync( + Task> ITelegramGateway.ReadRecentAsync( string dialogId, int limit, CancellationToken ct) => Task.FromResult>([]); /// - public Task ReadSourceAsync( + Task ITelegramGateway.ReadSourceAsync( string dialogId, long msgId, CancellationToken ct) => Task.FromResult(new TelegramSourceContentDto(false, null, null)); /// - public Task> SearchAsync( + Task> ITelegramGateway.SearchAsync( string query, int limit, CancellationToken ct) => Task.FromResult>([]); /// - public Task InfoAsync(string dialogId, CancellationToken ct) + Task ITelegramGateway.InfoAsync(string dialogId, CancellationToken ct) => Task.FromResult(new TelegramChannelInfoDto(dialogId, string.Empty, string.Empty, string.Empty, SourceDefaults.DefaultHue, null, false)); /// - public Task ReadForEvalAsync( + Task ITelegramGateway.ReadForEvalAsync( string dialogId, int limit, CancellationToken ct) => Task.FromResult(new TelegramEvalReadDto(false, "no_history", [])); /// - public Task JoinAsync(string username, CancellationToken ct) => Task.CompletedTask; + Task ITelegramGateway.JoinAsync(string username, CancellationToken ct) => Task.CompletedTask; /// - public Task LeaveAsync(string dialogId, CancellationToken ct) => Task.CompletedTask; + Task ITelegramGateway.LeaveAsync(string dialogId, CancellationToken ct) => Task.CompletedTask; } diff --git a/src/core/Deal.Infrastructure/Integrations/Storage/Services/LocalFileStorage.cs b/src/core/Deal.Infrastructure/Integrations/Storage/Services/LocalFileStorage.cs index 3a75e94..2619ddb 100644 --- a/src/core/Deal.Infrastructure/Integrations/Storage/Services/LocalFileStorage.cs +++ b/src/core/Deal.Infrastructure/Integrations/Storage/Services/LocalFileStorage.cs @@ -32,7 +32,7 @@ public sealed class LocalFileStorage : IFileStorage public override string ToString() => $"LocalFileStorage (root: {_rootPath})"; /// - public async Task PutAsync( + async Task IFileStorage.PutAsync( string objectKey, Stream content, string contentType, @@ -56,7 +56,7 @@ public sealed class LocalFileStorage : IFileStorage } /// - public Task GetAsync(string objectKey, CancellationToken ct) + Task IFileStorage.GetAsync(string objectKey, CancellationToken ct) { string path = ResolvePath(objectKey); if (!File.Exists(path)) @@ -69,7 +69,7 @@ public sealed class LocalFileStorage : IFileStorage } /// - public Task StatAsync(string objectKey, CancellationToken ct) + Task IFileStorage.StatAsync(string objectKey, CancellationToken ct) { string path = ResolvePath(objectKey); if (!File.Exists(path)) @@ -82,7 +82,7 @@ public sealed class LocalFileStorage : IFileStorage } /// - public Task DeleteAsync(string objectKey, CancellationToken ct) + Task IFileStorage.DeleteAsync(string objectKey, CancellationToken ct) { string path = ResolvePath(objectKey); if (File.Exists(path)) diff --git a/src/core/Deal.Infrastructure/Integrations/Storage/Services/MinioFileStorage.cs b/src/core/Deal.Infrastructure/Integrations/Storage/Services/MinioFileStorage.cs index 1be9229..8e47439 100644 --- a/src/core/Deal.Infrastructure/Integrations/Storage/Services/MinioFileStorage.cs +++ b/src/core/Deal.Infrastructure/Integrations/Storage/Services/MinioFileStorage.cs @@ -67,7 +67,7 @@ public sealed class MinioFileStorage : IFileStorage public override string ToString() => $"MinioFileStorage (endpoint: {_endpoint}; bucket: {_bucket})"; /// - public async Task PutAsync( + async Task IFileStorage.PutAsync( string objectKey, Stream content, string contentType, @@ -99,7 +99,7 @@ public sealed class MinioFileStorage : IFileStorage } /// - public async Task GetAsync(string objectKey, CancellationToken ct) + async Task IFileStorage.GetAsync(string objectKey, CancellationToken ct) { MemoryStream buffer = new(); try @@ -129,7 +129,7 @@ public sealed class MinioFileStorage : IFileStorage } /// - public async Task StatAsync(string objectKey, CancellationToken ct) + async Task IFileStorage.StatAsync(string objectKey, CancellationToken ct) { try { @@ -145,7 +145,7 @@ public sealed class MinioFileStorage : IFileStorage } /// - public async Task DeleteAsync(string objectKey, CancellationToken ct) + async Task IFileStorage.DeleteAsync(string objectKey, CancellationToken ct) { try { diff --git a/src/core/Deal.Infrastructure/Persistence/Repositories/DiscoveryStore.Blacklist.cs b/src/core/Deal.Infrastructure/Persistence/Repositories/DiscoveryStore.Blacklist.cs index 2a1d0c8..9df820d 100644 --- a/src/core/Deal.Infrastructure/Persistence/Repositories/DiscoveryStore.Blacklist.cs +++ b/src/core/Deal.Infrastructure/Persistence/Repositories/DiscoveryStore.Blacklist.cs @@ -1,6 +1,7 @@ using Deal.Infrastructure.Persistence.Entities; using Deal.Modules.Discovery.Application.Models; using Microsoft.EntityFrameworkCore; +using Deal.Modules.Discovery.Application.Abstractions; namespace Deal.Infrastructure.Persistence.Repositories; @@ -10,7 +11,7 @@ namespace Deal.Infrastructure.Persistence.Repositories; public sealed partial class DiscoveryStore { /// - public async Task UpsertBlacklistAsync( + async Task IDiscoveryStore.UpsertBlacklistAsync( string dialogId, string name, string reason, @@ -38,13 +39,13 @@ public sealed partial class DiscoveryStore } /// - public async Task RemoveBlacklistAsync(string dialogId, CancellationToken ct) + async Task IDiscoveryStore.RemoveBlacklistAsync(string dialogId, CancellationToken ct) { await _dbContext.DiscBlacklist.Where(entry => entry.DialogId == dialogId).ExecuteDeleteAsync(ct); } /// - public async Task GetBlacklistAsync(string dialogId, CancellationToken ct) + async Task IDiscoveryStore.GetBlacklistAsync(string dialogId, CancellationToken ct) { DiscBlacklistEntity? row = await _dbContext.DiscBlacklist .AsNoTracking() @@ -53,7 +54,7 @@ public sealed partial class DiscoveryStore } /// - public async Task> ListBlacklistAsync(CancellationToken ct) + async Task> IDiscoveryStore.ListBlacklistAsync(CancellationToken ct) { List rows = await _dbContext.DiscBlacklist .AsNoTracking() diff --git a/src/core/Deal.Infrastructure/Persistence/Repositories/DiscoveryStore.Candidates.cs b/src/core/Deal.Infrastructure/Persistence/Repositories/DiscoveryStore.Candidates.cs index 6573fd7..57f27fd 100644 --- a/src/core/Deal.Infrastructure/Persistence/Repositories/DiscoveryStore.Candidates.cs +++ b/src/core/Deal.Infrastructure/Persistence/Repositories/DiscoveryStore.Candidates.cs @@ -1,6 +1,7 @@ using Deal.Infrastructure.Persistence.Entities; using Deal.Modules.Discovery.Application.Models; using Microsoft.EntityFrameworkCore; +using Deal.Modules.Discovery.Application.Abstractions; namespace Deal.Infrastructure.Persistence.Repositories; @@ -10,7 +11,7 @@ namespace Deal.Infrastructure.Persistence.Repositories; public sealed partial class DiscoveryStore { /// - public async Task> ListCandidatesAsync( + async Task> IDiscoveryStore.ListCandidatesAsync( string taskId, string? status, CancellationToken ct) @@ -26,7 +27,7 @@ public sealed partial class DiscoveryStore } /// - public async Task GetCandidateAsync(string dialogId, CancellationToken ct) + async Task IDiscoveryStore.GetCandidateAsync(string dialogId, CancellationToken ct) { DiscCandidateEntity? row = await _dbContext.DiscCandidates .AsNoTracking() @@ -35,19 +36,19 @@ public sealed partial class DiscoveryStore } /// - public async Task IsDialogMonitoredAsync(string dialogId, CancellationToken ct) + async Task IDiscoveryStore.IsDialogMonitoredAsync(string dialogId, CancellationToken ct) { return await _dbContext.Dialogs.AnyAsync(dialog => dialog.Id == dialogId, ct); } /// - public Task IsBlacklistedAsync(string dialogId, CancellationToken ct) + Task IDiscoveryStore.IsBlacklistedAsync(string dialogId, CancellationToken ct) { return _dbContext.DiscBlacklist.AnyAsync(row => row.DialogId == dialogId, ct); } /// - public async Task CreateCandidateAsync(DiscoveryCandidateRow row, CancellationToken ct) + async Task IDiscoveryStore.CreateCandidateAsync(DiscoveryCandidateRow row, CancellationToken ct) { DateTimeOffset now = DateTimeOffset.UtcNow; _dbContext.DiscCandidates.Add(new DiscCandidateEntity @@ -66,13 +67,13 @@ public sealed partial class DiscoveryStore } /// - public async Task DeleteCandidateAsync(string dialogId, CancellationToken ct) + async Task IDiscoveryStore.DeleteCandidateAsync(string dialogId, CancellationToken ct) { await _dbContext.DiscCandidates.Where(candidate => candidate.DialogId == dialogId).ExecuteDeleteAsync(ct); } /// - public async Task PatchCandidateAsync( + async Task IDiscoveryStore.PatchCandidateAsync( string dialogId, DiscoveryCandidatePatch patch, CancellationToken ct) @@ -91,7 +92,7 @@ public sealed partial class DiscoveryStore } /// - public async Task SetCandidateStatusAsync( + async Task IDiscoveryStore.SetCandidateStatusAsync( string dialogId, string status, CancellationToken ct) @@ -110,7 +111,7 @@ public sealed partial class DiscoveryStore } /// - public async Task SetCandidateJoinedAsync( + async Task IDiscoveryStore.SetCandidateJoinedAsync( string dialogId, bool autoJoined, CancellationToken ct) @@ -130,7 +131,7 @@ public sealed partial class DiscoveryStore } /// - public async Task IncrementJoinFailuresAsync(string dialogId, CancellationToken ct) + async Task IDiscoveryStore.IncrementJoinFailuresAsync(string dialogId, CancellationToken ct) { DiscCandidateEntity? row = await _dbContext.DiscCandidates .FirstOrDefaultAsync(candidate => candidate.DialogId == dialogId, ct); @@ -146,7 +147,7 @@ public sealed partial class DiscoveryStore } /// - public async Task SetCandidateRejectedAsync(string dialogId, CancellationToken ct) + async Task IDiscoveryStore.SetCandidateRejectedAsync(string dialogId, CancellationToken ct) { DiscCandidateEntity? row = await _dbContext.DiscCandidates .FirstOrDefaultAsync(candidate => candidate.DialogId == dialogId, ct); diff --git a/src/core/Deal.Infrastructure/Persistence/Repositories/DiscoveryStore.Logs.cs b/src/core/Deal.Infrastructure/Persistence/Repositories/DiscoveryStore.Logs.cs index a688408..4ae185f 100644 --- a/src/core/Deal.Infrastructure/Persistence/Repositories/DiscoveryStore.Logs.cs +++ b/src/core/Deal.Infrastructure/Persistence/Repositories/DiscoveryStore.Logs.cs @@ -1,6 +1,7 @@ using Deal.Infrastructure.Persistence.Entities; using Deal.Modules.Discovery.Application.Models; using Microsoft.EntityFrameworkCore; +using Deal.Modules.Discovery.Application.Abstractions; namespace Deal.Infrastructure.Persistence.Repositories; @@ -10,7 +11,7 @@ namespace Deal.Infrastructure.Persistence.Repositories; public sealed partial class DiscoveryStore { /// - public async Task AddLogAsync( + async Task IDiscoveryStore.AddLogAsync( string logId, string taskId, string logEvent, @@ -29,7 +30,7 @@ public sealed partial class DiscoveryStore } /// - public async Task CountLogEventAsync( + async Task IDiscoveryStore.CountLogEventAsync( string logEvent, DateTimeOffset sinceUtc, CancellationToken ct) @@ -38,7 +39,7 @@ public sealed partial class DiscoveryStore } /// - public async Task> ListTaskLogAsync( + async Task> IDiscoveryStore.ListTaskLogAsync( string taskId, int limit, CancellationToken ct) diff --git a/src/core/Deal.Infrastructure/Persistence/Repositories/DiscoveryStore.Tasks.cs b/src/core/Deal.Infrastructure/Persistence/Repositories/DiscoveryStore.Tasks.cs index 5cbe731..e39bae7 100644 --- a/src/core/Deal.Infrastructure/Persistence/Repositories/DiscoveryStore.Tasks.cs +++ b/src/core/Deal.Infrastructure/Persistence/Repositories/DiscoveryStore.Tasks.cs @@ -1,6 +1,7 @@ using Deal.Infrastructure.Persistence.Entities; using Deal.Modules.Discovery.Application.Models; using Microsoft.EntityFrameworkCore; +using Deal.Modules.Discovery.Application.Abstractions; namespace Deal.Infrastructure.Persistence.Repositories; @@ -10,7 +11,7 @@ namespace Deal.Infrastructure.Persistence.Repositories; public sealed partial class DiscoveryStore { /// - public async Task> ListTasksAsync(CancellationToken ct) + async Task> IDiscoveryStore.ListTasksAsync(CancellationToken ct) { List rows = await _dbContext.DiscTasks .AsNoTracking() @@ -20,7 +21,7 @@ public sealed partial class DiscoveryStore } /// - public async Task GetTaskAsync(string taskId, CancellationToken ct) + async Task IDiscoveryStore.GetTaskAsync(string taskId, CancellationToken ct) { DiscTaskEntity? row = await _dbContext.DiscTasks .AsNoTracking() @@ -29,7 +30,7 @@ public sealed partial class DiscoveryStore } /// - public async Task CreateTaskAsync(DiscoveryTaskRow row, CancellationToken ct) + async Task IDiscoveryStore.CreateTaskAsync(DiscoveryTaskRow row, CancellationToken ct) { DateTimeOffset now = DateTimeOffset.UtcNow; _dbContext.DiscTasks.Add(new DiscTaskEntity @@ -52,7 +53,7 @@ public sealed partial class DiscoveryStore } /// - public async Task PatchTaskAsync( + async Task IDiscoveryStore.PatchTaskAsync( string taskId, DiscoveryTaskPatch patch, CancellationToken ct) @@ -71,7 +72,7 @@ public sealed partial class DiscoveryStore } /// - public async Task DeleteTaskAsync(string taskId, CancellationToken ct) + async Task IDiscoveryStore.DeleteTaskAsync(string taskId, CancellationToken ct) { DiscTaskEntity? row = await _dbContext.DiscTasks .FirstOrDefaultAsync(task => task.Id == taskId, ct); @@ -88,7 +89,7 @@ public sealed partial class DiscoveryStore } /// - public async Task SetTaskRunningAsync( + async Task IDiscoveryStore.SetTaskRunningAsync( string taskId, bool resetProgress, CancellationToken ct) @@ -117,7 +118,7 @@ public sealed partial class DiscoveryStore } /// - public async Task SetTaskPausedAsync(string taskId, CancellationToken ct) + async Task IDiscoveryStore.SetTaskPausedAsync(string taskId, CancellationToken ct) { DiscTaskEntity? row = await _dbContext.DiscTasks .FirstOrDefaultAsync(task => task.Id == taskId, ct); @@ -133,7 +134,7 @@ public sealed partial class DiscoveryStore } /// - public async Task SetTaskDoneAsync(string taskId, CancellationToken ct) + async Task IDiscoveryStore.SetTaskDoneAsync(string taskId, CancellationToken ct) { DiscTaskEntity? row = await _dbContext.DiscTasks .FirstOrDefaultAsync(task => task.Id == taskId, ct); @@ -149,7 +150,7 @@ public sealed partial class DiscoveryStore } /// - public async Task BumpTaskCounterAsync( + async Task IDiscoveryStore.BumpTaskCounterAsync( string taskId, DiscoveryCounterField field, int n, @@ -186,7 +187,7 @@ public sealed partial class DiscoveryStore } /// - public async Task AdvanceSearchAsync( + async Task IDiscoveryStore.AdvanceSearchAsync( string taskId, int nextIndex, bool searchDone, @@ -207,7 +208,7 @@ public sealed partial class DiscoveryStore } /// - public async Task SumActivePlanAsync(string? excludeTaskId, CancellationToken ct) + async Task IDiscoveryStore.SumActivePlanAsync(string? excludeTaskId, CancellationToken ct) { IQueryable query = _dbContext.DiscTasks .Where(task => task.Status != "done" && task.Status != "failed"); diff --git a/src/core/Deal.Infrastructure/Persistence/Repositories/KanbanStore.Cards.cs b/src/core/Deal.Infrastructure/Persistence/Repositories/KanbanStore.Cards.cs index 23d7ae5..d4a0c3d 100644 --- a/src/core/Deal.Infrastructure/Persistence/Repositories/KanbanStore.Cards.cs +++ b/src/core/Deal.Infrastructure/Persistence/Repositories/KanbanStore.Cards.cs @@ -2,6 +2,7 @@ using Deal.Infrastructure.Persistence.Entities; using Deal.Modules.Cards.Application.Sources; using Deal.Modules.Kanban.Application.Models; using Microsoft.EntityFrameworkCore; +using Deal.Modules.Kanban.Application.Abstractions; namespace Deal.Infrastructure.Persistence.Repositories; @@ -11,7 +12,7 @@ namespace Deal.Infrastructure.Persistence.Repositories; public sealed partial class KanbanStore { /// - public async Task> ListCardsAsync(CardsQuery query, CancellationToken ct) + async Task> ICardStore.ListCardsAsync(CardsQuery query, CancellationToken ct) { IQueryable queryable = _dbContext.Cards.AsNoTracking(); if (query.Col is null) @@ -30,7 +31,7 @@ public sealed partial class KanbanStore } /// - public async Task> SearchCardsAsync( + async Task> ICardStore.SearchCardsAsync( string q, int limit, CancellationToken ct) @@ -61,7 +62,7 @@ public sealed partial class KanbanStore } /// - public async Task GetCardAsync(string cardId, CancellationToken ct) + async Task ICardStore.GetCardAsync(string cardId, CancellationToken ct) { CardEntity? entity = await _dbContext.Cards .AsNoTracking() @@ -76,7 +77,7 @@ public sealed partial class KanbanStore } /// - public async Task GetCardBySourceAsync( + async Task ICardStore.GetCardBySourceAsync( SourceRef source, CancellationToken ct) { @@ -104,7 +105,7 @@ public sealed partial class KanbanStore } /// - public async Task AddCardAsync(CardSnapshot snapshot, CancellationToken ct) + async Task ICardStore.AddCardAsync(CardSnapshot snapshot, CancellationToken ct) { // CreatedAt проставляет хранилище (UTC-now) — в snapshot поля нет (см. CardSnapshot). _dbContext.Cards.Add(ToCardEntity(snapshot)); @@ -131,7 +132,7 @@ public sealed partial class KanbanStore } /// - public async Task UpdateColumnAsync(CardColumnUpdateDto update, CancellationToken ct) + async Task ICardStore.UpdateColumnAsync(CardColumnUpdateDto update, CancellationToken ct) { CardEntity? entity = await _dbContext.Cards.SingleOrDefaultAsync(card => card.Id == update.CardId, ct); if (entity is null) @@ -152,7 +153,7 @@ public sealed partial class KanbanStore } /// - public async Task ApplyReclassificationAsync(CardReclassificationDto update, CancellationToken ct) + async Task ICardStore.ApplyReclassificationAsync(CardReclassificationDto update, CancellationToken ct) { CardEntity? entity = await _dbContext.Cards.SingleOrDefaultAsync(card => card.Id == update.CardId, ct); if (entity is null) @@ -182,7 +183,7 @@ public sealed partial class KanbanStore } /// - public async Task UpdateSeenAsync( + async Task ICardStore.UpdateSeenAsync( string? cardId, string? col, CancellationToken ct) @@ -201,7 +202,7 @@ public sealed partial class KanbanStore } /// - public async Task DeleteForeverAsync(string cardId, CancellationToken ct) + async Task ICardStore.DeleteForeverAsync(string cardId, CancellationToken ct) { await using var transaction = await _dbContext.Database.BeginTransactionAsync(ct); await _dbContext.DedupEntries @@ -214,7 +215,7 @@ public sealed partial class KanbanStore } /// - public async Task ClearColAsync(string col, CancellationToken ct) + async Task ICardStore.ClearColAsync(string col, CancellationToken ct) { await using var transaction = await _dbContext.Database.BeginTransactionAsync(ct); await _dbContext.DedupEntries @@ -228,7 +229,7 @@ public sealed partial class KanbanStore } /// - public async Task> CountCardsByColAsync(CancellationToken ct) + async Task> ICardStore.CountCardsByColAsync(CancellationToken ct) { var rows = await _dbContext.Cards .AsNoTracking() diff --git a/src/core/Deal.Infrastructure/Persistence/Repositories/KanbanStore.Comments.cs b/src/core/Deal.Infrastructure/Persistence/Repositories/KanbanStore.Comments.cs index dfaf599..b2badf4 100644 --- a/src/core/Deal.Infrastructure/Persistence/Repositories/KanbanStore.Comments.cs +++ b/src/core/Deal.Infrastructure/Persistence/Repositories/KanbanStore.Comments.cs @@ -2,6 +2,7 @@ using Deal.Infrastructure.Persistence.Entities; using Deal.Modules.Cards.Application.Models; using Deal.Modules.Kanban.Application.Models; using Microsoft.EntityFrameworkCore; +using Deal.Modules.Kanban.Application.Abstractions; namespace Deal.Infrastructure.Persistence.Repositories; @@ -11,7 +12,7 @@ namespace Deal.Infrastructure.Persistence.Repositories; public sealed partial class KanbanStore { /// - public async Task> ListCommentsAsync(string cardId, CancellationToken ct) + async Task> ICardStore.ListCommentsAsync(string cardId, CancellationToken ct) { List entities = await _dbContext.LeadComments .AsNoTracking() @@ -23,7 +24,7 @@ public sealed partial class KanbanStore } /// - public async Task AddCommentAsync( + async Task ICardStore.AddCommentAsync( string commentId, string cardId, string by, @@ -42,7 +43,7 @@ public sealed partial class KanbanStore } /// - public async Task AddMoveAsync(CardMoveDto move, CancellationToken ct) + async Task ICardStore.AddMoveAsync(CardMoveDto move, CancellationToken ct) { _dbContext.CardMoves.Add(new CardMoveEntity { @@ -60,7 +61,7 @@ public sealed partial class KanbanStore public Task CountMovesAsync(CancellationToken ct) => _dbContext.CardMoves.CountAsync(ct); /// - public async Task> GetAiMarkupExamplesAsync(int limit, CancellationToken ct) + async Task> ICardStore.GetAiMarkupExamplesAsync(int limit, CancellationToken ct) { return await _dbContext.CardMoves .AsNoTracking() diff --git a/src/core/Deal.Infrastructure/Persistence/Repositories/KanbanStore.Containers.cs b/src/core/Deal.Infrastructure/Persistence/Repositories/KanbanStore.Containers.cs index bf35d06..66bbf81 100644 --- a/src/core/Deal.Infrastructure/Persistence/Repositories/KanbanStore.Containers.cs +++ b/src/core/Deal.Infrastructure/Persistence/Repositories/KanbanStore.Containers.cs @@ -2,6 +2,7 @@ using Deal.Infrastructure.Persistence.Entities; using Deal.Modules.Cards.Application.Models; using Deal.Modules.Kanban.Application.Models; using Microsoft.EntityFrameworkCore; +using Deal.Modules.Kanban.Application.Abstractions; namespace Deal.Infrastructure.Persistence.Repositories; @@ -11,7 +12,7 @@ namespace Deal.Infrastructure.Persistence.Repositories; public sealed partial class KanbanStore { /// - public async Task> ListContainersAsync(string? space, CancellationToken ct) + async Task> ICardStore.ListContainersAsync(string? space, CancellationToken ct) { IQueryable queryable = _dbContext.Containers.AsNoTracking(); if (space is not null) @@ -28,7 +29,7 @@ public sealed partial class KanbanStore } /// - public async Task GetContainerAsync(string containerId, CancellationToken ct) + async Task ICardStore.GetContainerAsync(string containerId, CancellationToken ct) { ContainerEntity? entity = await _dbContext.Containers .AsNoTracking() @@ -37,14 +38,14 @@ public sealed partial class KanbanStore } /// - public async Task CreateContainerAsync(ContainerDto container, CancellationToken ct) + async Task ICardStore.CreateContainerAsync(ContainerDto container, CancellationToken ct) { _dbContext.Containers.Add(ToContainerEntity(container)); await _dbContext.SaveChangesAsync(ct); } /// - public async Task UpdateContainerAsync(ContainerDto container, CancellationToken ct) + async Task ICardStore.UpdateContainerAsync(ContainerDto container, CancellationToken ct) { // Полное обновление строки (сервис читает Get + применяет ContainerPatchDto): JSON-поля пишутся // целиком, CreatedAt не трогаем — одним UPDATE. @@ -64,7 +65,7 @@ public sealed partial class KanbanStore } /// - public async Task DeleteContainerAsync(string containerId, CancellationToken ct) + async Task ICardStore.DeleteContainerAsync(string containerId, CancellationToken ct) { // Два изменения разных таблиц — в одной транзакции: либо карточки ушли в «Неразобранное» и контейнер // удалён, либо ничего не изменилось. @@ -84,7 +85,7 @@ public sealed partial class KanbanStore } /// - public async Task ReorderContainersAsync( + async Task ICardStore.ReorderContainersAsync( string space, IReadOnlyList containerIds, CancellationToken ct) diff --git a/src/core/Deal.Infrastructure/Persistence/Repositories/KanbanStore.Selected.cs b/src/core/Deal.Infrastructure/Persistence/Repositories/KanbanStore.Selected.cs index 688bf36..7a0d446 100644 --- a/src/core/Deal.Infrastructure/Persistence/Repositories/KanbanStore.Selected.cs +++ b/src/core/Deal.Infrastructure/Persistence/Repositories/KanbanStore.Selected.cs @@ -1,6 +1,7 @@ using Deal.Infrastructure.Persistence.Entities; using Deal.Modules.Kanban.Application.Models; using Microsoft.EntityFrameworkCore; +using Deal.Modules.Kanban.Application.Abstractions; namespace Deal.Infrastructure.Persistence.Repositories; @@ -10,7 +11,7 @@ namespace Deal.Infrastructure.Persistence.Repositories; public sealed partial class KanbanStore { /// - public async Task> ListSelectedCardsAsync(string? containerId, CancellationToken ct) + async Task> ICardStore.ListSelectedCardsAsync(string? containerId, CancellationToken ct) { IQueryable queryable = _dbContext.Cards.AsNoTracking(); if (containerId is not null) @@ -29,7 +30,7 @@ public sealed partial class KanbanStore } /// - public async Task PatchCardAsync( + async Task ICardStore.PatchCardAsync( string cardId, CardPatch patch, CancellationToken ct) @@ -68,7 +69,7 @@ public sealed partial class KanbanStore } /// - public async Task AddLinkAsync( + async Task ICardStore.AddLinkAsync( string cardId, CardLinkDto link, CancellationToken ct) @@ -85,7 +86,7 @@ public sealed partial class KanbanStore } /// - public async Task RemoveLinkAsync( + async Task ICardStore.RemoveLinkAsync( string cardId, string linkId, CancellationToken ct) @@ -106,7 +107,7 @@ public sealed partial class KanbanStore } /// - public async Task AddFileAsync( + async Task ICardStore.AddFileAsync( string cardId, CardFileDto file, CancellationToken ct) @@ -123,7 +124,7 @@ public sealed partial class KanbanStore } /// - public async Task RemoveFileAsync( + async Task ICardStore.RemoveFileAsync( string cardId, string fileId, CancellationToken ct) @@ -144,7 +145,7 @@ public sealed partial class KanbanStore } /// - public async Task MoveCardStageAsync( + async Task ICardStore.MoveCardStageAsync( string cardId, string containerId, CardHistoryDto historyEntry, @@ -176,7 +177,7 @@ public sealed partial class KanbanStore } /// - public async Task SetReminderAsync( + async Task ICardStore.SetReminderAsync( string cardId, long atMs, CancellationToken ct) @@ -191,7 +192,7 @@ public sealed partial class KanbanStore } /// - public async Task ClearReminderAsync(string cardId, CancellationToken ct) + async Task ICardStore.ClearReminderAsync(string cardId, CancellationToken ct) { await _dbContext.Cards .Where(card => card.Id == cardId) @@ -202,7 +203,7 @@ public sealed partial class KanbanStore } /// - public async Task ClearStageAsync(string containerId, CancellationToken ct) + async Task ICardStore.ClearStageAsync(string containerId, CancellationToken ct) { return await _dbContext.Cards .Where(card => card.Col == containerId) @@ -210,7 +211,7 @@ public sealed partial class KanbanStore } /// - public async Task> ListDueRemindersAsync(DateTimeOffset now, CancellationToken ct) + async Task> ICardStore.ListDueRemindersAsync(DateTimeOffset now, CancellationToken ct) { return await _dbContext.Cards .AsNoTracking() @@ -224,7 +225,7 @@ public sealed partial class KanbanStore } /// - public async Task MarkRemindersFiredAsync(IReadOnlyList cardIds, CancellationToken ct) + async Task ICardStore.MarkRemindersFiredAsync(IReadOnlyList cardIds, CancellationToken ct) { if (cardIds.Count == 0) { @@ -237,7 +238,7 @@ public sealed partial class KanbanStore } /// - public async Task ClearExpiredRemindersAsync(DateTimeOffset now, CancellationToken ct) + async Task ICardStore.ClearExpiredRemindersAsync(DateTimeOffset now, CancellationToken ct) { return await _dbContext.Cards .Where(card => card.ReminderAt != null && card.ReminderAt <= now) diff --git a/src/core/Deal.Infrastructure/Persistence/Repositories/KanbanStore.StorageRules.cs b/src/core/Deal.Infrastructure/Persistence/Repositories/KanbanStore.StorageRules.cs index 616b47a..2835a39 100644 --- a/src/core/Deal.Infrastructure/Persistence/Repositories/KanbanStore.StorageRules.cs +++ b/src/core/Deal.Infrastructure/Persistence/Repositories/KanbanStore.StorageRules.cs @@ -2,6 +2,7 @@ using Deal.Infrastructure.Persistence.Entities; using Deal.Modules.Cards.Application.Models; using Deal.Modules.Kanban.Application.Models; using Microsoft.EntityFrameworkCore; +using Deal.Modules.Kanban.Application.Abstractions; namespace Deal.Infrastructure.Persistence.Repositories; @@ -11,7 +12,7 @@ namespace Deal.Infrastructure.Persistence.Repositories; public sealed partial class KanbanStore { /// - public async Task> ListArchiveCandidatesAsync(DateTimeOffset receivedBeforeUtc, CancellationToken ct) + async Task> ICardStore.ListArchiveCandidatesAsync(DateTimeOffset receivedBeforeUtc, CancellationToken ct) { List boardIds = await _dbContext.Containers .AsNoTracking() @@ -28,7 +29,7 @@ public sealed partial class KanbanStore } /// - public async Task ArchiveAsync( + async Task ICardStore.ArchiveAsync( IReadOnlyList cardIds, DateTimeOffset archivedAt, CancellationToken ct) @@ -49,7 +50,7 @@ public sealed partial class KanbanStore } /// - public async Task> ListExpiredArchiveCandidatesAsync(DateTimeOffset archivedBeforeUtc, CancellationToken ct) + async Task> ICardStore.ListExpiredArchiveCandidatesAsync(DateTimeOffset archivedBeforeUtc, CancellationToken ct) { return await _dbContext.Cards .AsNoTracking() @@ -61,7 +62,7 @@ public sealed partial class KanbanStore } /// - public async Task> ListTrashCandidatesAsync(DateTimeOffset receivedBeforeUtc, CancellationToken ct) + async Task> ICardStore.ListTrashCandidatesAsync(DateTimeOffset receivedBeforeUtc, CancellationToken ct) { return await _dbContext.Cards .AsNoTracking() @@ -71,7 +72,7 @@ public sealed partial class KanbanStore } /// - public async Task PurgeAsync(IReadOnlyList cardIds, CancellationToken ct) + async Task ICardStore.PurgeAsync(IReadOnlyList cardIds, CancellationToken ct) { if (cardIds.Count == 0) { @@ -90,7 +91,7 @@ public sealed partial class KanbanStore } /// - public async Task> ListCardsForConversionAsync(CancellationToken ct) + async Task> ICardStore.ListCardsForConversionAsync(CancellationToken ct) { List entities = await _dbContext.Cards .AsNoTracking() @@ -102,7 +103,7 @@ public sealed partial class KanbanStore } /// - public async Task UpdateConversionAsync( + async Task ICardStore.UpdateConversionAsync( string cardId, double? convFrom, double? convTo, @@ -119,7 +120,7 @@ public sealed partial class KanbanStore } /// - public async Task> ListInboxWithSourceAsync(CancellationToken ct) + async Task> ICardStore.ListInboxWithSourceAsync(CancellationToken ct) { List entities = await _dbContext.Cards .AsNoTracking() diff --git a/src/core/Deal.Infrastructure/Persistence/Repositories/TenantLimitStore.cs b/src/core/Deal.Infrastructure/Persistence/Repositories/TenantLimitStore.cs index ec32257..368b0e4 100644 --- a/src/core/Deal.Infrastructure/Persistence/Repositories/TenantLimitStore.cs +++ b/src/core/Deal.Infrastructure/Persistence/Repositories/TenantLimitStore.cs @@ -59,17 +59,17 @@ public sealed class TenantLimitStore : ITenantLimitStore } /// - public async Task GetOrCreateAsync( + async Task ITenantLimitStore.GetOrCreateAsync( Guid tenantId, CancellationToken ct, - TokenLimitDefaults? defaults = null) + TokenLimitDefaults? defaults) { TenantLimitEntity entity = await LoadOrCreateAsync(tenantId, defaults ?? _defaults, ct); return ToLimitDto(entity); } /// - public async Task GetStateAsync(Guid tenantId, CancellationToken ct) + async Task ITenantLimitStore.GetStateAsync(Guid tenantId, CancellationToken ct) { TenantLimitEntity entity = await LoadOrCreateAsync(tenantId, _defaults, ct); await ResetIfPeriodExpiredAsync(entity, ct); @@ -77,7 +77,7 @@ public sealed class TenantLimitStore : ITenantLimitStore } /// - public async Task AddUsageAsync( + async Task ITenantLimitStore.AddUsageAsync( Guid tenantId, long tokens, CancellationToken ct) @@ -110,7 +110,7 @@ public sealed class TenantLimitStore : ITenantLimitStore } /// - public async Task UpdateBudgetAsync( + async Task ITenantLimitStore.UpdateBudgetAsync( Guid tenantId, long budgetTokens, string period, @@ -133,7 +133,7 @@ public sealed class TenantLimitStore : ITenantLimitStore } /// - public async Task TryMarkWarnedAsync(Guid tenantId, CancellationToken ct) + async Task ITenantLimitStore.TryMarkWarnedAsync(Guid tenantId, CancellationToken ct) { TenantLimitEntity entity = await LoadOrCreateAsync(tenantId, _defaults, ct); await ResetIfPeriodExpiredAsync(entity, ct); @@ -149,7 +149,7 @@ public sealed class TenantLimitStore : ITenantLimitStore } /// - public async Task TryMarkNotifiedExhaustedAsync(Guid tenantId, CancellationToken ct) + async Task ITenantLimitStore.TryMarkNotifiedExhaustedAsync(Guid tenantId, CancellationToken ct) { TenantLimitEntity entity = await LoadOrCreateAsync(tenantId, _defaults, ct); await ResetIfPeriodExpiredAsync(entity, ct); @@ -220,7 +220,7 @@ public sealed class TenantLimitStore : ITenantLimitStore } /// - public async Task ResetExpiredPeriodsAsync(DateTimeOffset now, CancellationToken ct) + async Task ITenantLimitStore.ResetExpiredPeriodsAsync(DateTimeOffset now, CancellationToken ct) { // Трогаем только строки с накоплениями (расход/флаги): строки без накоплений чистить нечего. List candidates = await _dbContext.TenantLimits diff --git a/src/core/Deal.Infrastructure/Security/AesGcmSecretCipher.cs b/src/core/Deal.Infrastructure/Security/AesGcmSecretCipher.cs index 176032b..979b214 100644 --- a/src/core/Deal.Infrastructure/Security/AesGcmSecretCipher.cs +++ b/src/core/Deal.Infrastructure/Security/AesGcmSecretCipher.cs @@ -40,7 +40,7 @@ public sealed class AesGcmSecretCipher : ISecretCipher } /// - public string Encrypt(string plainText) + string ISecretCipher.Encrypt(string plainText) { if (string.IsNullOrEmpty(plainText)) { @@ -66,7 +66,7 @@ public sealed class AesGcmSecretCipher : ISecretCipher } /// - public string Decrypt(string cipherText) + string ISecretCipher.Decrypt(string cipherText) { if (string.IsNullOrEmpty(cipherText) || !cipherText.StartsWith(EncryptedPrefix, StringComparison.Ordinal)) { diff --git a/src/core/Deal.Modules.Discovery/Application/Services/DiscoverySearchErrorCounter.cs b/src/core/Deal.Modules.Discovery/Application/Services/DiscoverySearchErrorCounter.cs index 233fa62..98d1b42 100644 --- a/src/core/Deal.Modules.Discovery/Application/Services/DiscoverySearchErrorCounter.cs +++ b/src/core/Deal.Modules.Discovery/Application/Services/DiscoverySearchErrorCounter.cs @@ -44,7 +44,7 @@ public sealed class DiscoverySearchErrorCounter : IDiscoverySearchErrorCounter } /// - public int Next(string taskId) + int IDiscoverySearchErrorCounter.Next(string taskId) { EvictExpired(); Entry fresh = _failures.AddOrUpdate( @@ -56,7 +56,7 @@ public sealed class DiscoverySearchErrorCounter : IDiscoverySearchErrorCounter } /// - public void Reset(string taskId) + void IDiscoverySearchErrorCounter.Reset(string taskId) { EvictExpired(); _failures.TryRemove(taskId, out _); diff --git a/src/core/tests/Deal.Tests.Unit/Contracts/BudgetedAiClassifierTests.cs b/src/core/tests/Deal.Tests.Unit/Contracts/BudgetedAiClassifierTests.cs index 9b6ca3a..cb302e3 100644 --- a/src/core/tests/Deal.Tests.Unit/Contracts/BudgetedAiClassifierTests.cs +++ b/src/core/tests/Deal.Tests.Unit/Contracts/BudgetedAiClassifierTests.cs @@ -7,6 +7,8 @@ using Deal.SharedKernel.Tenants.Models; using Deal.Tests.Unit.Modules.Settings; using Deal.Tests.Unit.Modules.Tenants; using Microsoft.Extensions.Logging.Abstractions; +using Deal.Contracts.Integrations.Abstractions; +using Deal.SharedKernel.Tenants.Abstractions; namespace Deal.Tests.Unit.Contracts; @@ -147,7 +149,7 @@ public sealed class BudgetedAiClassifierTests // Paid: Платный фейк-исполнитель (счётчики/ответы сценария). // Settings: KV-настройки тенанта (маркеры LocalFieldsParser). private sealed record Context( - BudgetedAiClassifier Decorator, + IAiClassifier Decorator, FakeAiClassifier Paid, FakeSettingsStore Settings); @@ -160,10 +162,10 @@ public sealed class BudgetedAiClassifierTests var settings = new FakeSettingsStore(); var limits = new FakeTenantLimitStore(); configure?.Invoke(limits); - var tenantContext = new TenantContext(); + ITenantContext tenantContext = new TenantContext(); tenantContext.SetTenant(new TenantId(TenantIdValue)); var paid = new FakeAiClassifier(); - var decorator = new BudgetedAiClassifier( + IAiClassifier decorator = new BudgetedAiClassifier( paid, new LocalAiClassifier(new LocalFieldsParser(settings)), limits, diff --git a/src/core/tests/Deal.Tests.Unit/Contracts/BudgetedAiToolsTests.cs b/src/core/tests/Deal.Tests.Unit/Contracts/BudgetedAiToolsTests.cs index f510f46..772bc70 100644 --- a/src/core/tests/Deal.Tests.Unit/Contracts/BudgetedAiToolsTests.cs +++ b/src/core/tests/Deal.Tests.Unit/Contracts/BudgetedAiToolsTests.cs @@ -6,6 +6,8 @@ using Deal.Modules.Tenants.Application.Models; using Deal.SharedKernel.Tenants.Models; using Deal.Tests.Unit.Modules.Tenants; using Microsoft.Extensions.Logging.Abstractions; +using Deal.Contracts.Integrations.Abstractions; +using Deal.SharedKernel.Tenants.Abstractions; namespace Deal.Tests.Unit.Contracts; @@ -128,7 +130,7 @@ public sealed class BudgetedAiToolsTests // Decorator: Декоратор бюджетного гейта. // Paid: Платный фейк-исполнитель (счётчик/ответы сценария). private sealed record Context( - BudgetedAiTools Decorator, + IAiTools Decorator, FakeAiTools Paid); // Собирает контекст: платный фейк + фейк лимитов и tenant-контекст (как регистрирует @@ -139,10 +141,10 @@ public sealed class BudgetedAiToolsTests { var limits = new FakeTenantLimitStore(); configure?.Invoke(limits); - var tenantContext = new TenantContext(); + ITenantContext tenantContext = new TenantContext(); tenantContext.SetTenant(new TenantId(TenantIdValue)); var paid = new FakeAiTools(); - var decorator = new BudgetedAiTools( + IAiTools decorator = new BudgetedAiTools( paid, limits, tenantContext, diff --git a/src/core/tests/Deal.Tests.Unit/Contracts/GrpcAiToolsTests.cs b/src/core/tests/Deal.Tests.Unit/Contracts/GrpcAiToolsTests.cs index 5962a89..2f79129 100644 --- a/src/core/tests/Deal.Tests.Unit/Contracts/GrpcAiToolsTests.cs +++ b/src/core/tests/Deal.Tests.Unit/Contracts/GrpcAiToolsTests.cs @@ -13,6 +13,8 @@ using Deal.Tests.Unit.Modules.Settings; using Deal.Tests.Unit.Modules.Tenants; using Deal.Tests.Unit.Support; using Microsoft.Extensions.Logging.Abstractions; +using Deal.Contracts.Integrations.Abstractions; +using Deal.SharedKernel.Tenants.Abstractions; namespace Deal.Tests.Unit.Contracts; @@ -40,7 +42,7 @@ public sealed class GrpcAiToolsTests FakeSecretCipher cipher = new(); FakeTenantLimitStore limits = new(); - GrpcAiTools tools = CreateTools(port, settings, cipher, limits); + IAiTools tools = CreateTools(port, settings, cipher, limits); AiGenerateKeywordsResultDto result = await tools.GenerateKeywordsAsync("Бэкенд-разработка на Python", CancellationToken.None); Assert.True(result.Ok); @@ -63,7 +65,7 @@ public sealed class GrpcAiToolsTests await AiGrpcTestHost.RunAsync(AiGrpcTestHost.DefaultToken, new RecordingAiService(), async (port, service) => { service.KeywordsUnavailable = true; - GrpcAiTools tools = CreateTools(port, new FakeSettingsStore(), new FakeSecretCipher()); + IAiTools tools = CreateTools(port, new FakeSettingsStore(), new FakeSecretCipher()); AiGenerateKeywordsResultDto result = await tools.GenerateKeywordsAsync("описание", CancellationToken.None); @@ -86,7 +88,7 @@ public sealed class GrpcAiToolsTests Usage = new Usage { Prompt = 200, Completion = 10, Total = 210 }, }; FakeTenantLimitStore limits = new(); - GrpcAiTools tools = CreateTools(port, new FakeSettingsStore(), new FakeSecretCipher(), limits); + IAiTools tools = CreateTools(port, new FakeSettingsStore(), new FakeSecretCipher(), limits); AiEvaluateFitResultDto result = await tools.EvaluateFitAsync( "Ищу дизайнера для лендинга", "Разработка сайтов на Python", new[] { "python", "бэкенд" }, CancellationToken.None); @@ -110,7 +112,7 @@ public sealed class GrpcAiToolsTests await AiGrpcTestHost.RunAsync(AiGrpcTestHost.DefaultToken, new RecordingAiService(), async (port, service) => { service.FitUnavailable = true; - GrpcAiTools tools = CreateTools(port, new FakeSettingsStore(), new FakeSecretCipher()); + IAiTools tools = CreateTools(port, new FakeSettingsStore(), new FakeSecretCipher()); await Assert.ThrowsAsync( () => tools.EvaluateFitAsync("текст", "описание", new[] { "ключ" }, CancellationToken.None)); @@ -129,7 +131,7 @@ public sealed class GrpcAiToolsTests FakeSecretCipher cipher, FakeTenantLimitStore? limits = null) { - var tenantContext = new TenantContext(); + ITenantContext tenantContext = new TenantContext(); tenantContext.SetTenant(new TenantId(TenantIdValue)); var connection = new AiGrpcConnection(new AiServiceOptions { UseLocal = false, Endpoint = $"http://127.0.0.1:{port}" }); return new GrpcAiTools( diff --git a/src/core/tests/Deal.Tests.Unit/Contracts/GrpcMlClientTests.cs b/src/core/tests/Deal.Tests.Unit/Contracts/GrpcMlClientTests.cs index 0f831d7..af33bce 100644 --- a/src/core/tests/Deal.Tests.Unit/Contracts/GrpcMlClientTests.cs +++ b/src/core/tests/Deal.Tests.Unit/Contracts/GrpcMlClientTests.cs @@ -15,6 +15,8 @@ using Deal.Tests.Unit.Modules.Settings; using Deal.Tests.Unit.Modules.Tenants; using Deal.Tests.Unit.Support; using Microsoft.Extensions.Logging.Abstractions; +using Deal.Contracts.Integrations.Abstractions; +using Deal.SharedKernel.Tenants.Abstractions; namespace Deal.Tests.Unit.Contracts; @@ -49,7 +51,7 @@ public sealed class GrpcMlClientTests Type = new TypeDecision { Take = true, Label = "hire", Value = "t:hire", Margin = 0.5 }, }; - GrpcMlClient client = CreateClient(port); + IMlClient client = CreateClient(port); MlPredictResultDto result = await client.PredictAsync("нужен middle python разработчик", CancellationToken.None); Assert.True(result.Take); @@ -78,7 +80,7 @@ public sealed class GrpcMlClientTests { service.PredictUnavailable = true; - GrpcMlClient client = CreateClient(port); + IMlClient client = CreateClient(port); MlPredictResultDto result = await client.PredictAsync("текст", CancellationToken.None); Assert.False(result.Take); @@ -113,7 +115,7 @@ public sealed class GrpcMlClientTests settings.Preload(SettingsKeys.AiDecisions, "3"); var learning = new FakeMlLearningStore { LearningCount = 9 }; - GrpcMlClient client = CreateClient(port, settings, learning); + IMlClient client = CreateClient(port, settings, learning); MlStatusResponseDto status = await client.StatusAsync(CancellationToken.None); Assert.True(status.Enabled); // mlEnabled не задан — дефолт true @@ -139,7 +141,7 @@ public sealed class GrpcMlClientTests { DateTimeOffset now = DateTimeOffset.UtcNow; var cache = new MlStatusCache(() => now); - GrpcMlClient client = CreateClient(port, cache: cache); + IMlClient client = CreateClient(port, cache: cache); service.StatusUnavailable = true; MlStatusResponseDto down = await client.StatusAsync(CancellationToken.None); @@ -173,7 +175,7 @@ public sealed class GrpcMlClientTests await MlGrpcTestHost.RunAsync(MlGrpcTestHost.DefaultToken, new RecordingMlService(), async (port, service) => { DateTimeOffset now = DateTimeOffset.UtcNow; - GrpcMlClient client = CreateClient(port, cache: new MlStatusCache(() => now)); + IMlClient client = CreateClient(port, cache: new MlStatusCache(() => now)); _ = await client.StatusAsync(CancellationToken.None); now = now.AddSeconds(5); // в пределах TTL 15 с — повторный вызов не ходит в сервис @@ -196,7 +198,7 @@ public sealed class GrpcMlClientTests var learning = new FakeMlLearningStore(); learning.SeedOutbox("mle_1", "текст 1", "b_a", 1.0); learning.SeedOutbox("mle_2", "текст 2", "spam", 1.0); - GrpcMlClient client = CreateClient(port, learning: learning); + IMlClient client = CreateClient(port, learning: learning); // Прогреть кэш статуса (до сброса — 1 вызов Status), затем сброс. _ = await client.StatusAsync(CancellationToken.None); @@ -221,7 +223,7 @@ public sealed class GrpcMlClientTests var learning = new FakeMlLearningStore(); learning.SeedOutbox("mle_1", "текст", "b_a", 1.0); - GrpcMlClient client = CreateClient(port, learning: learning); + IMlClient client = CreateClient(port, learning: learning); MlResetResultDto reset = await client.ResetAsync(CancellationToken.None); Assert.False(reset.Ok); @@ -239,7 +241,7 @@ public sealed class GrpcMlClientTests var learning = new FakeMlLearningStore(); learning.SeedOutbox("mle_1", "текст", "spam", 1.0); - GrpcMlClient client = CreateClient(port, learning: learning); + IMlClient client = CreateClient(port, learning: learning); MlResetResultDto reset = await client.ResetAsync(CancellationToken.None); Assert.False(reset.Ok); @@ -255,7 +257,7 @@ public sealed class GrpcMlClientTests await MlGrpcTestHost.RunAsync(MlGrpcTestHost.DefaultToken, new RecordingMlService(), async (port, service) => { var learning = new FakeMlLearningStore(); - GrpcMlClient client = CreateClient(port, learning: learning); + IMlClient client = CreateClient(port, learning: learning); await client.PushAsync(" нужен python ", "b_junior", 1.0, CancellationToken.None); @@ -275,7 +277,7 @@ public sealed class GrpcMlClientTests { await MlGrpcTestHost.RunAsync(MlGrpcTestHost.DefaultToken, new RecordingMlService(), async (port, service) => { - GrpcMlClient client = CreateClient(port); + IMlClient client = CreateClient(port); var items = new[] { new MlOutboxEntryDto("mle_1", "текст 1", "b_a", 1.0), @@ -311,7 +313,7 @@ public sealed class GrpcMlClientTests FakeMlLearningStore? learning = null, MlStatusCache? cache = null) { - var tenantContext = new TenantContext(); + ITenantContext tenantContext = new TenantContext(); tenantContext.SetTenant(new TenantId(TenantIdValue)); var options = new MlServiceOptions { UseLocal = false, Endpoint = $"http://127.0.0.1:{port}" }; return new GrpcMlClient( diff --git a/src/core/tests/Deal.Tests.Unit/Contracts/GrpcTelegramClientTests.cs b/src/core/tests/Deal.Tests.Unit/Contracts/GrpcTelegramClientTests.cs index da9f44e..7db97df 100644 --- a/src/core/tests/Deal.Tests.Unit/Contracts/GrpcTelegramClientTests.cs +++ b/src/core/tests/Deal.Tests.Unit/Contracts/GrpcTelegramClientTests.cs @@ -10,6 +10,7 @@ using Deal.Tests.Unit.Grpc; using Deal.Tests.Unit.Support; using Grpc.Core; using Microsoft.Extensions.Logging.Abstractions; +using Deal.SharedKernel.Tenants.Abstractions; namespace Deal.Tests.Unit.Contracts; @@ -235,7 +236,7 @@ public sealed class GrpcTelegramClientTests }); using (connection) { - var gateway = new GrpcTelegramClient( + ITelegramGateway gateway = new GrpcTelegramClient( new TenantContext(), connection, NullLogger.Instance); await Assert.ThrowsAsync(() => gateway.StatusAsync(CancellationToken.None)); } @@ -247,7 +248,7 @@ public sealed class GrpcTelegramClientTests // Возвращает: Гейт (транспорт живёт на время сценария). private static GrpcTelegramClient CreateClient(int port) { - var tenantContext = new TenantContext(); + ITenantContext tenantContext = new TenantContext(); tenantContext.SetTenant(new TenantId(Guid.NewGuid().ToString("N"))); var connection = new TelegramGrpcConnection(new TelegramServiceOptions { diff --git a/src/core/tests/Deal.Tests.Unit/Contracts/IntegrationsDiTests.cs b/src/core/tests/Deal.Tests.Unit/Contracts/IntegrationsDiTests.cs index f1f1919..caf5547 100644 --- a/src/core/tests/Deal.Tests.Unit/Contracts/IntegrationsDiTests.cs +++ b/src/core/tests/Deal.Tests.Unit/Contracts/IntegrationsDiTests.cs @@ -141,7 +141,7 @@ public sealed class IntegrationsDiTests TelegramServiceOptions telegramOptions) { var services = new ServiceCollection(); - var tenantContext = new TenantContext(); + ITenantContext tenantContext = new TenantContext(); tenantContext.SetTenant(new TenantId(Guid.NewGuid().ToString("N"))); services.AddSingleton(tenantContext); services.AddScoped(_ => new FakeSettingsStore()); diff --git a/src/core/tests/Deal.Tests.Unit/Contracts/LocalAiToolsTests.cs b/src/core/tests/Deal.Tests.Unit/Contracts/LocalAiToolsTests.cs index 9928e86..1779907 100644 --- a/src/core/tests/Deal.Tests.Unit/Contracts/LocalAiToolsTests.cs +++ b/src/core/tests/Deal.Tests.Unit/Contracts/LocalAiToolsTests.cs @@ -1,4 +1,5 @@ using Deal.Infrastructure.Integrations.Services; +using Deal.Contracts.Integrations.Abstractions; namespace Deal.Tests.Unit.Contracts; @@ -7,7 +8,7 @@ namespace Deal.Tests.Unit.Contracts; /// public sealed class LocalAiToolsTests { - private readonly LocalAiTools _tools = new(); + private readonly IAiTools _tools = new LocalAiTools(); [Fact] public async Task GenerateKeywordsAsync_AlwaysThrowsNotSupported() diff --git a/src/core/tests/Deal.Tests.Unit/Contracts/PipelineWorkerGrpcAiTests.cs b/src/core/tests/Deal.Tests.Unit/Contracts/PipelineWorkerGrpcAiTests.cs index 2d7463c..4e6720a 100644 --- a/src/core/tests/Deal.Tests.Unit/Contracts/PipelineWorkerGrpcAiTests.cs +++ b/src/core/tests/Deal.Tests.Unit/Contracts/PipelineWorkerGrpcAiTests.cs @@ -20,6 +20,7 @@ using Deal.Tests.Unit.Modules.Settings; using Deal.Tests.Unit.Modules.Tenants; using Deal.Tests.Unit.Support; using Microsoft.Extensions.Logging.Abstractions; +using Deal.SharedKernel.Tenants.Abstractions; namespace Deal.Tests.Unit.Contracts; @@ -169,7 +170,7 @@ public sealed class PipelineWorkerGrpcAiTests var rules = new IncomingRules(settings); var fieldsParser = new LocalFieldsParser(settings); - var tenantContext = new TenantContext(); + ITenantContext tenantContext = new TenantContext(); tenantContext.SetTenant(new TenantId(TenantIdValue)); var connection = new AiGrpcConnection( new AiServiceOptions { UseLocal = false, Endpoint = $"http://127.0.0.1:{port}" }); diff --git a/src/core/tests/Deal.Tests.Unit/Infrastructure/TenantLimitStoreTests.cs b/src/core/tests/Deal.Tests.Unit/Infrastructure/TenantLimitStoreTests.cs index bab169f..30e88bf 100644 --- a/src/core/tests/Deal.Tests.Unit/Infrastructure/TenantLimitStoreTests.cs +++ b/src/core/tests/Deal.Tests.Unit/Infrastructure/TenantLimitStoreTests.cs @@ -4,6 +4,7 @@ using Deal.Infrastructure.Persistence.Repositories; using Deal.Modules.Tenants.Application.Models; using Deal.Modules.Tenants.Application.Services; using Microsoft.EntityFrameworkCore; +using Deal.Modules.Tenants.Application.Abstractions; namespace Deal.Tests.Unit.Infrastructure; @@ -18,7 +19,7 @@ public sealed class TenantLimitStoreTests [Fact] public async Task GetOrCreateAsync_NoRow_CreatesDefaultBudget() { - (DealDbContext db, TenantLimitStore store) = CreateStore(); + (DealDbContext db, ITenantLimitStore store) = CreateStore(); var tenantId = Guid.NewGuid(); AddTenant(db, tenantId, TenantStatuses.Active); @@ -39,7 +40,7 @@ public sealed class TenantLimitStoreTests [Fact] public async Task GetOrCreateAsync_WithExplicitDefaults_UsesThem() { - (DealDbContext db, TenantLimitStore store) = CreateStore(); + (DealDbContext db, ITenantLimitStore store) = CreateStore(); var tenantId = Guid.NewGuid(); AddTenant(db, tenantId, TenantStatuses.Active); @@ -54,7 +55,7 @@ public sealed class TenantLimitStoreTests public async Task GetStateAsync_ReturnsStatusAndAllowed() { var tenantId = Guid.NewGuid(); - (DealDbContext db, TenantLimitStore store) = CreateStore(); + (DealDbContext db, ITenantLimitStore store) = CreateStore(); // Активный тенант, расход ниже бюджета → Allowed=true. AddTenant(db, tenantId, TenantStatuses.Active); @@ -75,7 +76,7 @@ public sealed class TenantLimitStoreTests public async Task AddUsageAsync_WhenPeriodExpired_ResetsUsedAndStartsNewPeriod() { var tenantId = Guid.NewGuid(); - (DealDbContext db, TenantLimitStore store) = CreateStore(clock: () => Now); + (DealDbContext db, ITenantLimitStore store) = CreateStore(clock: () => Now); AddTenant(db, tenantId, TenantStatuses.Active); AddLimit(db, tenantId, budgetTokens: 1000, TenantLimitPeriods.Month, periodStart: Now.AddMonths(-2), usedTokens: 700, warned80: true, notifiedExhausted: true); @@ -93,7 +94,7 @@ public sealed class TenantLimitStoreTests public async Task AddUsageAsync_WithinPeriod_Accumulates() { var tenantId = Guid.NewGuid(); - (DealDbContext db, TenantLimitStore store) = CreateStore(clock: () => Now); + (DealDbContext db, ITenantLimitStore store) = CreateStore(clock: () => Now); AddTenant(db, tenantId, TenantStatuses.Active); AddLimit(db, tenantId, budgetTokens: 1000, TenantLimitPeriods.Month, Now, usedTokens: 300); @@ -107,7 +108,7 @@ public sealed class TenantLimitStoreTests public async Task AddUsageAsync_Crossing80Percent_DoesNotSetWarned80() { var tenantId = Guid.NewGuid(); - (DealDbContext db, TenantLimitStore store) = CreateStore(clock: () => Now); + (DealDbContext db, ITenantLimitStore store) = CreateStore(clock: () => Now); AddTenant(db, tenantId, TenantStatuses.Active); AddLimit(db, tenantId, budgetTokens: 1000, TenantLimitPeriods.Month, Now, usedTokens: 700); @@ -123,7 +124,7 @@ public sealed class TenantLimitStoreTests public async Task AddUsageAsync_Exhaustion_DoesNotSetFlagsButDisallows() { var tenantId = Guid.NewGuid(); - (DealDbContext db, TenantLimitStore store) = CreateStore(clock: () => Now); + (DealDbContext db, ITenantLimitStore store) = CreateStore(clock: () => Now); AddTenant(db, tenantId, TenantStatuses.Active); AddLimit(db, tenantId, budgetTokens: 1000, TenantLimitPeriods.Month, Now, usedTokens: 950); @@ -140,7 +141,7 @@ public sealed class TenantLimitStoreTests { // Нулевой расход не инкрементирует, но строка лимита заводится лениво (закрыт путь записи). var tenantId = Guid.NewGuid(); - (DealDbContext db, TenantLimitStore store) = CreateStore(clock: () => Now); + (DealDbContext db, ITenantLimitStore store) = CreateStore(clock: () => Now); AddTenant(db, tenantId, TenantStatuses.Active); BudgetStateDto state = await store.AddUsageAsync(tenantId, tokens: 0, CancellationToken.None); @@ -153,7 +154,7 @@ public sealed class TenantLimitStoreTests public async Task UpdateBudgetAsync_AppliesNewBudgetAndPeriod_AndResetsFlags() { var tenantId = Guid.NewGuid(); - (DealDbContext db, TenantLimitStore store) = CreateStore(clock: () => Now); + (DealDbContext db, ITenantLimitStore store) = CreateStore(clock: () => Now); AddTenant(db, tenantId, TenantStatuses.Active); AddLimit(db, tenantId, budgetTokens: 1000, TenantLimitPeriods.Month, Now, usedTokens: 900, warned80: true, notifiedExhausted: true); @@ -170,7 +171,7 @@ public sealed class TenantLimitStoreTests public async Task UpdateBudgetAsync_InvalidArguments_Throw() { var tenantId = Guid.NewGuid(); - (DealDbContext db, TenantLimitStore store) = CreateStore(clock: () => Now); + (DealDbContext db, ITenantLimitStore store) = CreateStore(clock: () => Now); AddTenant(db, tenantId, TenantStatuses.Active); await Assert.ThrowsAsync( @@ -183,7 +184,7 @@ public sealed class TenantLimitStoreTests public async Task TryMarkWarnedAsync_MarksOnceAtThreshold() { var tenantId = Guid.NewGuid(); - (DealDbContext db, TenantLimitStore store) = CreateStore(clock: () => Now); + (DealDbContext db, ITenantLimitStore store) = CreateStore(clock: () => Now); AddTenant(db, tenantId, TenantStatuses.Active); AddLimit(db, tenantId, budgetTokens: 1000, TenantLimitPeriods.Month, Now, usedTokens: 800); @@ -203,7 +204,7 @@ public sealed class TenantLimitStoreTests public async Task TryMarkNotifiedExhaustedAsync_MarksOnceAtExhaustion() { var tenantId = Guid.NewGuid(); - (DealDbContext db, TenantLimitStore store) = CreateStore(clock: () => Now); + (DealDbContext db, ITenantLimitStore store) = CreateStore(clock: () => Now); AddTenant(db, tenantId, TenantStatuses.Active); AddLimit(db, tenantId, budgetTokens: 1000, TenantLimitPeriods.Month, Now, usedTokens: 1000); @@ -218,7 +219,7 @@ public sealed class TenantLimitStoreTests [Fact] public async Task ResetExpiredPeriodsAsync_ResetsOnlyExpiredPeriods() { - (DealDbContext db, TenantLimitStore store) = CreateStore(); + (DealDbContext db, ITenantLimitStore store) = CreateStore(); var expiredTenant = Guid.NewGuid(); var currentTenant = Guid.NewGuid(); // Период месяца начат за 2 месяца назад → истёк к Now; расход и флаги должны обнулиться. @@ -244,13 +245,13 @@ public sealed class TenantLimitStoreTests // Создаёт адаптер на уникальной InMemory-БД с фиксированными/системными часами. // clock: Источник «сейчас» (null — системные часы). // Возвращает: Кортеж (контекст для seed/проверок, адаптер). - private static (DealDbContext Db, TenantLimitStore Store) CreateStore(Func? clock = null) + private static (DealDbContext Db, ITenantLimitStore Store) CreateStore(Func? clock = null) { var options = new DbContextOptionsBuilder() .UseInMemoryDatabase(Guid.NewGuid().ToString("N")) .Options; var db = new DealDbContext(options); - var store = new TenantLimitStore( + ITenantLimitStore store = new TenantLimitStore( db, TokenBudgetDefaults.Default, new TokenBudgetService(), clock ?? (() => DateTimeOffset.UtcNow)); return (db, store); } diff --git a/src/core/tests/Deal.Tests.Unit/Modules/Discovery/DiscoverySearchErrorCounterTests.cs b/src/core/tests/Deal.Tests.Unit/Modules/Discovery/DiscoverySearchErrorCounterTests.cs index 1c5f64c..f4d743b 100644 --- a/src/core/tests/Deal.Tests.Unit/Modules/Discovery/DiscoverySearchErrorCounterTests.cs +++ b/src/core/tests/Deal.Tests.Unit/Modules/Discovery/DiscoverySearchErrorCounterTests.cs @@ -1,4 +1,5 @@ using Deal.Modules.Discovery.Application.Services; +using Deal.Modules.Discovery.Application.Abstractions; namespace Deal.Tests.Unit.Modules.Discovery; @@ -10,7 +11,7 @@ public sealed class DiscoverySearchErrorCounterTests [Fact] public void Next_IncrementsPerTask_ResetClears() { - var counter = new DiscoverySearchErrorCounter(() => DateTimeOffset.UtcNow); + IDiscoverySearchErrorCounter counter = new DiscoverySearchErrorCounter(() => DateTimeOffset.UtcNow); Assert.Equal(1, counter.Next("dt_1")); Assert.Equal(2, counter.Next("dt_1")); @@ -26,7 +27,7 @@ public sealed class DiscoverySearchErrorCounterTests public void Next_ExpiredEntry_EvictsAndStartsFromOne() { var now = new DateTimeOffset(2026, 9, 9, 12, 0, 0, TimeSpan.Zero); - var counter = new DiscoverySearchErrorCounter(() => now); + IDiscoverySearchErrorCounter counter = new DiscoverySearchErrorCounter(() => now); Assert.Equal(1, counter.Next("dt_1")); Assert.Equal(2, counter.Next("dt_1")); @@ -41,7 +42,7 @@ public sealed class DiscoverySearchErrorCounterTests public void Next_FreshEntry_NotEvictedAfterTtlReset() { var now = new DateTimeOffset(2026, 9, 9, 12, 0, 0, TimeSpan.Zero); - var counter = new DiscoverySearchErrorCounter(() => now); + IDiscoverySearchErrorCounter counter = new DiscoverySearchErrorCounter(() => now); Assert.Equal(1, counter.Next("dt_1")); @@ -57,7 +58,7 @@ public sealed class DiscoverySearchErrorCounterTests public void Reset_EvictsExpiredOfOtherTasks() { var now = new DateTimeOffset(2026, 9, 9, 12, 0, 0, TimeSpan.Zero); - var counter = new DiscoverySearchErrorCounter(() => now); + IDiscoverySearchErrorCounter counter = new DiscoverySearchErrorCounter(() => now); counter.Next("dt_1"); // устареет counter.Next("dt_2"); // свежая diff --git a/src/core/tests/Deal.Tests.Unit/Support/AiConnectionCheckerTests.cs b/src/core/tests/Deal.Tests.Unit/Support/AiConnectionCheckerTests.cs index 4735871..29605f3 100644 --- a/src/core/tests/Deal.Tests.Unit/Support/AiConnectionCheckerTests.cs +++ b/src/core/tests/Deal.Tests.Unit/Support/AiConnectionCheckerTests.cs @@ -1,6 +1,7 @@ using System.Net; using Deal.Infrastructure.Integrations.Services; using Deal.Modules.Settings.Application.Models; +using Deal.Modules.Settings.Application.Abstractions; namespace Deal.Tests.Unit.Support; @@ -15,7 +16,7 @@ public sealed class AiConnectionCheckerTests public async Task CheckAsync_CloudProviderWithoutKey_ReturnsNoKeyMessageAndNoHttp() { StubHttpMessageHandler handler = CreateHandler(HttpStatusCode.OK); - AiConnectionChecker checker = CreateChecker(handler); + IAiConnectionChecker checker = CreateChecker(handler); AiCheckResultDto result = await checker.CheckAsync(CloudDeepSeekRequest(), CancellationToken.None); @@ -33,7 +34,7 @@ public sealed class AiConnectionCheckerTests public async Task CheckAsync_LocalProvider_ReturnsOkLocalServerMessageAndNoHttp() { StubHttpMessageHandler handler = CreateHandler(HttpStatusCode.OK); - AiConnectionChecker checker = CreateChecker(handler); + IAiConnectionChecker checker = CreateChecker(handler); var request = new AiCheckRequest( ProviderId: "ollama", BaseUrl: "http://localhost:11434/v1", @@ -57,7 +58,7 @@ public sealed class AiConnectionCheckerTests public async Task CheckAsync_UnknownProvider_ReturnsProviderNotAllowedAndNoHttp() { StubHttpMessageHandler handler = CreateHandler(HttpStatusCode.OK); - AiConnectionChecker checker = CreateChecker(handler); + IAiConnectionChecker checker = CreateChecker(handler); var request = new AiCheckRequest( ProviderId: "unknown-provider", BaseUrl: "https://example.com", @@ -77,7 +78,7 @@ public sealed class AiConnectionCheckerTests public async Task CheckAsync_NonHttpBaseUrl_ReturnsInvalidBaseUrlAndNoHttp() { StubHttpMessageHandler handler = CreateHandler(HttpStatusCode.OK); - AiConnectionChecker checker = CreateChecker(handler); + IAiConnectionChecker checker = CreateChecker(handler); AiCheckResultDto result = await checker.CheckAsync( CloudDeepSeekRequest(baseUrl: "ftp://api.deepseek.com", key: "sk-1234567890ab"), @@ -94,7 +95,7 @@ public sealed class AiConnectionCheckerTests public async Task CheckAsync_SuccessfulHttp_ReturnsOkConnectedWithStatusAndMaskedKey() { StubHttpMessageHandler handler = CreateHandler(HttpStatusCode.OK); - AiConnectionChecker checker = CreateChecker(handler); + IAiConnectionChecker checker = CreateChecker(handler); AiCheckRequest request = CloudDeepSeekRequest( baseUrl: "https://api.deepseek.com/", key: "sk-1234567890ab"); @@ -121,7 +122,7 @@ public sealed class AiConnectionCheckerTests public async Task CheckAsync_KeyRejectedStatuses_ReturnsKeyRejectedMessage(int statusCode) { StubHttpMessageHandler handler = CreateHandler((HttpStatusCode)statusCode); - AiConnectionChecker checker = CreateChecker(handler); + IAiConnectionChecker checker = CreateChecker(handler); AiCheckResultDto result = await checker.CheckAsync( CloudDeepSeekRequest(key: "sk-1234567890ab"), @@ -136,7 +137,7 @@ public sealed class AiConnectionCheckerTests public async Task CheckAsync_OtherHttpError_ReturnsHttpCheckMessage() { StubHttpMessageHandler handler = CreateHandler(HttpStatusCode.InternalServerError); - AiConnectionChecker checker = CreateChecker(handler); + IAiConnectionChecker checker = CreateChecker(handler); AiCheckResultDto result = await checker.CheckAsync( CloudDeepSeekRequest(key: "sk-1234567890ab"), @@ -152,7 +153,7 @@ public sealed class AiConnectionCheckerTests { StubHttpMessageHandler handler = new((_, _) => Task.FromException(new HttpRequestException("Connection refused"))); - AiConnectionChecker checker = CreateChecker(handler); + IAiConnectionChecker checker = CreateChecker(handler); AiCheckResultDto result = await checker.CheckAsync( CloudDeepSeekRequest(key: "sk-1234567890ab"), @@ -169,7 +170,7 @@ public sealed class AiConnectionCheckerTests public async Task CheckAsync_Anthropic_UsesV1ModelsPathAndApiKeyHeaders() { StubHttpMessageHandler handler = CreateHandler(HttpStatusCode.OK); - AiConnectionChecker checker = CreateChecker(handler); + IAiConnectionChecker checker = CreateChecker(handler); var request = new AiCheckRequest( ProviderId: "anthropic", BaseUrl: "https://api.anthropic.com", @@ -197,7 +198,7 @@ public sealed class AiConnectionCheckerTests public async Task CheckAsync_PrivateBaseUrl_ReturnsSsrfBlockedAndNoHttp() { StubHttpMessageHandler handler = CreateHandler(HttpStatusCode.OK); - AiConnectionChecker checker = CreateChecker(handler); + IAiConnectionChecker checker = CreateChecker(handler); // SSRF-гейт (Security review): приватные/локальные адреса для проверки не-local провайдера запрещены // (метаданные облака 169.254.169.254, loopback, приватные подсети) — HTTP не выполняется вовсе. diff --git a/src/core/tests/Deal.Tests.Unit/Support/CbrRateSourceTests.cs b/src/core/tests/Deal.Tests.Unit/Support/CbrRateSourceTests.cs index 7b2ba0b..8e743df 100644 --- a/src/core/tests/Deal.Tests.Unit/Support/CbrRateSourceTests.cs +++ b/src/core/tests/Deal.Tests.Unit/Support/CbrRateSourceTests.cs @@ -2,6 +2,7 @@ using System.Net; using System.Text; using Deal.Infrastructure.Integrations.Services; using Microsoft.Extensions.Logging.Abstractions; +using Deal.Modules.Settings.Application.Abstractions; namespace Deal.Tests.Unit.Support; @@ -16,7 +17,7 @@ public sealed class CbrRateSourceTests public async Task FetchAsync_SamplePayload_ReturnsRatesToRub() { StubHttpMessageHandler handler = CreateJsonHandler(SamplePayload); - CbrRateSource source = CreateSource(handler); + IRatesSource source = CreateSource(handler); Dictionary? rates = await source.FetchAsync(CancellationToken.None); @@ -38,7 +39,7 @@ public sealed class CbrRateSourceTests { string payload = """{"Valute":{"XCD":{"Nominal":3,"Value":10.0},"TRL":{"Nominal":1,"Value":1.23456789}}}"""; StubHttpMessageHandler handler = CreateJsonHandler(payload); - CbrRateSource source = CreateSource(handler); + IRatesSource source = CreateSource(handler); Dictionary? rates = await source.FetchAsync(CancellationToken.None); @@ -52,7 +53,7 @@ public sealed class CbrRateSourceTests [Fact] public async Task FetchAsync_HttpErrorStatus_ReturnsNull() { - CbrRateSource source = CreateSource(CreateStatusHandler(HttpStatusCode.InternalServerError)); + IRatesSource source = CreateSource(CreateStatusHandler(HttpStatusCode.InternalServerError)); Dictionary? rates = await source.FetchAsync(CancellationToken.None); @@ -64,7 +65,7 @@ public sealed class CbrRateSourceTests { StubHttpMessageHandler handler = new((_, _) => Task.FromException(new HttpRequestException("Connection refused"))); - CbrRateSource source = CreateSource(handler); + IRatesSource source = CreateSource(handler); Dictionary? rates = await source.FetchAsync(CancellationToken.None); @@ -75,7 +76,7 @@ public sealed class CbrRateSourceTests public async Task FetchAsync_InvalidJsonBody_ReturnsNull() { StubHttpMessageHandler handler = CreateJsonHandler("{это не JSON"); - CbrRateSource source = CreateSource(handler); + IRatesSource source = CreateSource(handler); Dictionary? rates = await source.FetchAsync(CancellationToken.None); @@ -86,7 +87,7 @@ public sealed class CbrRateSourceTests public async Task FetchAsync_MissingValuteObject_ReturnsNull() { StubHttpMessageHandler handler = CreateJsonHandler("""{"Date":"2026-09-05T11:30:00+03:00"}"""); - CbrRateSource source = CreateSource(handler); + IRatesSource source = CreateSource(handler); Dictionary? rates = await source.FetchAsync(CancellationToken.None); @@ -98,7 +99,7 @@ public sealed class CbrRateSourceTests { string payload = """{"Valute":{"USD":{"Nominal":1,"Value":"abc"},"EUR":{"Nominal":1,"Value":99.9}}}"""; StubHttpMessageHandler handler = CreateJsonHandler(payload); - CbrRateSource source = CreateSource(handler); + IRatesSource source = CreateSource(handler); Dictionary? rates = await source.FetchAsync(CancellationToken.None); @@ -110,7 +111,7 @@ public sealed class CbrRateSourceTests { string payload = """{"Valute":{"USD":{"Nominal":0,"Value":92.5}}}"""; StubHttpMessageHandler handler = CreateJsonHandler(payload); - CbrRateSource source = CreateSource(handler); + IRatesSource source = CreateSource(handler); Dictionary? rates = await source.FetchAsync(CancellationToken.None); diff --git a/src/core/tests/Deal.Tests.Unit/Support/GrpcAiClassifierTests.cs b/src/core/tests/Deal.Tests.Unit/Support/GrpcAiClassifierTests.cs index c6371cd..da7a260 100644 --- a/src/core/tests/Deal.Tests.Unit/Support/GrpcAiClassifierTests.cs +++ b/src/core/tests/Deal.Tests.Unit/Support/GrpcAiClassifierTests.cs @@ -17,6 +17,8 @@ using Deal.Tests.Unit.Modules.Cards; using Deal.Tests.Unit.Modules.Settings; using Deal.Tests.Unit.Modules.Tenants; using Microsoft.Extensions.Logging.Abstractions; +using Deal.Contracts.Integrations.Abstractions; +using Deal.SharedKernel.Tenants.Abstractions; namespace Deal.Tests.Unit.Support; @@ -54,7 +56,7 @@ public sealed class GrpcAiClassifierTests settings.Preload(SettingsKeys.DomainKeywords, Json(TestKeywords)); FakeTenantLimitStore limits = new(); - GrpcAiClassifier classifier = CreateClassifier(port, settings, cipher, kanjStore, limits); + IAiClassifier classifier = CreateClassifier(port, settings, cipher, kanjStore, limits); AiFilterResultDto result = await classifier.FilterAsync("Купите телеграм-канал", CancellationToken.None); Assert.False(result.Pass); @@ -85,7 +87,7 @@ public sealed class GrpcAiClassifierTests { service.FilterUnavailable = true; (FakeSettingsStore settings, FakeSecretCipher cipher, FakeKanjStore kanjStore) = Context(port); - GrpcAiClassifier classifier = CreateClassifier(port, settings, cipher, kanjStore); + IAiClassifier classifier = CreateClassifier(port, settings, cipher, kanjStore); await Assert.ThrowsAsync( () => classifier.FilterAsync("текст", CancellationToken.None)); @@ -99,7 +101,7 @@ public sealed class GrpcAiClassifierTests { string text = new string('а', 5000); (FakeSettingsStore settings, FakeSecretCipher cipher, FakeKanjStore kanjStore) = Context(port); - GrpcAiClassifier classifier = CreateClassifier(port, settings, cipher, kanjStore); + IAiClassifier classifier = CreateClassifier(port, settings, cipher, kanjStore); await classifier.FilterAsync(text, CancellationToken.None); @@ -149,7 +151,7 @@ public sealed class GrpcAiClassifierTests }); FakeTenantLimitStore limits = new(); - GrpcAiClassifier classifier = CreateClassifier(port, settings, cipher, kanjStore, limits); + IAiClassifier classifier = CreateClassifier(port, settings, cipher, kanjStore, limits); AiParsedCardDto parsed = await classifier.ClassifyAsync("Нужен Python-разработчик, оплата от 2000$", CancellationToken.None); Assert.Equal("Middle Python в команду", parsed.Title); @@ -190,7 +192,7 @@ public sealed class GrpcAiClassifierTests (FakeSettingsStore settings, FakeSecretCipher cipher, FakeKanjStore kanjStore) = Context(port); FakeTenantLimitStore limits = new(); - GrpcAiClassifier classifier = CreateClassifier(port, settings, cipher, kanjStore, limits); + IAiClassifier classifier = CreateClassifier(port, settings, cipher, kanjStore, limits); // ok=false (модель без JSON) → AiUnavailableException: воркер падает в локальный разбор (aiFail). await Assert.ThrowsAsync( @@ -209,7 +211,7 @@ public sealed class GrpcAiClassifierTests { service.ClassifyUnavailable = true; (FakeSettingsStore settings, FakeSecretCipher cipher, FakeKanjStore kanjStore) = Context(port); - GrpcAiClassifier classifier = CreateClassifier(port, settings, cipher, kanjStore); + IAiClassifier classifier = CreateClassifier(port, settings, cipher, kanjStore); await Assert.ThrowsAsync( () => classifier.ClassifyAsync("текст", CancellationToken.None)); @@ -236,7 +238,7 @@ public sealed class GrpcAiClassifierTests }, }.ToJsonString()); - GrpcAiClassifier classifier = CreateClassifier(port, settings, cipher, kanjStore); + IAiClassifier classifier = CreateClassifier(port, settings, cipher, kanjStore); await classifier.ClassifyAsync("текст", CancellationToken.None); // ProviderConfig из настроек: id/base/model — переопределение, apiKey расшифрован, api_style — из каталога. @@ -260,7 +262,7 @@ public sealed class GrpcAiClassifierTests service.ClassifyReply = new ClassifyReply { Ok = true, Json = """{"title":"Т","stack":[],"is_spam":false}""" }; string text = new string('б', 6000); (FakeSettingsStore settings, FakeSecretCipher cipher, FakeKanjStore kanjStore) = Context(port); - GrpcAiClassifier classifier = CreateClassifier(port, settings, cipher, kanjStore); + IAiClassifier classifier = CreateClassifier(port, settings, cipher, kanjStore); await classifier.ClassifyAsync(text, CancellationToken.None); @@ -285,7 +287,7 @@ public sealed class GrpcAiClassifierTests FakeKanjStore kanjStore, FakeTenantLimitStore? limits = null) { - var tenantContext = new TenantContext(); + ITenantContext tenantContext = new TenantContext(); tenantContext.SetTenant(new TenantId(TenantIdValue)); var connection = new AiGrpcConnection(new AiServiceOptions { UseLocal = false, Endpoint = $"http://127.0.0.1:{port}" }); return new GrpcAiClassifier( diff --git a/src/core/tests/Deal.Tests.Unit/Support/LocalFileStorageTests.cs b/src/core/tests/Deal.Tests.Unit/Support/LocalFileStorageTests.cs index 39113fa..330d31c 100644 --- a/src/core/tests/Deal.Tests.Unit/Support/LocalFileStorageTests.cs +++ b/src/core/tests/Deal.Tests.Unit/Support/LocalFileStorageTests.cs @@ -1,6 +1,7 @@ using System.Text; using Deal.Contracts.Integrations.Models; using Deal.Infrastructure.Integrations.Storage.Services; +using Deal.Contracts.Integrations.Abstractions; namespace Deal.Tests.Unit.Support; @@ -13,7 +14,7 @@ public sealed class LocalFileStorageTests : IDisposable private static readonly byte[] SampleContent = Encoding.UTF8.GetBytes("файл-вложение-deal-123"); private readonly string _tempRoot; - private readonly LocalFileStorage _storage; + private readonly IFileStorage _storage; public LocalFileStorageTests() { diff --git a/src/core/tests/Deal.Tests.Unit/Support/TokenUsageRecorderTests.cs b/src/core/tests/Deal.Tests.Unit/Support/TokenUsageRecorderTests.cs index d5a5040..dfe6e66 100644 --- a/src/core/tests/Deal.Tests.Unit/Support/TokenUsageRecorderTests.cs +++ b/src/core/tests/Deal.Tests.Unit/Support/TokenUsageRecorderTests.cs @@ -8,6 +8,7 @@ using Deal.Modules.Tenants.Application.Services; using Deal.SharedKernel.Tenants.Models; using Deal.Tests.Unit.Modules.Settings; using Deal.Tests.Unit.Modules.Tenants; +using Deal.SharedKernel.Tenants.Abstractions; namespace Deal.Tests.Unit.Support; @@ -96,7 +97,7 @@ public sealed class TokenUsageRecorderTests { var settings = new FakeSettingsStore(); var limits = new FakeTenantLimitStore(); - var tenantContext = new TenantContext(); // без SetTenant — списание вне tenant-контекста невозможно. + ITenantContext tenantContext = new TenantContext(); // без SetTenant — списание вне tenant-контекста невозможно. var recorder = new TokenUsageRecorder( settings, limits, tenantContext, new TokenUsageEventService(new FakeTokenUsageEventStore())); @@ -144,7 +145,7 @@ public sealed class TokenUsageRecorderTests var settings = new FakeSettingsStore(); var limits = new FakeTenantLimitStore(); var events = new FakeTokenUsageEventStore(); - var tenantContext = new TenantContext(); + ITenantContext tenantContext = new TenantContext(); tenantContext.SetTenant(new TenantId(TenantIdValue)); var recorder = new TokenUsageRecorder(settings, limits, tenantContext, new TokenUsageEventService(events)); return (settings, limits, events, recorder); diff --git a/src/telegram-service/Deal.Telegram.Tests/Grpc/CoreIngressClientTests.cs b/src/telegram-service/Deal.Telegram.Tests/Grpc/CoreIngressClientTests.cs index eda5b81..75342d3 100644 --- a/src/telegram-service/Deal.Telegram.Tests/Grpc/CoreIngressClientTests.cs +++ b/src/telegram-service/Deal.Telegram.Tests/Grpc/CoreIngressClientTests.cs @@ -25,7 +25,7 @@ public sealed class CoreIngressClientTests (string endpoint, RecordingIngressService server, WebApplication app) = await FakeIngressServer.StartAsync(); try { - CoreIngressClient client = CreateClient(endpoint); + ICoreIngressClient client = CreateClient(endpoint); PushSourceReply reply = await client.PushSourceAsync( TenantId, @@ -65,7 +65,7 @@ public sealed class CoreIngressClientTests try { server.MonitoredIds.Add("-1001234567890"); - CoreIngressClient client = CreateClient(endpoint); + ICoreIngressClient client = CreateClient(endpoint); var entries = new List { new() { Id = "-1001234567890", Name = "IT Канал", Kind = "channel" }, @@ -91,7 +91,7 @@ public sealed class CoreIngressClientTests public async Task PushSource_UnreachableCore_ThrowsSessionException() { // Порт без сервера: соединение отклоняется — вызов падает до deadline (RpcTimeout 15 с). - CoreIngressClient client = CreateClient("http://127.0.0.1:1"); + ICoreIngressClient client = CreateClient("http://127.0.0.1:1"); SessionException exception = await Assert.ThrowsAsync( () => client.PushSourceAsync( diff --git a/src/telegram-service/Deal.Telegram/Core/CoreIngressClient.cs b/src/telegram-service/Deal.Telegram/Core/CoreIngressClient.cs index 1e9298f..a70bff6 100644 --- a/src/telegram-service/Deal.Telegram/Core/CoreIngressClient.cs +++ b/src/telegram-service/Deal.Telegram/Core/CoreIngressClient.cs @@ -7,6 +7,7 @@ using Deal.Grpc.Telegram; using Deal.Telegram.Sessions; using Grpc.Core; using Grpc.Net.Client; +using Deal.Telegram.Core; namespace Deal.Telegram.Core; @@ -44,7 +45,7 @@ public sealed class CoreIngressClient : ICoreIngressClient } /// - public async Task PushSourceAsync( + async Task ICoreIngressClient.PushSourceAsync( string tenantId, PushSourceRequest request, CancellationToken cancellationToken) @@ -61,7 +62,7 @@ public sealed class CoreIngressClient : ICoreIngressClient } /// - public async Task> SyncDialogsAsync( + async Task> ICoreIngressClient.SyncDialogsAsync( string tenantId, IReadOnlyList entries, CancellationToken cancellationToken) diff --git a/src/telegram-service/Deal.Telegram/Dialogs/RandomBackfillPacer.cs b/src/telegram-service/Deal.Telegram/Dialogs/RandomBackfillPacer.cs index 8ca4289..ad2a112 100644 --- a/src/telegram-service/Deal.Telegram/Dialogs/RandomBackfillPacer.cs +++ b/src/telegram-service/Deal.Telegram/Dialogs/RandomBackfillPacer.cs @@ -1,3 +1,4 @@ +using Deal.Telegram.Dialogs; namespace Deal.Telegram.Dialogs; /// @@ -6,7 +7,7 @@ namespace Deal.Telegram.Dialogs; public sealed class RandomBackfillPacer : IBackfillPacer { /// - public async Task WaitAsync( + async Task IBackfillPacer.WaitAsync( double minSeconds, double maxSeconds, CancellationToken cancellationToken) diff --git a/src/telegram-service/Deal.Telegram/Telegram/ClientFactory.cs b/src/telegram-service/Deal.Telegram/Telegram/ClientFactory.cs index 7470b04..9051ac9 100644 --- a/src/telegram-service/Deal.Telegram/Telegram/ClientFactory.cs +++ b/src/telegram-service/Deal.Telegram/Telegram/ClientFactory.cs @@ -1,3 +1,4 @@ +using Deal.Telegram.Telegram; namespace Deal.Telegram.Telegram; /// @@ -6,7 +7,7 @@ namespace Deal.Telegram.Telegram; public sealed class ClientFactory : ITelegramClientFactory { /// - public ISessionClient Create( + ISessionClient ITelegramClientFactory.Create( int apiId, string apiHash, byte[]? storedSession) diff --git a/src/telegram-service/Deal.Telegram/Telegram/WTelegramSessionClient.cs b/src/telegram-service/Deal.Telegram/Telegram/WTelegramSessionClient.cs index 99b49b3..31da04c 100644 --- a/src/telegram-service/Deal.Telegram/Telegram/WTelegramSessionClient.cs +++ b/src/telegram-service/Deal.Telegram/Telegram/WTelegramSessionClient.cs @@ -5,6 +5,7 @@ using Grpc.Core; using TL; using WTelegram; using RpcException = TL.RpcException; +using Deal.Telegram.Telegram; namespace Deal.Telegram.Telegram; @@ -82,7 +83,7 @@ public sealed class WTelegramSessionClient : ISessionClient public byte[]? SessionBytes => Volatile.Read(ref _latestSessionBytes); /// - public async Task ConnectAsync(CancellationToken cancellationToken) + async Task ISessionClient.ConnectAsync(CancellationToken cancellationToken) { if (IsConnected) { @@ -94,7 +95,7 @@ public sealed class WTelegramSessionClient : ISessionClient } /// - public async Task RequestCodeAsync(string phone, CancellationToken cancellationToken) + async Task ISessionClient.RequestCodeAsync(string phone, CancellationToken cancellationToken) { _phone = phone; _phoneCodeHash = null; @@ -118,7 +119,7 @@ public sealed class WTelegramSessionClient : ISessionClient } /// - public async Task SubmitCodeAsync(string code, CancellationToken cancellationToken) + async Task ISessionClient.SubmitCodeAsync(string code, CancellationToken cancellationToken) { if (_phoneAlreadyAuthorized) { @@ -158,7 +159,7 @@ public sealed class WTelegramSessionClient : ISessionClient } /// - public async Task SubmitPasswordAsync(string password, CancellationToken cancellationToken) + async Task ISessionClient.SubmitPasswordAsync(string password, CancellationToken cancellationToken) { try { @@ -178,7 +179,7 @@ public sealed class WTelegramSessionClient : ISessionClient } /// - public async Task StartQrAsync(Action onQrUrl, CancellationToken cancellationToken) + async Task ISessionClient.StartQrAsync(Action onQrUrl, CancellationToken cancellationToken) { try { @@ -196,13 +197,13 @@ public sealed class WTelegramSessionClient : ISessionClient } /// - public async Task LogOutAsync(CancellationToken cancellationToken) + async Task ISessionClient.LogOutAsync(CancellationToken cancellationToken) { await _client.Auth_LogOut().WaitAsync(cancellationToken).ConfigureAwait(false); } /// - public async Task GetAccountAsync(CancellationToken cancellationToken) + async Task ISessionClient.GetAccountAsync(CancellationToken cancellationToken) { UserBase[] users = await _client.Users_GetUsers(InputUser.Self).WaitAsync(cancellationToken).ConfigureAwait(false); string username = users.OfType().FirstOrDefault()?.username ?? string.Empty; @@ -221,7 +222,7 @@ public sealed class WTelegramSessionClient : ISessionClient public event Func? MessageReceived; /// - public async Task> GetDialogsAsync(int limit, CancellationToken cancellationToken) + async Task> ISessionClient.GetDialogsAsync(int limit, CancellationToken cancellationToken) { Messages_DialogsBase result = await RunTlCallAsync(() => _client.Messages_GetDialogs(limit: limit), cancellationToken).ConfigureAwait(false); (DialogBase[] dialogs, Dictionary chats, Dictionary users) = UnpackDialogs(result); @@ -241,7 +242,7 @@ public sealed class WTelegramSessionClient : ISessionClient } /// - public async Task> GetMessagesAsync( + async Task> ISessionClient.GetMessagesAsync( string dialogId, int limit, CancellationToken cancellationToken) @@ -265,7 +266,7 @@ public sealed class WTelegramSessionClient : ISessionClient } /// - public async Task GetMessageAsync( + async Task ISessionClient.GetMessageAsync( string dialogId, long msgId, CancellationToken cancellationToken) @@ -295,7 +296,7 @@ public sealed class WTelegramSessionClient : ISessionClient } /// - public async Task MarkReadAsync(string dialogId, CancellationToken cancellationToken) + async Task ISessionClient.MarkReadAsync(string dialogId, CancellationToken cancellationToken) { InputPeer peer = await ResolvePeerAsync(dialogId, cancellationToken).ConfigureAwait(false); await RunTlCallAsync(() => _client.ReadHistory(peer), cancellationToken).ConfigureAwait(false); @@ -303,7 +304,7 @@ public sealed class WTelegramSessionClient : ISessionClient /// - public async Task> SearchAsync( + async Task> ISessionClient.SearchAsync( string query, int limit, CancellationToken cancellationToken) @@ -327,7 +328,7 @@ public sealed class WTelegramSessionClient : ISessionClient } /// - public async Task GetInfoAsync(string dialogId, CancellationToken cancellationToken) + async Task ISessionClient.GetInfoAsync(string dialogId, CancellationToken cancellationToken) { TelegramSourceInfo unknown = DefaultSourceInfo(dialogId); if (!TryParseSignedId(dialogId, out bool isChannel, out bool isChat, out bool isUser, out long rawId)) @@ -361,7 +362,7 @@ public sealed class WTelegramSessionClient : ISessionClient } /// - public async Task ReadForEvalAsync( + async Task ISessionClient.ReadForEvalAsync( string dialogId, int limit, CancellationToken cancellationToken) @@ -410,7 +411,7 @@ public sealed class WTelegramSessionClient : ISessionClient } /// - public async Task JoinAsync(string username, CancellationToken cancellationToken) + async Task ISessionClient.JoinAsync(string username, CancellationToken cancellationToken) { Contacts_ResolvedPeer resolved = await RunTlCallAsync(() => _client.Contacts_ResolveUsername(username), cancellationToken).ConfigureAwait(false); CacheEntities(resolved.chats.Values, resolved.users.Values); @@ -425,7 +426,7 @@ public sealed class WTelegramSessionClient : ISessionClient } /// - public async Task LeaveAsync(string dialogId, CancellationToken cancellationToken) + async Task ISessionClient.LeaveAsync(string dialogId, CancellationToken cancellationToken) { if (!TryParseSignedId(dialogId, out bool isChannel, out _, out _, out long rawId) || !isChannel) { @@ -923,7 +924,7 @@ public sealed class WTelegramSessionClient : ISessionClient // Канал/пользователь без access_hash (рестарт/новый источник): доливаем сущности // первой страницей списка диалогов — каталог ядра строится из неё же (лимит 500, как refresh). - await GetDialogsAsync(DialogResolvePageSize, cancellationToken).ConfigureAwait(false); + await ((ISessionClient)this).GetDialogsAsync(DialogResolvePageSize, cancellationToken).ConfigureAwait(false); return BuildPeer(isChannel, isChat, isUser, rawId) ?? throw new SessionException(StatusCode.InvalidArgument, SessionErrorMessages.UnknownDialog); } @@ -945,7 +946,7 @@ public sealed class WTelegramSessionClient : ISessionClient } } - await GetDialogsAsync(DialogResolvePageSize, cancellationToken).ConfigureAwait(false); + await ((ISessionClient)this).GetDialogsAsync(DialogResolvePageSize, cancellationToken).ConfigureAwait(false); lock (_entityCacheGate) { return _entityAccessHashes.TryGetValue(dialogId, out long accessHash)