Перевести реализации интерфейсов на явные (§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:
@@ -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);
|
||||
|
||||
@@ -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()
|
||||
|
||||
+12
-11
@@ -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);
|
||||
}
|
||||
|
||||
+5
-4
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user