Перевести реализации интерфейсов на явные (§11, вариант A)

Codemod scripts/make_explicit.py: 161 член в 30 прод-файлах конвертирован в
вид "Тип IFoo.Член" (частичные классы и многострочные сигнатуры учтены; Card и
ICard-семейство — DTO, оставлены implicit). Потребители, дёргавшие классы
напрямую, перетипизированы на интерфейсы: 8 мест в проде (самовызовы через
((ISessionClient)this), снят дефолт параметра в явной реализации) и 17 тестовых
файлов (поля, tuple-деконструкции, var/target-typed new). Build 5 sln 0/0,
тесты 1340/130/52/38/9 зелёные.
This commit is contained in:
Rustam Khalimov
2026-09-11 20:16:06 +03:00
parent 355723f4ad
commit 206d61068a
48 changed files with 516 additions and 250 deletions
+234
View File
@@ -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<indent>\s*)(?P<mods>(?:public\s+|internal\s+|protected\s+|private\s+|static\s+|"
r"virtual\s+|override\s+|sealed\s+|async\s+|new\s+)*)"
r"(?P<ret>[\w<>\[\],\s.?]+?)\s(?P<name>\w+)\s*(?:<[^>]*>)?\s*\(")
PROP_DECL = re.compile(
r"^(?P<indent>\s*)(?P<mods>(?:public\s+|internal\s+|protected\s+|private\s+|static\s+|"
r"virtual\s+|override\s+|sealed\s+|new\s+)*)"
r"(?P<type>[\w<>\[\],\s.?]+?)\s(?P<name>\w+)\s*\{\s*(?P<accessors>(?: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())
@@ -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<LlmHttpException>(() =>
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<LlmHttpException>(() =>
client.ChatAsync(OpenAiConfig(), "Система", "Сообщение", CancellationToken.None));
@@ -171,7 +171,7 @@ public sealed class LlmHttpClientTests
{
Content = new StringContent("<html>upstream error</html>", System.Text.Encoding.UTF8, "text/html"),
});
LlmHttpClient client = CreateClient(handler);
IProviderClient client = CreateClient(handler);
await Assert.ThrowsAsync<LlmHttpException>(() =>
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);
+1 -1
View File
@@ -65,7 +65,7 @@ public sealed class LlmHttpClient : IProviderClient
/// <param name="systemPrompt">Системный промпт.</param>
/// <param name="userText">Пользовательское сообщение/контекст.</param>
/// <returns>Текст ответа и usage API-ответа (null при его отсутствии).</returns>
public async Task<ProviderChatResult> ChatAsync(
async Task<ProviderChatResult> IProviderClient.ChatAsync(
LlmConfig config,
string systemPrompt,
string userText,
@@ -17,8 +17,8 @@ public sealed class TenantContext : ITenantContext
public string? SchemaName => Current.Value?.SchemaName;
/// <inheritdoc />
public void SetTenant(TenantId tenantId) => Current.Value = tenantId;
void ITenantContext.SetTenant(TenantId tenantId) => Current.Value = tenantId;
/// <inheritdoc />
public void Reset() => Current.Value = null;
void ITenantContext.Reset() => Current.Value = null;
}
@@ -79,7 +79,7 @@ public sealed class AiConnectionChecker : IAiConnectionChecker
}
/// <inheritdoc />
public async Task<AiCheckResultDto> CheckAsync(AiCheckRequest request, CancellationToken ct)
async Task<AiCheckResultDto> IAiConnectionChecker.CheckAsync(AiCheckRequest request, CancellationToken ct)
{
ArgumentNullException.ThrowIfNull(request);
@@ -55,7 +55,7 @@ public sealed class BudgetedAiClassifier : IAiClassifier
}
/// <inheritdoc />
public async Task<AiFilterResultDto> FilterAsync(string text, CancellationToken ct)
async Task<AiFilterResultDto> IAiClassifier.FilterAsync(string text, CancellationToken ct)
{
if (await IsPaidAllowedAsync(ct))
{
@@ -68,7 +68,7 @@ public sealed class BudgetedAiClassifier : IAiClassifier
}
/// <inheritdoc />
public async Task<AiParsedCardDto> ClassifyAsync(string text, CancellationToken ct)
async Task<AiParsedCardDto> IAiClassifier.ClassifyAsync(string text, CancellationToken ct)
{
if (await IsPaidAllowedAsync(ct))
{
@@ -51,7 +51,7 @@ public sealed class BudgetedAiTools : IAiTools
}
/// <inheritdoc />
public async Task<AiGenerateKeywordsResultDto> GenerateKeywordsAsync(string description, CancellationToken ct)
async Task<AiGenerateKeywordsResultDto> IAiTools.GenerateKeywordsAsync(string description, CancellationToken ct)
{
BudgetStateDto state = await GateStateAsync(ct);
if (state.Allowed)
@@ -70,7 +70,7 @@ public sealed class BudgetedAiTools : IAiTools
}
/// <inheritdoc />
public async Task<AiEvaluateFitResultDto> EvaluateFitAsync(
async Task<AiEvaluateFitResultDto> IAiTools.EvaluateFitAsync(
string text,
string description,
IReadOnlyCollection<string> keywords,
@@ -48,7 +48,7 @@ public sealed class CbrRateSource : IRatesSource
}
/// <inheritdoc />
public async Task<Dictionary<string, double>?> FetchAsync(CancellationToken ct)
async Task<Dictionary<string, double>?> IRatesSource.FetchAsync(CancellationToken ct)
{
try
{
@@ -71,7 +71,7 @@ public sealed class GrpcAiClassifier : IAiClassifier
}
/// <inheritdoc />
public async Task<AiFilterResultDto> FilterAsync(string text, CancellationToken ct)
async Task<AiFilterResultDto> IAiClassifier.FilterAsync(string text, CancellationToken ct)
{
TenantId tenantId = RequireTenant();
try
@@ -107,7 +107,7 @@ public sealed class GrpcAiClassifier : IAiClassifier
}
/// <inheritdoc />
public async Task<AiParsedCardDto> ClassifyAsync(string text, CancellationToken ct)
async Task<AiParsedCardDto> IAiClassifier.ClassifyAsync(string text, CancellationToken ct)
{
TenantId tenantId = RequireTenant();
string systemPrompt = await _contextBuilder.BuildClassifySystemPromptAsync(ct);
@@ -71,7 +71,7 @@ public sealed class GrpcAiTools : IAiTools
}
/// <inheritdoc />
public async Task<AiGenerateKeywordsResultDto> GenerateKeywordsAsync(string description, CancellationToken ct)
async Task<AiGenerateKeywordsResultDto> IAiTools.GenerateKeywordsAsync(string description, CancellationToken ct)
{
TenantId tenantId = RequireTenant();
try
@@ -105,7 +105,7 @@ public sealed class GrpcAiTools : IAiTools
}
/// <inheritdoc />
public async Task<AiEvaluateFitResultDto> EvaluateFitAsync(
async Task<AiEvaluateFitResultDto> IAiTools.EvaluateFitAsync(
string text,
string description,
IReadOnlyCollection<string> keywords,
@@ -98,7 +98,7 @@ public sealed class GrpcMlClient : IMlClient, IMlTrainClient
}
/// <inheritdoc />
public async Task<MlStatusResponseDto> StatusAsync(CancellationToken ct)
async Task<MlStatusResponseDto> IMlClient.StatusAsync(CancellationToken ct)
{
TenantId tenantId = RequireTenant();
MlStatusCache.Snapshot snapshot = await GetServiceSnapshotAsync(tenantId, ct);
@@ -123,7 +123,7 @@ public sealed class GrpcMlClient : IMlClient, IMlTrainClient
}
/// <inheritdoc />
public async Task<MlPredictResultDto> PredictAsync(string text, CancellationToken ct)
async Task<MlPredictResultDto> IMlClient.PredictAsync(string text, CancellationToken ct)
{
TenantId tenantId = RequireTenant();
try
@@ -144,7 +144,7 @@ public sealed class GrpcMlClient : IMlClient, IMlTrainClient
}
/// <inheritdoc />
public async Task<MlResetResultDto> ResetAsync(CancellationToken ct)
async Task<MlResetResultDto> IMlClient.ResetAsync(CancellationToken ct)
{
TenantId tenantId = RequireTenant();
ResetReply reply;
@@ -172,7 +172,7 @@ public sealed class GrpcMlClient : IMlClient, IMlTrainClient
}
/// <inheritdoc />
public async Task PushAsync(
async Task IMlClient.PushAsync(
string text,
string label,
double delta,
@@ -182,7 +182,7 @@ public sealed class GrpcMlClient : IMlClient, IMlTrainClient
}
/// <inheritdoc />
public async Task<int> TrainBatchAsync(IReadOnlyList<MlOutboxEntryDto> items, CancellationToken ct)
async Task<int> IMlTrainClient.TrainBatchAsync(IReadOnlyList<MlOutboxEntryDto> items, CancellationToken ct)
{
TenantId tenantId = RequireTenant();
var request = new TrainBatchRequest();
@@ -59,7 +59,7 @@ public sealed class GrpcTelegramClient : ITelegramGateway
}
/// <inheritdoc />
public async Task<TelegramAccountStatusDto> StatusAsync(CancellationToken ct)
async Task<TelegramAccountStatusDto> ITelegramGateway.StatusAsync(CancellationToken ct)
{
TenantId tenantId = RequireTenant();
try
@@ -82,7 +82,7 @@ public sealed class GrpcTelegramClient : ITelegramGateway
}
/// <inheritdoc />
public async Task<TelegramAuthResultDto> StartPhoneAsync(
async Task<TelegramAuthResultDto> ITelegramGateway.StartPhoneAsync(
string phone,
int apiId,
string apiHash,
@@ -105,7 +105,7 @@ public sealed class GrpcTelegramClient : ITelegramGateway
}
/// <inheritdoc />
public async Task<TelegramAuthResultDto> StartQrAsync(
async Task<TelegramAuthResultDto> ITelegramGateway.StartQrAsync(
int apiId,
string apiHash,
CancellationToken ct)
@@ -129,7 +129,7 @@ public sealed class GrpcTelegramClient : ITelegramGateway
}
/// <inheritdoc />
public async Task<string> SendCodeAsync(string code, CancellationToken ct)
async Task<string> ITelegramGateway.SendCodeAsync(string code, CancellationToken ct)
{
TenantId tenantId = RequireTenant();
try
@@ -147,7 +147,7 @@ public sealed class GrpcTelegramClient : ITelegramGateway
}
/// <inheritdoc />
public async Task<string> SendPasswordAsync(string password, CancellationToken ct)
async Task<string> ITelegramGateway.SendPasswordAsync(string password, CancellationToken ct)
{
TenantId tenantId = RequireTenant();
try
@@ -165,7 +165,7 @@ public sealed class GrpcTelegramClient : ITelegramGateway
}
/// <inheritdoc />
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
}
/// <inheritdoc />
public async Task<IReadOnlyList<TelegramDialogEntryDto>> RefreshDialogsAsync(CancellationToken ct)
async Task<IReadOnlyList<TelegramDialogEntryDto>> ITelegramGateway.RefreshDialogsAsync(CancellationToken ct)
{
TenantId tenantId = RequireTenant();
try
@@ -198,7 +198,7 @@ public sealed class GrpcTelegramClient : ITelegramGateway
}
/// <inheritdoc />
public async Task SetMonitorAsync(
async Task ITelegramGateway.SetMonitorAsync(
string dialogId,
bool enabled,
CancellationToken ct)
@@ -218,7 +218,7 @@ public sealed class GrpcTelegramClient : ITelegramGateway
}
/// <inheritdoc />
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
}
/// <inheritdoc />
public async Task<int> BackfillAsync(
async Task<int> ITelegramGateway.BackfillAsync(
string dialogId,
bool force,
CancellationToken ct)
@@ -256,7 +256,7 @@ public sealed class GrpcTelegramClient : ITelegramGateway
}
/// <inheritdoc />
public async Task<IReadOnlyList<TelegramRecentMessageDto>> ReadRecentAsync(
async Task<IReadOnlyList<TelegramRecentMessageDto>> ITelegramGateway.ReadRecentAsync(
string dialogId,
int limit,
CancellationToken ct)
@@ -279,7 +279,7 @@ public sealed class GrpcTelegramClient : ITelegramGateway
}
/// <inheritdoc />
public async Task<TelegramSourceContentDto> ReadSourceAsync(
async Task<TelegramSourceContentDto> ITelegramGateway.ReadSourceAsync(
string dialogId,
long msgId,
CancellationToken ct)
@@ -303,7 +303,7 @@ public sealed class GrpcTelegramClient : ITelegramGateway
}
/// <inheritdoc />
public async Task<IReadOnlyList<TelegramDialogEntryDto>> SearchAsync(
async Task<IReadOnlyList<TelegramDialogEntryDto>> ITelegramGateway.SearchAsync(
string query,
int limit,
CancellationToken ct)
@@ -324,7 +324,7 @@ public sealed class GrpcTelegramClient : ITelegramGateway
}
/// <inheritdoc />
public async Task<TelegramChannelInfoDto> InfoAsync(string dialogId, CancellationToken ct)
async Task<TelegramChannelInfoDto> ITelegramGateway.InfoAsync(string dialogId, CancellationToken ct)
{
TenantId tenantId = RequireTenant();
try
@@ -350,7 +350,7 @@ public sealed class GrpcTelegramClient : ITelegramGateway
}
/// <inheritdoc />
public async Task<TelegramEvalReadDto> ReadForEvalAsync(
async Task<TelegramEvalReadDto> ITelegramGateway.ReadForEvalAsync(
string dialogId,
int limit,
CancellationToken ct)
@@ -381,7 +381,7 @@ public sealed class GrpcTelegramClient : ITelegramGateway
}
/// <inheritdoc />
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
}
/// <inheritdoc />
public async Task LeaveAsync(string dialogId, CancellationToken ct)
async Task ITelegramGateway.LeaveAsync(string dialogId, CancellationToken ct)
{
TenantId tenantId = RequireTenant();
try
@@ -13,11 +13,11 @@ public sealed class LocalAiTools : IAiTools
"ИИ-инструменты доступны только при подключённом ai-service (Services:Ai:UseLocal=false).";
/// <inheritdoc />
public Task<AiGenerateKeywordsResultDto> GenerateKeywordsAsync(string description, CancellationToken ct)
Task<AiGenerateKeywordsResultDto> IAiTools.GenerateKeywordsAsync(string description, CancellationToken ct)
=> throw new NotSupportedException(NotSupportedMessage);
/// <inheritdoc />
public Task<AiEvaluateFitResultDto> EvaluateFitAsync(
Task<AiEvaluateFitResultDto> IAiTools.EvaluateFitAsync(
string text,
string description,
IReadOnlyCollection<string> keywords,
@@ -12,13 +12,13 @@ public sealed class LocalTelegramGateway : ITelegramGateway
private const string IdlePhase = "idle";
/// <inheritdoc />
public Task<TelegramAccountStatusDto> StatusAsync(CancellationToken ct)
Task<TelegramAccountStatusDto> ITelegramGateway.StatusAsync(CancellationToken ct)
{
return Task.FromResult(new TelegramAccountStatusDto(IdlePhase, false, false, string.Empty, null, null));
}
/// <inheritdoc />
public Task<TelegramAuthResultDto> StartPhoneAsync(
Task<TelegramAuthResultDto> ITelegramGateway.StartPhoneAsync(
string phone,
int apiId,
string apiHash,
@@ -26,7 +26,7 @@ public sealed class LocalTelegramGateway : ITelegramGateway
=> Task.FromResult(new TelegramAuthResultDto(IdlePhase, null));
/// <inheritdoc />
public Task<TelegramAuthResultDto> StartQrAsync(
Task<TelegramAuthResultDto> ITelegramGateway.StartQrAsync(
int apiId,
string apiHash,
CancellationToken ct)
@@ -39,20 +39,20 @@ public sealed class LocalTelegramGateway : ITelegramGateway
public Task<string> SendPasswordAsync(string password, CancellationToken ct) => Task.FromResult(IdlePhase);
/// <inheritdoc />
public Task LogoutAsync(CancellationToken ct) => Task.CompletedTask;
Task ITelegramGateway.LogoutAsync(CancellationToken ct) => Task.CompletedTask;
/// <inheritdoc />
public Task<IReadOnlyList<TelegramDialogEntryDto>> RefreshDialogsAsync(CancellationToken ct)
Task<IReadOnlyList<TelegramDialogEntryDto>> ITelegramGateway.RefreshDialogsAsync(CancellationToken ct)
=> Task.FromResult<IReadOnlyList<TelegramDialogEntryDto>>([]);
/// <inheritdoc />
public Task SetMonitorAsync(
Task ITelegramGateway.SetMonitorAsync(
string dialogId,
bool enabled,
CancellationToken ct) => Task.CompletedTask;
/// <inheritdoc />
public Task SetMonitorAllAsync(bool enabled, CancellationToken ct) => Task.CompletedTask;
Task ITelegramGateway.SetMonitorAllAsync(bool enabled, CancellationToken ct) => Task.CompletedTask;
/// <inheritdoc />
public Task<int> BackfillAsync(
@@ -61,40 +61,40 @@ public sealed class LocalTelegramGateway : ITelegramGateway
CancellationToken ct) => Task.FromResult(0);
/// <inheritdoc />
public Task<IReadOnlyList<TelegramRecentMessageDto>> ReadRecentAsync(
Task<IReadOnlyList<TelegramRecentMessageDto>> ITelegramGateway.ReadRecentAsync(
string dialogId,
int limit,
CancellationToken ct)
=> Task.FromResult<IReadOnlyList<TelegramRecentMessageDto>>([]);
/// <inheritdoc />
public Task<TelegramSourceContentDto> ReadSourceAsync(
Task<TelegramSourceContentDto> ITelegramGateway.ReadSourceAsync(
string dialogId,
long msgId,
CancellationToken ct)
=> Task.FromResult(new TelegramSourceContentDto(false, null, null));
/// <inheritdoc />
public Task<IReadOnlyList<TelegramDialogEntryDto>> SearchAsync(
Task<IReadOnlyList<TelegramDialogEntryDto>> ITelegramGateway.SearchAsync(
string query,
int limit,
CancellationToken ct)
=> Task.FromResult<IReadOnlyList<TelegramDialogEntryDto>>([]);
/// <inheritdoc />
public Task<TelegramChannelInfoDto> InfoAsync(string dialogId, CancellationToken ct)
Task<TelegramChannelInfoDto> ITelegramGateway.InfoAsync(string dialogId, CancellationToken ct)
=> Task.FromResult(new TelegramChannelInfoDto(dialogId, string.Empty, string.Empty, string.Empty, SourceDefaults.DefaultHue, null, false));
/// <inheritdoc />
public Task<TelegramEvalReadDto> ReadForEvalAsync(
Task<TelegramEvalReadDto> ITelegramGateway.ReadForEvalAsync(
string dialogId,
int limit,
CancellationToken ct)
=> Task.FromResult(new TelegramEvalReadDto(false, "no_history", []));
/// <inheritdoc />
public Task JoinAsync(string username, CancellationToken ct) => Task.CompletedTask;
Task ITelegramGateway.JoinAsync(string username, CancellationToken ct) => Task.CompletedTask;
/// <inheritdoc />
public Task LeaveAsync(string dialogId, CancellationToken ct) => Task.CompletedTask;
Task ITelegramGateway.LeaveAsync(string dialogId, CancellationToken ct) => Task.CompletedTask;
}
@@ -32,7 +32,7 @@ public sealed class LocalFileStorage : IFileStorage
public override string ToString() => $"LocalFileStorage (root: {_rootPath})";
/// <inheritdoc />
public async Task<string> PutAsync(
async Task<string> IFileStorage.PutAsync(
string objectKey,
Stream content,
string contentType,
@@ -56,7 +56,7 @@ public sealed class LocalFileStorage : IFileStorage
}
/// <inheritdoc />
public Task<Stream?> GetAsync(string objectKey, CancellationToken ct)
Task<Stream?> IFileStorage.GetAsync(string objectKey, CancellationToken ct)
{
string path = ResolvePath(objectKey);
if (!File.Exists(path))
@@ -69,7 +69,7 @@ public sealed class LocalFileStorage : IFileStorage
}
/// <inheritdoc />
public Task<FileMeta?> StatAsync(string objectKey, CancellationToken ct)
Task<FileMeta?> IFileStorage.StatAsync(string objectKey, CancellationToken ct)
{
string path = ResolvePath(objectKey);
if (!File.Exists(path))
@@ -82,7 +82,7 @@ public sealed class LocalFileStorage : IFileStorage
}
/// <inheritdoc />
public Task DeleteAsync(string objectKey, CancellationToken ct)
Task IFileStorage.DeleteAsync(string objectKey, CancellationToken ct)
{
string path = ResolvePath(objectKey);
if (File.Exists(path))
@@ -67,7 +67,7 @@ public sealed class MinioFileStorage : IFileStorage
public override string ToString() => $"MinioFileStorage (endpoint: {_endpoint}; bucket: {_bucket})";
/// <inheritdoc />
public async Task<string> PutAsync(
async Task<string> IFileStorage.PutAsync(
string objectKey,
Stream content,
string contentType,
@@ -99,7 +99,7 @@ public sealed class MinioFileStorage : IFileStorage
}
/// <inheritdoc />
public async Task<Stream?> GetAsync(string objectKey, CancellationToken ct)
async Task<Stream?> IFileStorage.GetAsync(string objectKey, CancellationToken ct)
{
MemoryStream buffer = new();
try
@@ -129,7 +129,7 @@ public sealed class MinioFileStorage : IFileStorage
}
/// <inheritdoc />
public async Task<FileMeta?> StatAsync(string objectKey, CancellationToken ct)
async Task<FileMeta?> IFileStorage.StatAsync(string objectKey, CancellationToken ct)
{
try
{
@@ -145,7 +145,7 @@ public sealed class MinioFileStorage : IFileStorage
}
/// <inheritdoc />
public async Task DeleteAsync(string objectKey, CancellationToken ct)
async Task IFileStorage.DeleteAsync(string objectKey, CancellationToken ct)
{
try
{
@@ -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
{
/// <inheritdoc />
public async Task UpsertBlacklistAsync(
async Task IDiscoveryStore.UpsertBlacklistAsync(
string dialogId,
string name,
string reason,
@@ -38,13 +39,13 @@ public sealed partial class DiscoveryStore
}
/// <inheritdoc />
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);
}
/// <inheritdoc />
public async Task<DiscoveryBlacklistDto?> GetBlacklistAsync(string dialogId, CancellationToken ct)
async Task<DiscoveryBlacklistDto?> IDiscoveryStore.GetBlacklistAsync(string dialogId, CancellationToken ct)
{
DiscBlacklistEntity? row = await _dbContext.DiscBlacklist
.AsNoTracking()
@@ -53,7 +54,7 @@ public sealed partial class DiscoveryStore
}
/// <inheritdoc />
public async Task<IReadOnlyList<DiscoveryBlacklistDto>> ListBlacklistAsync(CancellationToken ct)
async Task<IReadOnlyList<DiscoveryBlacklistDto>> IDiscoveryStore.ListBlacklistAsync(CancellationToken ct)
{
List<DiscBlacklistEntity> rows = await _dbContext.DiscBlacklist
.AsNoTracking()
@@ -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
{
/// <inheritdoc />
public async Task<IReadOnlyList<DiscoveryCandidateDto>> ListCandidatesAsync(
async Task<IReadOnlyList<DiscoveryCandidateDto>> IDiscoveryStore.ListCandidatesAsync(
string taskId,
string? status,
CancellationToken ct)
@@ -26,7 +27,7 @@ public sealed partial class DiscoveryStore
}
/// <inheritdoc />
public async Task<DiscoveryCandidateDto?> GetCandidateAsync(string dialogId, CancellationToken ct)
async Task<DiscoveryCandidateDto?> IDiscoveryStore.GetCandidateAsync(string dialogId, CancellationToken ct)
{
DiscCandidateEntity? row = await _dbContext.DiscCandidates
.AsNoTracking()
@@ -35,19 +36,19 @@ public sealed partial class DiscoveryStore
}
/// <inheritdoc />
public async Task<bool> IsDialogMonitoredAsync(string dialogId, CancellationToken ct)
async Task<bool> IDiscoveryStore.IsDialogMonitoredAsync(string dialogId, CancellationToken ct)
{
return await _dbContext.Dialogs.AnyAsync(dialog => dialog.Id == dialogId, ct);
}
/// <inheritdoc />
public Task<bool> IsBlacklistedAsync(string dialogId, CancellationToken ct)
Task<bool> IDiscoveryStore.IsBlacklistedAsync(string dialogId, CancellationToken ct)
{
return _dbContext.DiscBlacklist.AnyAsync(row => row.DialogId == dialogId, ct);
}
/// <inheritdoc />
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
}
/// <inheritdoc />
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);
}
/// <inheritdoc />
public async Task<bool> PatchCandidateAsync(
async Task<bool> IDiscoveryStore.PatchCandidateAsync(
string dialogId,
DiscoveryCandidatePatch patch,
CancellationToken ct)
@@ -91,7 +92,7 @@ public sealed partial class DiscoveryStore
}
/// <inheritdoc />
public async Task<bool> SetCandidateStatusAsync(
async Task<bool> IDiscoveryStore.SetCandidateStatusAsync(
string dialogId,
string status,
CancellationToken ct)
@@ -110,7 +111,7 @@ public sealed partial class DiscoveryStore
}
/// <inheritdoc />
public async Task<bool> SetCandidateJoinedAsync(
async Task<bool> IDiscoveryStore.SetCandidateJoinedAsync(
string dialogId,
bool autoJoined,
CancellationToken ct)
@@ -130,7 +131,7 @@ public sealed partial class DiscoveryStore
}
/// <inheritdoc />
public async Task<int?> IncrementJoinFailuresAsync(string dialogId, CancellationToken ct)
async Task<int?> 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
}
/// <inheritdoc />
public async Task<bool> SetCandidateRejectedAsync(string dialogId, CancellationToken ct)
async Task<bool> IDiscoveryStore.SetCandidateRejectedAsync(string dialogId, CancellationToken ct)
{
DiscCandidateEntity? row = await _dbContext.DiscCandidates
.FirstOrDefaultAsync(candidate => candidate.DialogId == dialogId, ct);
@@ -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
{
/// <inheritdoc />
public async Task AddLogAsync(
async Task IDiscoveryStore.AddLogAsync(
string logId,
string taskId,
string logEvent,
@@ -29,7 +30,7 @@ public sealed partial class DiscoveryStore
}
/// <inheritdoc />
public async Task<int> CountLogEventAsync(
async Task<int> IDiscoveryStore.CountLogEventAsync(
string logEvent,
DateTimeOffset sinceUtc,
CancellationToken ct)
@@ -38,7 +39,7 @@ public sealed partial class DiscoveryStore
}
/// <inheritdoc />
public async Task<IReadOnlyList<DiscoveryLogDto>> ListTaskLogAsync(
async Task<IReadOnlyList<DiscoveryLogDto>> IDiscoveryStore.ListTaskLogAsync(
string taskId,
int limit,
CancellationToken ct)
@@ -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
{
/// <inheritdoc />
public async Task<IReadOnlyList<DiscoveryTaskDto>> ListTasksAsync(CancellationToken ct)
async Task<IReadOnlyList<DiscoveryTaskDto>> IDiscoveryStore.ListTasksAsync(CancellationToken ct)
{
List<DiscTaskEntity> rows = await _dbContext.DiscTasks
.AsNoTracking()
@@ -20,7 +21,7 @@ public sealed partial class DiscoveryStore
}
/// <inheritdoc />
public async Task<DiscoveryTaskDto?> GetTaskAsync(string taskId, CancellationToken ct)
async Task<DiscoveryTaskDto?> IDiscoveryStore.GetTaskAsync(string taskId, CancellationToken ct)
{
DiscTaskEntity? row = await _dbContext.DiscTasks
.AsNoTracking()
@@ -29,7 +30,7 @@ public sealed partial class DiscoveryStore
}
/// <inheritdoc />
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
}
/// <inheritdoc />
public async Task<bool> PatchTaskAsync(
async Task<bool> IDiscoveryStore.PatchTaskAsync(
string taskId,
DiscoveryTaskPatch patch,
CancellationToken ct)
@@ -71,7 +72,7 @@ public sealed partial class DiscoveryStore
}
/// <inheritdoc />
public async Task<bool> DeleteTaskAsync(string taskId, CancellationToken ct)
async Task<bool> 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
}
/// <inheritdoc />
public async Task<bool> SetTaskRunningAsync(
async Task<bool> IDiscoveryStore.SetTaskRunningAsync(
string taskId,
bool resetProgress,
CancellationToken ct)
@@ -117,7 +118,7 @@ public sealed partial class DiscoveryStore
}
/// <inheritdoc />
public async Task<bool> SetTaskPausedAsync(string taskId, CancellationToken ct)
async Task<bool> 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
}
/// <inheritdoc />
public async Task<bool> SetTaskDoneAsync(string taskId, CancellationToken ct)
async Task<bool> 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
}
/// <inheritdoc />
public async Task<bool> BumpTaskCounterAsync(
async Task<bool> IDiscoveryStore.BumpTaskCounterAsync(
string taskId,
DiscoveryCounterField field,
int n,
@@ -186,7 +187,7 @@ public sealed partial class DiscoveryStore
}
/// <inheritdoc />
public async Task<bool> AdvanceSearchAsync(
async Task<bool> IDiscoveryStore.AdvanceSearchAsync(
string taskId,
int nextIndex,
bool searchDone,
@@ -207,7 +208,7 @@ public sealed partial class DiscoveryStore
}
/// <inheritdoc />
public async Task<int> SumActivePlanAsync(string? excludeTaskId, CancellationToken ct)
async Task<int> IDiscoveryStore.SumActivePlanAsync(string? excludeTaskId, CancellationToken ct)
{
IQueryable<DiscTaskEntity> query = _dbContext.DiscTasks
.Where(task => task.Status != "done" && task.Status != "failed");
@@ -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
{
/// <inheritdoc />
public async Task<IReadOnlyList<CardDto>> ListCardsAsync(CardsQuery query, CancellationToken ct)
async Task<IReadOnlyList<CardDto>> ICardStore.ListCardsAsync(CardsQuery query, CancellationToken ct)
{
IQueryable<CardEntity> queryable = _dbContext.Cards.AsNoTracking();
if (query.Col is null)
@@ -30,7 +31,7 @@ public sealed partial class KanbanStore
}
/// <inheritdoc />
public async Task<IReadOnlyList<CardDto>> SearchCardsAsync(
async Task<IReadOnlyList<CardDto>> ICardStore.SearchCardsAsync(
string q,
int limit,
CancellationToken ct)
@@ -61,7 +62,7 @@ public sealed partial class KanbanStore
}
/// <inheritdoc />
public async Task<CardDto?> GetCardAsync(string cardId, CancellationToken ct)
async Task<CardDto?> ICardStore.GetCardAsync(string cardId, CancellationToken ct)
{
CardEntity? entity = await _dbContext.Cards
.AsNoTracking()
@@ -76,7 +77,7 @@ public sealed partial class KanbanStore
}
/// <inheritdoc />
public async Task<CardDto?> GetCardBySourceAsync(
async Task<CardDto?> ICardStore.GetCardBySourceAsync(
SourceRef source,
CancellationToken ct)
{
@@ -104,7 +105,7 @@ public sealed partial class KanbanStore
}
/// <inheritdoc />
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
}
/// <inheritdoc />
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
}
/// <inheritdoc />
public async Task<bool> ApplyReclassificationAsync(CardReclassificationDto update, CancellationToken ct)
async Task<bool> 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
}
/// <inheritdoc />
public async Task UpdateSeenAsync(
async Task ICardStore.UpdateSeenAsync(
string? cardId,
string? col,
CancellationToken ct)
@@ -201,7 +202,7 @@ public sealed partial class KanbanStore
}
/// <inheritdoc />
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
}
/// <inheritdoc />
public async Task<int> ClearColAsync(string col, CancellationToken ct)
async Task<int> 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
}
/// <inheritdoc />
public async Task<IReadOnlyDictionary<string, CardColumnCountDto>> CountCardsByColAsync(CancellationToken ct)
async Task<IReadOnlyDictionary<string, CardColumnCountDto>> ICardStore.CountCardsByColAsync(CancellationToken ct)
{
var rows = await _dbContext.Cards
.AsNoTracking()
@@ -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
{
/// <inheritdoc />
public async Task<IReadOnlyList<CardCommentDto>> ListCommentsAsync(string cardId, CancellationToken ct)
async Task<IReadOnlyList<CardCommentDto>> ICardStore.ListCommentsAsync(string cardId, CancellationToken ct)
{
List<LeadCommentEntity> entities = await _dbContext.LeadComments
.AsNoTracking()
@@ -23,7 +24,7 @@ public sealed partial class KanbanStore
}
/// <inheritdoc />
public async Task AddCommentAsync(
async Task ICardStore.AddCommentAsync(
string commentId,
string cardId,
string by,
@@ -42,7 +43,7 @@ public sealed partial class KanbanStore
}
/// <inheritdoc />
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<int> CountMovesAsync(CancellationToken ct) => _dbContext.CardMoves.CountAsync(ct);
/// <inheritdoc />
public async Task<IReadOnlyList<AiMarkupExampleDto>> GetAiMarkupExamplesAsync(int limit, CancellationToken ct)
async Task<IReadOnlyList<AiMarkupExampleDto>> ICardStore.GetAiMarkupExamplesAsync(int limit, CancellationToken ct)
{
return await _dbContext.CardMoves
.AsNoTracking()
@@ -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
{
/// <inheritdoc />
public async Task<IReadOnlyList<ContainerDto>> ListContainersAsync(string? space, CancellationToken ct)
async Task<IReadOnlyList<ContainerDto>> ICardStore.ListContainersAsync(string? space, CancellationToken ct)
{
IQueryable<ContainerEntity> queryable = _dbContext.Containers.AsNoTracking();
if (space is not null)
@@ -28,7 +29,7 @@ public sealed partial class KanbanStore
}
/// <inheritdoc />
public async Task<ContainerDto?> GetContainerAsync(string containerId, CancellationToken ct)
async Task<ContainerDto?> ICardStore.GetContainerAsync(string containerId, CancellationToken ct)
{
ContainerEntity? entity = await _dbContext.Containers
.AsNoTracking()
@@ -37,14 +38,14 @@ public sealed partial class KanbanStore
}
/// <inheritdoc />
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);
}
/// <inheritdoc />
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
}
/// <inheritdoc />
public async Task<int> DeleteContainerAsync(string containerId, CancellationToken ct)
async Task<int> ICardStore.DeleteContainerAsync(string containerId, CancellationToken ct)
{
// Два изменения разных таблиц — в одной транзакции: либо карточки ушли в «Неразобранное» и контейнер
// удалён, либо ничего не изменилось.
@@ -84,7 +85,7 @@ public sealed partial class KanbanStore
}
/// <inheritdoc />
public async Task ReorderContainersAsync(
async Task ICardStore.ReorderContainersAsync(
string space,
IReadOnlyList<string> containerIds,
CancellationToken ct)
@@ -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
{
/// <inheritdoc />
public async Task<IReadOnlyList<CardDto>> ListSelectedCardsAsync(string? containerId, CancellationToken ct)
async Task<IReadOnlyList<CardDto>> ICardStore.ListSelectedCardsAsync(string? containerId, CancellationToken ct)
{
IQueryable<CardEntity> queryable = _dbContext.Cards.AsNoTracking();
if (containerId is not null)
@@ -29,7 +30,7 @@ public sealed partial class KanbanStore
}
/// <inheritdoc />
public async Task<bool> PatchCardAsync(
async Task<bool> ICardStore.PatchCardAsync(
string cardId,
CardPatch patch,
CancellationToken ct)
@@ -68,7 +69,7 @@ public sealed partial class KanbanStore
}
/// <inheritdoc />
public async Task<bool> AddLinkAsync(
async Task<bool> ICardStore.AddLinkAsync(
string cardId,
CardLinkDto link,
CancellationToken ct)
@@ -85,7 +86,7 @@ public sealed partial class KanbanStore
}
/// <inheritdoc />
public async Task<bool> RemoveLinkAsync(
async Task<bool> ICardStore.RemoveLinkAsync(
string cardId,
string linkId,
CancellationToken ct)
@@ -106,7 +107,7 @@ public sealed partial class KanbanStore
}
/// <inheritdoc />
public async Task<bool> AddFileAsync(
async Task<bool> ICardStore.AddFileAsync(
string cardId,
CardFileDto file,
CancellationToken ct)
@@ -123,7 +124,7 @@ public sealed partial class KanbanStore
}
/// <inheritdoc />
public async Task<bool> RemoveFileAsync(
async Task<bool> ICardStore.RemoveFileAsync(
string cardId,
string fileId,
CancellationToken ct)
@@ -144,7 +145,7 @@ public sealed partial class KanbanStore
}
/// <inheritdoc />
public async Task<bool> MoveCardStageAsync(
async Task<bool> ICardStore.MoveCardStageAsync(
string cardId,
string containerId,
CardHistoryDto historyEntry,
@@ -176,7 +177,7 @@ public sealed partial class KanbanStore
}
/// <inheritdoc />
public async Task SetReminderAsync(
async Task ICardStore.SetReminderAsync(
string cardId,
long atMs,
CancellationToken ct)
@@ -191,7 +192,7 @@ public sealed partial class KanbanStore
}
/// <inheritdoc />
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
}
/// <inheritdoc />
public async Task<int> ClearStageAsync(string containerId, CancellationToken ct)
async Task<int> ICardStore.ClearStageAsync(string containerId, CancellationToken ct)
{
return await _dbContext.Cards
.Where(card => card.Col == containerId)
@@ -210,7 +211,7 @@ public sealed partial class KanbanStore
}
/// <inheritdoc />
public async Task<IReadOnlyList<CardReminderDueDto>> ListDueRemindersAsync(DateTimeOffset now, CancellationToken ct)
async Task<IReadOnlyList<CardReminderDueDto>> ICardStore.ListDueRemindersAsync(DateTimeOffset now, CancellationToken ct)
{
return await _dbContext.Cards
.AsNoTracking()
@@ -224,7 +225,7 @@ public sealed partial class KanbanStore
}
/// <inheritdoc />
public async Task MarkRemindersFiredAsync(IReadOnlyList<string> cardIds, CancellationToken ct)
async Task ICardStore.MarkRemindersFiredAsync(IReadOnlyList<string> cardIds, CancellationToken ct)
{
if (cardIds.Count == 0)
{
@@ -237,7 +238,7 @@ public sealed partial class KanbanStore
}
/// <inheritdoc />
public async Task<int> ClearExpiredRemindersAsync(DateTimeOffset now, CancellationToken ct)
async Task<int> ICardStore.ClearExpiredRemindersAsync(DateTimeOffset now, CancellationToken ct)
{
return await _dbContext.Cards
.Where(card => card.ReminderAt != null && card.ReminderAt <= now)
@@ -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
{
/// <inheritdoc />
public async Task<IReadOnlyList<string>> ListArchiveCandidatesAsync(DateTimeOffset receivedBeforeUtc, CancellationToken ct)
async Task<IReadOnlyList<string>> ICardStore.ListArchiveCandidatesAsync(DateTimeOffset receivedBeforeUtc, CancellationToken ct)
{
List<string> boardIds = await _dbContext.Containers
.AsNoTracking()
@@ -28,7 +29,7 @@ public sealed partial class KanbanStore
}
/// <inheritdoc />
public async Task<int> ArchiveAsync(
async Task<int> ICardStore.ArchiveAsync(
IReadOnlyList<string> cardIds,
DateTimeOffset archivedAt,
CancellationToken ct)
@@ -49,7 +50,7 @@ public sealed partial class KanbanStore
}
/// <inheritdoc />
public async Task<IReadOnlyList<string>> ListExpiredArchiveCandidatesAsync(DateTimeOffset archivedBeforeUtc, CancellationToken ct)
async Task<IReadOnlyList<string>> ICardStore.ListExpiredArchiveCandidatesAsync(DateTimeOffset archivedBeforeUtc, CancellationToken ct)
{
return await _dbContext.Cards
.AsNoTracking()
@@ -61,7 +62,7 @@ public sealed partial class KanbanStore
}
/// <inheritdoc />
public async Task<IReadOnlyList<string>> ListTrashCandidatesAsync(DateTimeOffset receivedBeforeUtc, CancellationToken ct)
async Task<IReadOnlyList<string>> ICardStore.ListTrashCandidatesAsync(DateTimeOffset receivedBeforeUtc, CancellationToken ct)
{
return await _dbContext.Cards
.AsNoTracking()
@@ -71,7 +72,7 @@ public sealed partial class KanbanStore
}
/// <inheritdoc />
public async Task<int> PurgeAsync(IReadOnlyList<string> cardIds, CancellationToken ct)
async Task<int> ICardStore.PurgeAsync(IReadOnlyList<string> cardIds, CancellationToken ct)
{
if (cardIds.Count == 0)
{
@@ -90,7 +91,7 @@ public sealed partial class KanbanStore
}
/// <inheritdoc />
public async Task<IReadOnlyList<CardDto>> ListCardsForConversionAsync(CancellationToken ct)
async Task<IReadOnlyList<CardDto>> ICardStore.ListCardsForConversionAsync(CancellationToken ct)
{
List<CardEntity> entities = await _dbContext.Cards
.AsNoTracking()
@@ -102,7 +103,7 @@ public sealed partial class KanbanStore
}
/// <inheritdoc />
public async Task UpdateConversionAsync(
async Task ICardStore.UpdateConversionAsync(
string cardId,
double? convFrom,
double? convTo,
@@ -119,7 +120,7 @@ public sealed partial class KanbanStore
}
/// <inheritdoc />
public async Task<IReadOnlyList<CardDto>> ListInboxWithSourceAsync(CancellationToken ct)
async Task<IReadOnlyList<CardDto>> ICardStore.ListInboxWithSourceAsync(CancellationToken ct)
{
List<CardEntity> entities = await _dbContext.Cards
.AsNoTracking()
@@ -59,17 +59,17 @@ public sealed class TenantLimitStore : ITenantLimitStore
}
/// <inheritdoc />
public async Task<TenantLimitDto> GetOrCreateAsync(
async Task<TenantLimitDto> ITenantLimitStore.GetOrCreateAsync(
Guid tenantId,
CancellationToken ct,
TokenLimitDefaults? defaults = null)
TokenLimitDefaults? defaults)
{
TenantLimitEntity entity = await LoadOrCreateAsync(tenantId, defaults ?? _defaults, ct);
return ToLimitDto(entity);
}
/// <inheritdoc />
public async Task<BudgetStateDto> GetStateAsync(Guid tenantId, CancellationToken ct)
async Task<BudgetStateDto> 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
}
/// <inheritdoc />
public async Task<BudgetStateDto> AddUsageAsync(
async Task<BudgetStateDto> ITenantLimitStore.AddUsageAsync(
Guid tenantId,
long tokens,
CancellationToken ct)
@@ -110,7 +110,7 @@ public sealed class TenantLimitStore : ITenantLimitStore
}
/// <inheritdoc />
public async Task<BudgetStateDto> UpdateBudgetAsync(
async Task<BudgetStateDto> ITenantLimitStore.UpdateBudgetAsync(
Guid tenantId,
long budgetTokens,
string period,
@@ -133,7 +133,7 @@ public sealed class TenantLimitStore : ITenantLimitStore
}
/// <inheritdoc />
public async Task<bool> TryMarkWarnedAsync(Guid tenantId, CancellationToken ct)
async Task<bool> 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
}
/// <inheritdoc />
public async Task<bool> TryMarkNotifiedExhaustedAsync(Guid tenantId, CancellationToken ct)
async Task<bool> 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
}
/// <inheritdoc />
public async Task<int> ResetExpiredPeriodsAsync(DateTimeOffset now, CancellationToken ct)
async Task<int> ITenantLimitStore.ResetExpiredPeriodsAsync(DateTimeOffset now, CancellationToken ct)
{
// Трогаем только строки с накоплениями (расход/флаги): строки без накоплений чистить нечего.
List<TenantLimitEntity> candidates = await _dbContext.TenantLimits
@@ -40,7 +40,7 @@ public sealed class AesGcmSecretCipher : ISecretCipher
}
/// <inheritdoc />
public string Encrypt(string plainText)
string ISecretCipher.Encrypt(string plainText)
{
if (string.IsNullOrEmpty(plainText))
{
@@ -66,7 +66,7 @@ public sealed class AesGcmSecretCipher : ISecretCipher
}
/// <inheritdoc />
public string Decrypt(string cipherText)
string ISecretCipher.Decrypt(string cipherText)
{
if (string.IsNullOrEmpty(cipherText) || !cipherText.StartsWith(EncryptedPrefix, StringComparison.Ordinal))
{
@@ -44,7 +44,7 @@ public sealed class DiscoverySearchErrorCounter : IDiscoverySearchErrorCounter
}
/// <inheritdoc />
public int Next(string taskId)
int IDiscoverySearchErrorCounter.Next(string taskId)
{
EvictExpired();
Entry fresh = _failures.AddOrUpdate(
@@ -56,7 +56,7 @@ public sealed class DiscoverySearchErrorCounter : IDiscoverySearchErrorCounter
}
/// <inheritdoc />
public void Reset(string taskId)
void IDiscoverySearchErrorCounter.Reset(string taskId)
{
EvictExpired();
_failures.TryRemove(taskId, out _);
@@ -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,
@@ -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,
@@ -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<AiUnavailableException>(
() => 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(
@@ -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(
@@ -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<GrpcTelegramClient>.Instance);
await Assert.ThrowsAsync<InvalidOperationException>(() => 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
{
@@ -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<ITenantContext>(tenantContext);
services.AddScoped<ISettingsStore>(_ => new FakeSettingsStore());
@@ -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;
/// </summary>
public sealed class LocalAiToolsTests
{
private readonly LocalAiTools _tools = new();
private readonly IAiTools _tools = new LocalAiTools();
[Fact]
public async Task GenerateKeywordsAsync_AlwaysThrowsNotSupported()
@@ -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}" });
@@ -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<ArgumentOutOfRangeException>(
@@ -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<DateTimeOffset>? clock = null)
private static (DealDbContext Db, ITenantLimitStore Store) CreateStore(Func<DateTimeOffset>? clock = null)
{
var options = new DbContextOptionsBuilder<DealDbContext>()
.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);
}
@@ -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"); // свежая
@@ -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<HttpResponseMessage>(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 не выполняется вовсе.
@@ -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<string, double>? 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<string, double>? 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<string, double>? rates = await source.FetchAsync(CancellationToken.None);
@@ -64,7 +65,7 @@ public sealed class CbrRateSourceTests
{
StubHttpMessageHandler handler = new((_, _) =>
Task.FromException<HttpResponseMessage>(new HttpRequestException("Connection refused")));
CbrRateSource source = CreateSource(handler);
IRatesSource source = CreateSource(handler);
Dictionary<string, double>? 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<string, double>? 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<string, double>? 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<string, double>? 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<string, double>? rates = await source.FetchAsync(CancellationToken.None);
@@ -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<AiUnavailableException>(
() => 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<AiUnavailableException>(
@@ -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<AiUnavailableException>(
() => 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(
@@ -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()
{
@@ -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);
@@ -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<DialogEntry>
{
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<SessionException>(
() => client.PushSourceAsync(
@@ -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
}
/// <inheritdoc />
public async Task<PushSourceReply> PushSourceAsync(
async Task<PushSourceReply> ICoreIngressClient.PushSourceAsync(
string tenantId,
PushSourceRequest request,
CancellationToken cancellationToken)
@@ -61,7 +62,7 @@ public sealed class CoreIngressClient : ICoreIngressClient
}
/// <inheritdoc />
public async Task<IReadOnlyList<string>> SyncDialogsAsync(
async Task<IReadOnlyList<string>> ICoreIngressClient.SyncDialogsAsync(
string tenantId,
IReadOnlyList<DialogEntry> entries,
CancellationToken cancellationToken)
@@ -1,3 +1,4 @@
using Deal.Telegram.Dialogs;
namespace Deal.Telegram.Dialogs;
/// <summary>
@@ -6,7 +7,7 @@ namespace Deal.Telegram.Dialogs;
public sealed class RandomBackfillPacer : IBackfillPacer
{
/// <inheritdoc />
public async Task WaitAsync(
async Task IBackfillPacer.WaitAsync(
double minSeconds,
double maxSeconds,
CancellationToken cancellationToken)
@@ -1,3 +1,4 @@
using Deal.Telegram.Telegram;
namespace Deal.Telegram.Telegram;
/// <summary>
@@ -6,7 +7,7 @@ namespace Deal.Telegram.Telegram;
public sealed class ClientFactory : ITelegramClientFactory
{
/// <inheritdoc />
public ISessionClient Create(
ISessionClient ITelegramClientFactory.Create(
int apiId,
string apiHash,
byte[]? storedSession)
@@ -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);
/// <inheritdoc />
public async Task ConnectAsync(CancellationToken cancellationToken)
async Task ISessionClient.ConnectAsync(CancellationToken cancellationToken)
{
if (IsConnected)
{
@@ -94,7 +95,7 @@ public sealed class WTelegramSessionClient : ISessionClient
}
/// <inheritdoc />
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
}
/// <inheritdoc />
public async Task<string?> SubmitCodeAsync(string code, CancellationToken cancellationToken)
async Task<string?> ISessionClient.SubmitCodeAsync(string code, CancellationToken cancellationToken)
{
if (_phoneAlreadyAuthorized)
{
@@ -158,7 +159,7 @@ public sealed class WTelegramSessionClient : ISessionClient
}
/// <inheritdoc />
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
}
/// <inheritdoc />
public async Task StartQrAsync(Action<string> onQrUrl, CancellationToken cancellationToken)
async Task ISessionClient.StartQrAsync(Action<string> onQrUrl, CancellationToken cancellationToken)
{
try
{
@@ -196,13 +197,13 @@ public sealed class WTelegramSessionClient : ISessionClient
}
/// <inheritdoc />
public async Task LogOutAsync(CancellationToken cancellationToken)
async Task ISessionClient.LogOutAsync(CancellationToken cancellationToken)
{
await _client.Auth_LogOut().WaitAsync(cancellationToken).ConfigureAwait(false);
}
/// <inheritdoc />
public async Task<string> GetAccountAsync(CancellationToken cancellationToken)
async Task<string> ISessionClient.GetAccountAsync(CancellationToken cancellationToken)
{
UserBase[] users = await _client.Users_GetUsers(InputUser.Self).WaitAsync(cancellationToken).ConfigureAwait(false);
string username = users.OfType<User>().FirstOrDefault()?.username ?? string.Empty;
@@ -221,7 +222,7 @@ public sealed class WTelegramSessionClient : ISessionClient
public event Func<TelegramMessage, Task>? MessageReceived;
/// <inheritdoc />
public async Task<IReadOnlyList<TelegramDialog>> GetDialogsAsync(int limit, CancellationToken cancellationToken)
async Task<IReadOnlyList<TelegramDialog>> ISessionClient.GetDialogsAsync(int limit, CancellationToken cancellationToken)
{
Messages_DialogsBase result = await RunTlCallAsync(() => _client.Messages_GetDialogs(limit: limit), cancellationToken).ConfigureAwait(false);
(DialogBase[] dialogs, Dictionary<long, ChatBase> chats, Dictionary<long, User> users) = UnpackDialogs(result);
@@ -241,7 +242,7 @@ public sealed class WTelegramSessionClient : ISessionClient
}
/// <inheritdoc />
public async Task<IReadOnlyList<TelegramMessage>> GetMessagesAsync(
async Task<IReadOnlyList<TelegramMessage>> ISessionClient.GetMessagesAsync(
string dialogId,
int limit,
CancellationToken cancellationToken)
@@ -265,7 +266,7 @@ public sealed class WTelegramSessionClient : ISessionClient
}
/// <inheritdoc />
public async Task<TelegramMessage?> GetMessageAsync(
async Task<TelegramMessage?> ISessionClient.GetMessageAsync(
string dialogId,
long msgId,
CancellationToken cancellationToken)
@@ -295,7 +296,7 @@ public sealed class WTelegramSessionClient : ISessionClient
}
/// <inheritdoc />
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
/// <inheritdoc />
public async Task<IReadOnlyList<TelegramDialog>> SearchAsync(
async Task<IReadOnlyList<TelegramDialog>> ISessionClient.SearchAsync(
string query,
int limit,
CancellationToken cancellationToken)
@@ -327,7 +328,7 @@ public sealed class WTelegramSessionClient : ISessionClient
}
/// <inheritdoc />
public async Task<TelegramSourceInfo> GetInfoAsync(string dialogId, CancellationToken cancellationToken)
async Task<TelegramSourceInfo> 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
}
/// <inheritdoc />
public async Task<DiscoveryReadResult> ReadForEvalAsync(
async Task<DiscoveryReadResult> ISessionClient.ReadForEvalAsync(
string dialogId,
int limit,
CancellationToken cancellationToken)
@@ -410,7 +411,7 @@ public sealed class WTelegramSessionClient : ISessionClient
}
/// <inheritdoc />
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
}
/// <inheritdoc />
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)