Перевести реализации интерфейсов на явные (§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()
|
public async Task ChatAsync_OpenAiStyle_BuildsWireRequest()
|
||||||
{
|
{
|
||||||
var handler = new StubHttpMessageHandler(StubHttpMessageHandler.JsonOk(OpenAiJsonReply));
|
var handler = new StubHttpMessageHandler(StubHttpMessageHandler.JsonOk(OpenAiJsonReply));
|
||||||
LlmHttpClient client = CreateClient(handler);
|
IProviderClient client = CreateClient(handler);
|
||||||
|
|
||||||
ProviderChatResult result = await client.ChatAsync(
|
ProviderChatResult result = await client.ChatAsync(
|
||||||
OpenAiConfig(apiKey: "secret-key"),
|
OpenAiConfig(apiKey: "secret-key"),
|
||||||
@@ -87,7 +87,7 @@ public sealed class LlmHttpClientTests
|
|||||||
public async Task ChatAsync_OpenAiStyleWithoutApiKey_SkipsAuthorization()
|
public async Task ChatAsync_OpenAiStyleWithoutApiKey_SkipsAuthorization()
|
||||||
{
|
{
|
||||||
var handler = new StubHttpMessageHandler(StubHttpMessageHandler.JsonOk(OpenAiJsonReply));
|
var handler = new StubHttpMessageHandler(StubHttpMessageHandler.JsonOk(OpenAiJsonReply));
|
||||||
LlmHttpClient client = CreateClient(handler);
|
IProviderClient client = CreateClient(handler);
|
||||||
|
|
||||||
await client.ChatAsync(OpenAiConfig(apiKey: null), "Система", "Сообщение", CancellationToken.None);
|
await client.ChatAsync(OpenAiConfig(apiKey: null), "Система", "Сообщение", CancellationToken.None);
|
||||||
|
|
||||||
@@ -103,7 +103,7 @@ public sealed class LlmHttpClientTests
|
|||||||
const string reasoningOnlyReply =
|
const string reasoningOnlyReply =
|
||||||
"""{ "choices": [ { "message": { "role": "assistant", "reasoning_content": "хм, подумаю" } } ] }""";
|
"""{ "choices": [ { "message": { "role": "assistant", "reasoning_content": "хм, подумаю" } } ] }""";
|
||||||
var handler = new StubHttpMessageHandler(StubHttpMessageHandler.JsonOk(reasoningOnlyReply));
|
var handler = new StubHttpMessageHandler(StubHttpMessageHandler.JsonOk(reasoningOnlyReply));
|
||||||
LlmHttpClient client = CreateClient(handler);
|
IProviderClient client = CreateClient(handler);
|
||||||
|
|
||||||
LlmHttpException exception = await Assert.ThrowsAsync<LlmHttpException>(() =>
|
LlmHttpException exception = await Assert.ThrowsAsync<LlmHttpException>(() =>
|
||||||
client.ChatAsync(OpenAiConfig(), "Система", "Сообщение", CancellationToken.None));
|
client.ChatAsync(OpenAiConfig(), "Система", "Сообщение", CancellationToken.None));
|
||||||
@@ -118,7 +118,7 @@ public sealed class LlmHttpClientTests
|
|||||||
public async Task ChatAsync_AnthropicStyle_BuildsWireRequest()
|
public async Task ChatAsync_AnthropicStyle_BuildsWireRequest()
|
||||||
{
|
{
|
||||||
var handler = new StubHttpMessageHandler(StubHttpMessageHandler.JsonOk(AnthropicJsonReply));
|
var handler = new StubHttpMessageHandler(StubHttpMessageHandler.JsonOk(AnthropicJsonReply));
|
||||||
LlmHttpClient client = CreateClient(handler);
|
IProviderClient client = CreateClient(handler);
|
||||||
|
|
||||||
ProviderChatResult result = await client.ChatAsync(
|
ProviderChatResult result = await client.ChatAsync(
|
||||||
AnthropicConfig(),
|
AnthropicConfig(),
|
||||||
@@ -153,7 +153,7 @@ public sealed class LlmHttpClientTests
|
|||||||
public async Task ChatAsync_HttpError_Throws()
|
public async Task ChatAsync_HttpError_Throws()
|
||||||
{
|
{
|
||||||
var handler = new StubHttpMessageHandler(StubHttpMessageHandler.Status(HttpStatusCode.InternalServerError));
|
var handler = new StubHttpMessageHandler(StubHttpMessageHandler.Status(HttpStatusCode.InternalServerError));
|
||||||
LlmHttpClient client = CreateClient(handler);
|
IProviderClient client = CreateClient(handler);
|
||||||
|
|
||||||
LlmHttpException exception = await Assert.ThrowsAsync<LlmHttpException>(() =>
|
LlmHttpException exception = await Assert.ThrowsAsync<LlmHttpException>(() =>
|
||||||
client.ChatAsync(OpenAiConfig(), "Система", "Сообщение", CancellationToken.None));
|
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"),
|
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>(() =>
|
await Assert.ThrowsAsync<LlmHttpException>(() =>
|
||||||
client.ChatAsync(OpenAiConfig(), "Система", "Сообщение", CancellationToken.None));
|
client.ChatAsync(OpenAiConfig(), "Система", "Сообщение", CancellationToken.None));
|
||||||
@@ -186,7 +186,7 @@ public sealed class LlmHttpClientTests
|
|||||||
var handler = new StubHttpMessageHandler(
|
var handler = new StubHttpMessageHandler(
|
||||||
StubHttpMessageHandler.JsonOk(OpenAiJsonReply),
|
StubHttpMessageHandler.JsonOk(OpenAiJsonReply),
|
||||||
delay: TestHandlerDelay);
|
delay: TestHandlerDelay);
|
||||||
LlmHttpClient client = new(
|
IProviderClient client = new LlmHttpClient(
|
||||||
TestHttpClient(handler),
|
TestHttpClient(handler),
|
||||||
TestCallTimeout,
|
TestCallTimeout,
|
||||||
TestCallTimeout);
|
TestCallTimeout);
|
||||||
|
|||||||
@@ -65,7 +65,7 @@ public sealed class LlmHttpClient : IProviderClient
|
|||||||
/// <param name="systemPrompt">Системный промпт.</param>
|
/// <param name="systemPrompt">Системный промпт.</param>
|
||||||
/// <param name="userText">Пользовательское сообщение/контекст.</param>
|
/// <param name="userText">Пользовательское сообщение/контекст.</param>
|
||||||
/// <returns>Текст ответа и usage API-ответа (null при его отсутствии).</returns>
|
/// <returns>Текст ответа и usage API-ответа (null при его отсутствии).</returns>
|
||||||
public async Task<ProviderChatResult> ChatAsync(
|
async Task<ProviderChatResult> IProviderClient.ChatAsync(
|
||||||
LlmConfig config,
|
LlmConfig config,
|
||||||
string systemPrompt,
|
string systemPrompt,
|
||||||
string userText,
|
string userText,
|
||||||
|
|||||||
@@ -17,8 +17,8 @@ public sealed class TenantContext : ITenantContext
|
|||||||
public string? SchemaName => Current.Value?.SchemaName;
|
public string? SchemaName => Current.Value?.SchemaName;
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public void SetTenant(TenantId tenantId) => Current.Value = tenantId;
|
void ITenantContext.SetTenant(TenantId tenantId) => Current.Value = tenantId;
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public void Reset() => Current.Value = null;
|
void ITenantContext.Reset() => Current.Value = null;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -79,7 +79,7 @@ public sealed class AiConnectionChecker : IAiConnectionChecker
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public async Task<AiCheckResultDto> CheckAsync(AiCheckRequest request, CancellationToken ct)
|
async Task<AiCheckResultDto> IAiConnectionChecker.CheckAsync(AiCheckRequest request, CancellationToken ct)
|
||||||
{
|
{
|
||||||
ArgumentNullException.ThrowIfNull(request);
|
ArgumentNullException.ThrowIfNull(request);
|
||||||
|
|
||||||
|
|||||||
@@ -55,7 +55,7 @@ public sealed class BudgetedAiClassifier : IAiClassifier
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public async Task<AiFilterResultDto> FilterAsync(string text, CancellationToken ct)
|
async Task<AiFilterResultDto> IAiClassifier.FilterAsync(string text, CancellationToken ct)
|
||||||
{
|
{
|
||||||
if (await IsPaidAllowedAsync(ct))
|
if (await IsPaidAllowedAsync(ct))
|
||||||
{
|
{
|
||||||
@@ -68,7 +68,7 @@ public sealed class BudgetedAiClassifier : IAiClassifier
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public async Task<AiParsedCardDto> ClassifyAsync(string text, CancellationToken ct)
|
async Task<AiParsedCardDto> IAiClassifier.ClassifyAsync(string text, CancellationToken ct)
|
||||||
{
|
{
|
||||||
if (await IsPaidAllowedAsync(ct))
|
if (await IsPaidAllowedAsync(ct))
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -51,7 +51,7 @@ public sealed class BudgetedAiTools : IAiTools
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public async Task<AiGenerateKeywordsResultDto> GenerateKeywordsAsync(string description, CancellationToken ct)
|
async Task<AiGenerateKeywordsResultDto> IAiTools.GenerateKeywordsAsync(string description, CancellationToken ct)
|
||||||
{
|
{
|
||||||
BudgetStateDto state = await GateStateAsync(ct);
|
BudgetStateDto state = await GateStateAsync(ct);
|
||||||
if (state.Allowed)
|
if (state.Allowed)
|
||||||
@@ -70,7 +70,7 @@ public sealed class BudgetedAiTools : IAiTools
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public async Task<AiEvaluateFitResultDto> EvaluateFitAsync(
|
async Task<AiEvaluateFitResultDto> IAiTools.EvaluateFitAsync(
|
||||||
string text,
|
string text,
|
||||||
string description,
|
string description,
|
||||||
IReadOnlyCollection<string> keywords,
|
IReadOnlyCollection<string> keywords,
|
||||||
|
|||||||
@@ -48,7 +48,7 @@ public sealed class CbrRateSource : IRatesSource
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public async Task<Dictionary<string, double>?> FetchAsync(CancellationToken ct)
|
async Task<Dictionary<string, double>?> IRatesSource.FetchAsync(CancellationToken ct)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -71,7 +71,7 @@ public sealed class GrpcAiClassifier : IAiClassifier
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public async Task<AiFilterResultDto> FilterAsync(string text, CancellationToken ct)
|
async Task<AiFilterResultDto> IAiClassifier.FilterAsync(string text, CancellationToken ct)
|
||||||
{
|
{
|
||||||
TenantId tenantId = RequireTenant();
|
TenantId tenantId = RequireTenant();
|
||||||
try
|
try
|
||||||
@@ -107,7 +107,7 @@ public sealed class GrpcAiClassifier : IAiClassifier
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public async Task<AiParsedCardDto> ClassifyAsync(string text, CancellationToken ct)
|
async Task<AiParsedCardDto> IAiClassifier.ClassifyAsync(string text, CancellationToken ct)
|
||||||
{
|
{
|
||||||
TenantId tenantId = RequireTenant();
|
TenantId tenantId = RequireTenant();
|
||||||
string systemPrompt = await _contextBuilder.BuildClassifySystemPromptAsync(ct);
|
string systemPrompt = await _contextBuilder.BuildClassifySystemPromptAsync(ct);
|
||||||
|
|||||||
@@ -71,7 +71,7 @@ public sealed class GrpcAiTools : IAiTools
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public async Task<AiGenerateKeywordsResultDto> GenerateKeywordsAsync(string description, CancellationToken ct)
|
async Task<AiGenerateKeywordsResultDto> IAiTools.GenerateKeywordsAsync(string description, CancellationToken ct)
|
||||||
{
|
{
|
||||||
TenantId tenantId = RequireTenant();
|
TenantId tenantId = RequireTenant();
|
||||||
try
|
try
|
||||||
@@ -105,7 +105,7 @@ public sealed class GrpcAiTools : IAiTools
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public async Task<AiEvaluateFitResultDto> EvaluateFitAsync(
|
async Task<AiEvaluateFitResultDto> IAiTools.EvaluateFitAsync(
|
||||||
string text,
|
string text,
|
||||||
string description,
|
string description,
|
||||||
IReadOnlyCollection<string> keywords,
|
IReadOnlyCollection<string> keywords,
|
||||||
|
|||||||
@@ -98,7 +98,7 @@ public sealed class GrpcMlClient : IMlClient, IMlTrainClient
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public async Task<MlStatusResponseDto> StatusAsync(CancellationToken ct)
|
async Task<MlStatusResponseDto> IMlClient.StatusAsync(CancellationToken ct)
|
||||||
{
|
{
|
||||||
TenantId tenantId = RequireTenant();
|
TenantId tenantId = RequireTenant();
|
||||||
MlStatusCache.Snapshot snapshot = await GetServiceSnapshotAsync(tenantId, ct);
|
MlStatusCache.Snapshot snapshot = await GetServiceSnapshotAsync(tenantId, ct);
|
||||||
@@ -123,7 +123,7 @@ public sealed class GrpcMlClient : IMlClient, IMlTrainClient
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public async Task<MlPredictResultDto> PredictAsync(string text, CancellationToken ct)
|
async Task<MlPredictResultDto> IMlClient.PredictAsync(string text, CancellationToken ct)
|
||||||
{
|
{
|
||||||
TenantId tenantId = RequireTenant();
|
TenantId tenantId = RequireTenant();
|
||||||
try
|
try
|
||||||
@@ -144,7 +144,7 @@ public sealed class GrpcMlClient : IMlClient, IMlTrainClient
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public async Task<MlResetResultDto> ResetAsync(CancellationToken ct)
|
async Task<MlResetResultDto> IMlClient.ResetAsync(CancellationToken ct)
|
||||||
{
|
{
|
||||||
TenantId tenantId = RequireTenant();
|
TenantId tenantId = RequireTenant();
|
||||||
ResetReply reply;
|
ResetReply reply;
|
||||||
@@ -172,7 +172,7 @@ public sealed class GrpcMlClient : IMlClient, IMlTrainClient
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public async Task PushAsync(
|
async Task IMlClient.PushAsync(
|
||||||
string text,
|
string text,
|
||||||
string label,
|
string label,
|
||||||
double delta,
|
double delta,
|
||||||
@@ -182,7 +182,7 @@ public sealed class GrpcMlClient : IMlClient, IMlTrainClient
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public async Task<int> TrainBatchAsync(IReadOnlyList<MlOutboxEntryDto> items, CancellationToken ct)
|
async Task<int> IMlTrainClient.TrainBatchAsync(IReadOnlyList<MlOutboxEntryDto> items, CancellationToken ct)
|
||||||
{
|
{
|
||||||
TenantId tenantId = RequireTenant();
|
TenantId tenantId = RequireTenant();
|
||||||
var request = new TrainBatchRequest();
|
var request = new TrainBatchRequest();
|
||||||
|
|||||||
@@ -59,7 +59,7 @@ public sealed class GrpcTelegramClient : ITelegramGateway
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public async Task<TelegramAccountStatusDto> StatusAsync(CancellationToken ct)
|
async Task<TelegramAccountStatusDto> ITelegramGateway.StatusAsync(CancellationToken ct)
|
||||||
{
|
{
|
||||||
TenantId tenantId = RequireTenant();
|
TenantId tenantId = RequireTenant();
|
||||||
try
|
try
|
||||||
@@ -82,7 +82,7 @@ public sealed class GrpcTelegramClient : ITelegramGateway
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public async Task<TelegramAuthResultDto> StartPhoneAsync(
|
async Task<TelegramAuthResultDto> ITelegramGateway.StartPhoneAsync(
|
||||||
string phone,
|
string phone,
|
||||||
int apiId,
|
int apiId,
|
||||||
string apiHash,
|
string apiHash,
|
||||||
@@ -105,7 +105,7 @@ public sealed class GrpcTelegramClient : ITelegramGateway
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public async Task<TelegramAuthResultDto> StartQrAsync(
|
async Task<TelegramAuthResultDto> ITelegramGateway.StartQrAsync(
|
||||||
int apiId,
|
int apiId,
|
||||||
string apiHash,
|
string apiHash,
|
||||||
CancellationToken ct)
|
CancellationToken ct)
|
||||||
@@ -129,7 +129,7 @@ public sealed class GrpcTelegramClient : ITelegramGateway
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public async Task<string> SendCodeAsync(string code, CancellationToken ct)
|
async Task<string> ITelegramGateway.SendCodeAsync(string code, CancellationToken ct)
|
||||||
{
|
{
|
||||||
TenantId tenantId = RequireTenant();
|
TenantId tenantId = RequireTenant();
|
||||||
try
|
try
|
||||||
@@ -147,7 +147,7 @@ public sealed class GrpcTelegramClient : ITelegramGateway
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public async Task<string> SendPasswordAsync(string password, CancellationToken ct)
|
async Task<string> ITelegramGateway.SendPasswordAsync(string password, CancellationToken ct)
|
||||||
{
|
{
|
||||||
TenantId tenantId = RequireTenant();
|
TenantId tenantId = RequireTenant();
|
||||||
try
|
try
|
||||||
@@ -165,7 +165,7 @@ public sealed class GrpcTelegramClient : ITelegramGateway
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public async Task LogoutAsync(CancellationToken ct)
|
async Task ITelegramGateway.LogoutAsync(CancellationToken ct)
|
||||||
{
|
{
|
||||||
TenantId tenantId = RequireTenant();
|
TenantId tenantId = RequireTenant();
|
||||||
try
|
try
|
||||||
@@ -181,7 +181,7 @@ public sealed class GrpcTelegramClient : ITelegramGateway
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public async Task<IReadOnlyList<TelegramDialogEntryDto>> RefreshDialogsAsync(CancellationToken ct)
|
async Task<IReadOnlyList<TelegramDialogEntryDto>> ITelegramGateway.RefreshDialogsAsync(CancellationToken ct)
|
||||||
{
|
{
|
||||||
TenantId tenantId = RequireTenant();
|
TenantId tenantId = RequireTenant();
|
||||||
try
|
try
|
||||||
@@ -198,7 +198,7 @@ public sealed class GrpcTelegramClient : ITelegramGateway
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public async Task SetMonitorAsync(
|
async Task ITelegramGateway.SetMonitorAsync(
|
||||||
string dialogId,
|
string dialogId,
|
||||||
bool enabled,
|
bool enabled,
|
||||||
CancellationToken ct)
|
CancellationToken ct)
|
||||||
@@ -218,7 +218,7 @@ public sealed class GrpcTelegramClient : ITelegramGateway
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public async Task SetMonitorAllAsync(bool enabled, CancellationToken ct)
|
async Task ITelegramGateway.SetMonitorAllAsync(bool enabled, CancellationToken ct)
|
||||||
{
|
{
|
||||||
TenantId tenantId = RequireTenant();
|
TenantId tenantId = RequireTenant();
|
||||||
try
|
try
|
||||||
@@ -235,7 +235,7 @@ public sealed class GrpcTelegramClient : ITelegramGateway
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public async Task<int> BackfillAsync(
|
async Task<int> ITelegramGateway.BackfillAsync(
|
||||||
string dialogId,
|
string dialogId,
|
||||||
bool force,
|
bool force,
|
||||||
CancellationToken ct)
|
CancellationToken ct)
|
||||||
@@ -256,7 +256,7 @@ public sealed class GrpcTelegramClient : ITelegramGateway
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public async Task<IReadOnlyList<TelegramRecentMessageDto>> ReadRecentAsync(
|
async Task<IReadOnlyList<TelegramRecentMessageDto>> ITelegramGateway.ReadRecentAsync(
|
||||||
string dialogId,
|
string dialogId,
|
||||||
int limit,
|
int limit,
|
||||||
CancellationToken ct)
|
CancellationToken ct)
|
||||||
@@ -279,7 +279,7 @@ public sealed class GrpcTelegramClient : ITelegramGateway
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public async Task<TelegramSourceContentDto> ReadSourceAsync(
|
async Task<TelegramSourceContentDto> ITelegramGateway.ReadSourceAsync(
|
||||||
string dialogId,
|
string dialogId,
|
||||||
long msgId,
|
long msgId,
|
||||||
CancellationToken ct)
|
CancellationToken ct)
|
||||||
@@ -303,7 +303,7 @@ public sealed class GrpcTelegramClient : ITelegramGateway
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public async Task<IReadOnlyList<TelegramDialogEntryDto>> SearchAsync(
|
async Task<IReadOnlyList<TelegramDialogEntryDto>> ITelegramGateway.SearchAsync(
|
||||||
string query,
|
string query,
|
||||||
int limit,
|
int limit,
|
||||||
CancellationToken ct)
|
CancellationToken ct)
|
||||||
@@ -324,7 +324,7 @@ public sealed class GrpcTelegramClient : ITelegramGateway
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public async Task<TelegramChannelInfoDto> InfoAsync(string dialogId, CancellationToken ct)
|
async Task<TelegramChannelInfoDto> ITelegramGateway.InfoAsync(string dialogId, CancellationToken ct)
|
||||||
{
|
{
|
||||||
TenantId tenantId = RequireTenant();
|
TenantId tenantId = RequireTenant();
|
||||||
try
|
try
|
||||||
@@ -350,7 +350,7 @@ public sealed class GrpcTelegramClient : ITelegramGateway
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public async Task<TelegramEvalReadDto> ReadForEvalAsync(
|
async Task<TelegramEvalReadDto> ITelegramGateway.ReadForEvalAsync(
|
||||||
string dialogId,
|
string dialogId,
|
||||||
int limit,
|
int limit,
|
||||||
CancellationToken ct)
|
CancellationToken ct)
|
||||||
@@ -381,7 +381,7 @@ public sealed class GrpcTelegramClient : ITelegramGateway
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public async Task JoinAsync(string username, CancellationToken ct)
|
async Task ITelegramGateway.JoinAsync(string username, CancellationToken ct)
|
||||||
{
|
{
|
||||||
TenantId tenantId = RequireTenant();
|
TenantId tenantId = RequireTenant();
|
||||||
try
|
try
|
||||||
@@ -398,7 +398,7 @@ public sealed class GrpcTelegramClient : ITelegramGateway
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public async Task LeaveAsync(string dialogId, CancellationToken ct)
|
async Task ITelegramGateway.LeaveAsync(string dialogId, CancellationToken ct)
|
||||||
{
|
{
|
||||||
TenantId tenantId = RequireTenant();
|
TenantId tenantId = RequireTenant();
|
||||||
try
|
try
|
||||||
|
|||||||
@@ -13,11 +13,11 @@ public sealed class LocalAiTools : IAiTools
|
|||||||
"ИИ-инструменты доступны только при подключённом ai-service (Services:Ai:UseLocal=false).";
|
"ИИ-инструменты доступны только при подключённом ai-service (Services:Ai:UseLocal=false).";
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public Task<AiGenerateKeywordsResultDto> GenerateKeywordsAsync(string description, CancellationToken ct)
|
Task<AiGenerateKeywordsResultDto> IAiTools.GenerateKeywordsAsync(string description, CancellationToken ct)
|
||||||
=> throw new NotSupportedException(NotSupportedMessage);
|
=> throw new NotSupportedException(NotSupportedMessage);
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public Task<AiEvaluateFitResultDto> EvaluateFitAsync(
|
Task<AiEvaluateFitResultDto> IAiTools.EvaluateFitAsync(
|
||||||
string text,
|
string text,
|
||||||
string description,
|
string description,
|
||||||
IReadOnlyCollection<string> keywords,
|
IReadOnlyCollection<string> keywords,
|
||||||
|
|||||||
@@ -12,13 +12,13 @@ public sealed class LocalTelegramGateway : ITelegramGateway
|
|||||||
private const string IdlePhase = "idle";
|
private const string IdlePhase = "idle";
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <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));
|
return Task.FromResult(new TelegramAccountStatusDto(IdlePhase, false, false, string.Empty, null, null));
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public Task<TelegramAuthResultDto> StartPhoneAsync(
|
Task<TelegramAuthResultDto> ITelegramGateway.StartPhoneAsync(
|
||||||
string phone,
|
string phone,
|
||||||
int apiId,
|
int apiId,
|
||||||
string apiHash,
|
string apiHash,
|
||||||
@@ -26,7 +26,7 @@ public sealed class LocalTelegramGateway : ITelegramGateway
|
|||||||
=> Task.FromResult(new TelegramAuthResultDto(IdlePhase, null));
|
=> Task.FromResult(new TelegramAuthResultDto(IdlePhase, null));
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public Task<TelegramAuthResultDto> StartQrAsync(
|
Task<TelegramAuthResultDto> ITelegramGateway.StartQrAsync(
|
||||||
int apiId,
|
int apiId,
|
||||||
string apiHash,
|
string apiHash,
|
||||||
CancellationToken ct)
|
CancellationToken ct)
|
||||||
@@ -39,20 +39,20 @@ public sealed class LocalTelegramGateway : ITelegramGateway
|
|||||||
public Task<string> SendPasswordAsync(string password, CancellationToken ct) => Task.FromResult(IdlePhase);
|
public Task<string> SendPasswordAsync(string password, CancellationToken ct) => Task.FromResult(IdlePhase);
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public Task LogoutAsync(CancellationToken ct) => Task.CompletedTask;
|
Task ITelegramGateway.LogoutAsync(CancellationToken ct) => Task.CompletedTask;
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public Task<IReadOnlyList<TelegramDialogEntryDto>> RefreshDialogsAsync(CancellationToken ct)
|
Task<IReadOnlyList<TelegramDialogEntryDto>> ITelegramGateway.RefreshDialogsAsync(CancellationToken ct)
|
||||||
=> Task.FromResult<IReadOnlyList<TelegramDialogEntryDto>>([]);
|
=> Task.FromResult<IReadOnlyList<TelegramDialogEntryDto>>([]);
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public Task SetMonitorAsync(
|
Task ITelegramGateway.SetMonitorAsync(
|
||||||
string dialogId,
|
string dialogId,
|
||||||
bool enabled,
|
bool enabled,
|
||||||
CancellationToken ct) => Task.CompletedTask;
|
CancellationToken ct) => Task.CompletedTask;
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public Task SetMonitorAllAsync(bool enabled, CancellationToken ct) => Task.CompletedTask;
|
Task ITelegramGateway.SetMonitorAllAsync(bool enabled, CancellationToken ct) => Task.CompletedTask;
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public Task<int> BackfillAsync(
|
public Task<int> BackfillAsync(
|
||||||
@@ -61,40 +61,40 @@ public sealed class LocalTelegramGateway : ITelegramGateway
|
|||||||
CancellationToken ct) => Task.FromResult(0);
|
CancellationToken ct) => Task.FromResult(0);
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public Task<IReadOnlyList<TelegramRecentMessageDto>> ReadRecentAsync(
|
Task<IReadOnlyList<TelegramRecentMessageDto>> ITelegramGateway.ReadRecentAsync(
|
||||||
string dialogId,
|
string dialogId,
|
||||||
int limit,
|
int limit,
|
||||||
CancellationToken ct)
|
CancellationToken ct)
|
||||||
=> Task.FromResult<IReadOnlyList<TelegramRecentMessageDto>>([]);
|
=> Task.FromResult<IReadOnlyList<TelegramRecentMessageDto>>([]);
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public Task<TelegramSourceContentDto> ReadSourceAsync(
|
Task<TelegramSourceContentDto> ITelegramGateway.ReadSourceAsync(
|
||||||
string dialogId,
|
string dialogId,
|
||||||
long msgId,
|
long msgId,
|
||||||
CancellationToken ct)
|
CancellationToken ct)
|
||||||
=> Task.FromResult(new TelegramSourceContentDto(false, null, null));
|
=> Task.FromResult(new TelegramSourceContentDto(false, null, null));
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public Task<IReadOnlyList<TelegramDialogEntryDto>> SearchAsync(
|
Task<IReadOnlyList<TelegramDialogEntryDto>> ITelegramGateway.SearchAsync(
|
||||||
string query,
|
string query,
|
||||||
int limit,
|
int limit,
|
||||||
CancellationToken ct)
|
CancellationToken ct)
|
||||||
=> Task.FromResult<IReadOnlyList<TelegramDialogEntryDto>>([]);
|
=> Task.FromResult<IReadOnlyList<TelegramDialogEntryDto>>([]);
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <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));
|
=> Task.FromResult(new TelegramChannelInfoDto(dialogId, string.Empty, string.Empty, string.Empty, SourceDefaults.DefaultHue, null, false));
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public Task<TelegramEvalReadDto> ReadForEvalAsync(
|
Task<TelegramEvalReadDto> ITelegramGateway.ReadForEvalAsync(
|
||||||
string dialogId,
|
string dialogId,
|
||||||
int limit,
|
int limit,
|
||||||
CancellationToken ct)
|
CancellationToken ct)
|
||||||
=> Task.FromResult(new TelegramEvalReadDto(false, "no_history", []));
|
=> Task.FromResult(new TelegramEvalReadDto(false, "no_history", []));
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public Task JoinAsync(string username, CancellationToken ct) => Task.CompletedTask;
|
Task ITelegramGateway.JoinAsync(string username, CancellationToken ct) => Task.CompletedTask;
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <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})";
|
public override string ToString() => $"LocalFileStorage (root: {_rootPath})";
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public async Task<string> PutAsync(
|
async Task<string> IFileStorage.PutAsync(
|
||||||
string objectKey,
|
string objectKey,
|
||||||
Stream content,
|
Stream content,
|
||||||
string contentType,
|
string contentType,
|
||||||
@@ -56,7 +56,7 @@ public sealed class LocalFileStorage : IFileStorage
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public Task<Stream?> GetAsync(string objectKey, CancellationToken ct)
|
Task<Stream?> IFileStorage.GetAsync(string objectKey, CancellationToken ct)
|
||||||
{
|
{
|
||||||
string path = ResolvePath(objectKey);
|
string path = ResolvePath(objectKey);
|
||||||
if (!File.Exists(path))
|
if (!File.Exists(path))
|
||||||
@@ -69,7 +69,7 @@ public sealed class LocalFileStorage : IFileStorage
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public Task<FileMeta?> StatAsync(string objectKey, CancellationToken ct)
|
Task<FileMeta?> IFileStorage.StatAsync(string objectKey, CancellationToken ct)
|
||||||
{
|
{
|
||||||
string path = ResolvePath(objectKey);
|
string path = ResolvePath(objectKey);
|
||||||
if (!File.Exists(path))
|
if (!File.Exists(path))
|
||||||
@@ -82,7 +82,7 @@ public sealed class LocalFileStorage : IFileStorage
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public Task DeleteAsync(string objectKey, CancellationToken ct)
|
Task IFileStorage.DeleteAsync(string objectKey, CancellationToken ct)
|
||||||
{
|
{
|
||||||
string path = ResolvePath(objectKey);
|
string path = ResolvePath(objectKey);
|
||||||
if (File.Exists(path))
|
if (File.Exists(path))
|
||||||
|
|||||||
@@ -67,7 +67,7 @@ public sealed class MinioFileStorage : IFileStorage
|
|||||||
public override string ToString() => $"MinioFileStorage (endpoint: {_endpoint}; bucket: {_bucket})";
|
public override string ToString() => $"MinioFileStorage (endpoint: {_endpoint}; bucket: {_bucket})";
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public async Task<string> PutAsync(
|
async Task<string> IFileStorage.PutAsync(
|
||||||
string objectKey,
|
string objectKey,
|
||||||
Stream content,
|
Stream content,
|
||||||
string contentType,
|
string contentType,
|
||||||
@@ -99,7 +99,7 @@ public sealed class MinioFileStorage : IFileStorage
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public async Task<Stream?> GetAsync(string objectKey, CancellationToken ct)
|
async Task<Stream?> IFileStorage.GetAsync(string objectKey, CancellationToken ct)
|
||||||
{
|
{
|
||||||
MemoryStream buffer = new();
|
MemoryStream buffer = new();
|
||||||
try
|
try
|
||||||
@@ -129,7 +129,7 @@ public sealed class MinioFileStorage : IFileStorage
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public async Task<FileMeta?> StatAsync(string objectKey, CancellationToken ct)
|
async Task<FileMeta?> IFileStorage.StatAsync(string objectKey, CancellationToken ct)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
@@ -145,7 +145,7 @@ public sealed class MinioFileStorage : IFileStorage
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public async Task DeleteAsync(string objectKey, CancellationToken ct)
|
async Task IFileStorage.DeleteAsync(string objectKey, CancellationToken ct)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
using Deal.Infrastructure.Persistence.Entities;
|
using Deal.Infrastructure.Persistence.Entities;
|
||||||
using Deal.Modules.Discovery.Application.Models;
|
using Deal.Modules.Discovery.Application.Models;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Deal.Modules.Discovery.Application.Abstractions;
|
||||||
|
|
||||||
namespace Deal.Infrastructure.Persistence.Repositories;
|
namespace Deal.Infrastructure.Persistence.Repositories;
|
||||||
|
|
||||||
@@ -10,7 +11,7 @@ namespace Deal.Infrastructure.Persistence.Repositories;
|
|||||||
public sealed partial class DiscoveryStore
|
public sealed partial class DiscoveryStore
|
||||||
{
|
{
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public async Task UpsertBlacklistAsync(
|
async Task IDiscoveryStore.UpsertBlacklistAsync(
|
||||||
string dialogId,
|
string dialogId,
|
||||||
string name,
|
string name,
|
||||||
string reason,
|
string reason,
|
||||||
@@ -38,13 +39,13 @@ public sealed partial class DiscoveryStore
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <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);
|
await _dbContext.DiscBlacklist.Where(entry => entry.DialogId == dialogId).ExecuteDeleteAsync(ct);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public async Task<DiscoveryBlacklistDto?> GetBlacklistAsync(string dialogId, CancellationToken ct)
|
async Task<DiscoveryBlacklistDto?> IDiscoveryStore.GetBlacklistAsync(string dialogId, CancellationToken ct)
|
||||||
{
|
{
|
||||||
DiscBlacklistEntity? row = await _dbContext.DiscBlacklist
|
DiscBlacklistEntity? row = await _dbContext.DiscBlacklist
|
||||||
.AsNoTracking()
|
.AsNoTracking()
|
||||||
@@ -53,7 +54,7 @@ public sealed partial class DiscoveryStore
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public async Task<IReadOnlyList<DiscoveryBlacklistDto>> ListBlacklistAsync(CancellationToken ct)
|
async Task<IReadOnlyList<DiscoveryBlacklistDto>> IDiscoveryStore.ListBlacklistAsync(CancellationToken ct)
|
||||||
{
|
{
|
||||||
List<DiscBlacklistEntity> rows = await _dbContext.DiscBlacklist
|
List<DiscBlacklistEntity> rows = await _dbContext.DiscBlacklist
|
||||||
.AsNoTracking()
|
.AsNoTracking()
|
||||||
|
|||||||
+12
-11
@@ -1,6 +1,7 @@
|
|||||||
using Deal.Infrastructure.Persistence.Entities;
|
using Deal.Infrastructure.Persistence.Entities;
|
||||||
using Deal.Modules.Discovery.Application.Models;
|
using Deal.Modules.Discovery.Application.Models;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Deal.Modules.Discovery.Application.Abstractions;
|
||||||
|
|
||||||
namespace Deal.Infrastructure.Persistence.Repositories;
|
namespace Deal.Infrastructure.Persistence.Repositories;
|
||||||
|
|
||||||
@@ -10,7 +11,7 @@ namespace Deal.Infrastructure.Persistence.Repositories;
|
|||||||
public sealed partial class DiscoveryStore
|
public sealed partial class DiscoveryStore
|
||||||
{
|
{
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public async Task<IReadOnlyList<DiscoveryCandidateDto>> ListCandidatesAsync(
|
async Task<IReadOnlyList<DiscoveryCandidateDto>> IDiscoveryStore.ListCandidatesAsync(
|
||||||
string taskId,
|
string taskId,
|
||||||
string? status,
|
string? status,
|
||||||
CancellationToken ct)
|
CancellationToken ct)
|
||||||
@@ -26,7 +27,7 @@ public sealed partial class DiscoveryStore
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public async Task<DiscoveryCandidateDto?> GetCandidateAsync(string dialogId, CancellationToken ct)
|
async Task<DiscoveryCandidateDto?> IDiscoveryStore.GetCandidateAsync(string dialogId, CancellationToken ct)
|
||||||
{
|
{
|
||||||
DiscCandidateEntity? row = await _dbContext.DiscCandidates
|
DiscCandidateEntity? row = await _dbContext.DiscCandidates
|
||||||
.AsNoTracking()
|
.AsNoTracking()
|
||||||
@@ -35,19 +36,19 @@ public sealed partial class DiscoveryStore
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <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);
|
return await _dbContext.Dialogs.AnyAsync(dialog => dialog.Id == dialogId, ct);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <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);
|
return _dbContext.DiscBlacklist.AnyAsync(row => row.DialogId == dialogId, ct);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public async Task CreateCandidateAsync(DiscoveryCandidateRow row, CancellationToken ct)
|
async Task IDiscoveryStore.CreateCandidateAsync(DiscoveryCandidateRow row, CancellationToken ct)
|
||||||
{
|
{
|
||||||
DateTimeOffset now = DateTimeOffset.UtcNow;
|
DateTimeOffset now = DateTimeOffset.UtcNow;
|
||||||
_dbContext.DiscCandidates.Add(new DiscCandidateEntity
|
_dbContext.DiscCandidates.Add(new DiscCandidateEntity
|
||||||
@@ -66,13 +67,13 @@ public sealed partial class DiscoveryStore
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <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);
|
await _dbContext.DiscCandidates.Where(candidate => candidate.DialogId == dialogId).ExecuteDeleteAsync(ct);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public async Task<bool> PatchCandidateAsync(
|
async Task<bool> IDiscoveryStore.PatchCandidateAsync(
|
||||||
string dialogId,
|
string dialogId,
|
||||||
DiscoveryCandidatePatch patch,
|
DiscoveryCandidatePatch patch,
|
||||||
CancellationToken ct)
|
CancellationToken ct)
|
||||||
@@ -91,7 +92,7 @@ public sealed partial class DiscoveryStore
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public async Task<bool> SetCandidateStatusAsync(
|
async Task<bool> IDiscoveryStore.SetCandidateStatusAsync(
|
||||||
string dialogId,
|
string dialogId,
|
||||||
string status,
|
string status,
|
||||||
CancellationToken ct)
|
CancellationToken ct)
|
||||||
@@ -110,7 +111,7 @@ public sealed partial class DiscoveryStore
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public async Task<bool> SetCandidateJoinedAsync(
|
async Task<bool> IDiscoveryStore.SetCandidateJoinedAsync(
|
||||||
string dialogId,
|
string dialogId,
|
||||||
bool autoJoined,
|
bool autoJoined,
|
||||||
CancellationToken ct)
|
CancellationToken ct)
|
||||||
@@ -130,7 +131,7 @@ public sealed partial class DiscoveryStore
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public async Task<int?> IncrementJoinFailuresAsync(string dialogId, CancellationToken ct)
|
async Task<int?> IDiscoveryStore.IncrementJoinFailuresAsync(string dialogId, CancellationToken ct)
|
||||||
{
|
{
|
||||||
DiscCandidateEntity? row = await _dbContext.DiscCandidates
|
DiscCandidateEntity? row = await _dbContext.DiscCandidates
|
||||||
.FirstOrDefaultAsync(candidate => candidate.DialogId == dialogId, ct);
|
.FirstOrDefaultAsync(candidate => candidate.DialogId == dialogId, ct);
|
||||||
@@ -146,7 +147,7 @@ public sealed partial class DiscoveryStore
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public async Task<bool> SetCandidateRejectedAsync(string dialogId, CancellationToken ct)
|
async Task<bool> IDiscoveryStore.SetCandidateRejectedAsync(string dialogId, CancellationToken ct)
|
||||||
{
|
{
|
||||||
DiscCandidateEntity? row = await _dbContext.DiscCandidates
|
DiscCandidateEntity? row = await _dbContext.DiscCandidates
|
||||||
.FirstOrDefaultAsync(candidate => candidate.DialogId == dialogId, ct);
|
.FirstOrDefaultAsync(candidate => candidate.DialogId == dialogId, ct);
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
using Deal.Infrastructure.Persistence.Entities;
|
using Deal.Infrastructure.Persistence.Entities;
|
||||||
using Deal.Modules.Discovery.Application.Models;
|
using Deal.Modules.Discovery.Application.Models;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Deal.Modules.Discovery.Application.Abstractions;
|
||||||
|
|
||||||
namespace Deal.Infrastructure.Persistence.Repositories;
|
namespace Deal.Infrastructure.Persistence.Repositories;
|
||||||
|
|
||||||
@@ -10,7 +11,7 @@ namespace Deal.Infrastructure.Persistence.Repositories;
|
|||||||
public sealed partial class DiscoveryStore
|
public sealed partial class DiscoveryStore
|
||||||
{
|
{
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public async Task AddLogAsync(
|
async Task IDiscoveryStore.AddLogAsync(
|
||||||
string logId,
|
string logId,
|
||||||
string taskId,
|
string taskId,
|
||||||
string logEvent,
|
string logEvent,
|
||||||
@@ -29,7 +30,7 @@ public sealed partial class DiscoveryStore
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public async Task<int> CountLogEventAsync(
|
async Task<int> IDiscoveryStore.CountLogEventAsync(
|
||||||
string logEvent,
|
string logEvent,
|
||||||
DateTimeOffset sinceUtc,
|
DateTimeOffset sinceUtc,
|
||||||
CancellationToken ct)
|
CancellationToken ct)
|
||||||
@@ -38,7 +39,7 @@ public sealed partial class DiscoveryStore
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public async Task<IReadOnlyList<DiscoveryLogDto>> ListTaskLogAsync(
|
async Task<IReadOnlyList<DiscoveryLogDto>> IDiscoveryStore.ListTaskLogAsync(
|
||||||
string taskId,
|
string taskId,
|
||||||
int limit,
|
int limit,
|
||||||
CancellationToken ct)
|
CancellationToken ct)
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
using Deal.Infrastructure.Persistence.Entities;
|
using Deal.Infrastructure.Persistence.Entities;
|
||||||
using Deal.Modules.Discovery.Application.Models;
|
using Deal.Modules.Discovery.Application.Models;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Deal.Modules.Discovery.Application.Abstractions;
|
||||||
|
|
||||||
namespace Deal.Infrastructure.Persistence.Repositories;
|
namespace Deal.Infrastructure.Persistence.Repositories;
|
||||||
|
|
||||||
@@ -10,7 +11,7 @@ namespace Deal.Infrastructure.Persistence.Repositories;
|
|||||||
public sealed partial class DiscoveryStore
|
public sealed partial class DiscoveryStore
|
||||||
{
|
{
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public async Task<IReadOnlyList<DiscoveryTaskDto>> ListTasksAsync(CancellationToken ct)
|
async Task<IReadOnlyList<DiscoveryTaskDto>> IDiscoveryStore.ListTasksAsync(CancellationToken ct)
|
||||||
{
|
{
|
||||||
List<DiscTaskEntity> rows = await _dbContext.DiscTasks
|
List<DiscTaskEntity> rows = await _dbContext.DiscTasks
|
||||||
.AsNoTracking()
|
.AsNoTracking()
|
||||||
@@ -20,7 +21,7 @@ public sealed partial class DiscoveryStore
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public async Task<DiscoveryTaskDto?> GetTaskAsync(string taskId, CancellationToken ct)
|
async Task<DiscoveryTaskDto?> IDiscoveryStore.GetTaskAsync(string taskId, CancellationToken ct)
|
||||||
{
|
{
|
||||||
DiscTaskEntity? row = await _dbContext.DiscTasks
|
DiscTaskEntity? row = await _dbContext.DiscTasks
|
||||||
.AsNoTracking()
|
.AsNoTracking()
|
||||||
@@ -29,7 +30,7 @@ public sealed partial class DiscoveryStore
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public async Task CreateTaskAsync(DiscoveryTaskRow row, CancellationToken ct)
|
async Task IDiscoveryStore.CreateTaskAsync(DiscoveryTaskRow row, CancellationToken ct)
|
||||||
{
|
{
|
||||||
DateTimeOffset now = DateTimeOffset.UtcNow;
|
DateTimeOffset now = DateTimeOffset.UtcNow;
|
||||||
_dbContext.DiscTasks.Add(new DiscTaskEntity
|
_dbContext.DiscTasks.Add(new DiscTaskEntity
|
||||||
@@ -52,7 +53,7 @@ public sealed partial class DiscoveryStore
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public async Task<bool> PatchTaskAsync(
|
async Task<bool> IDiscoveryStore.PatchTaskAsync(
|
||||||
string taskId,
|
string taskId,
|
||||||
DiscoveryTaskPatch patch,
|
DiscoveryTaskPatch patch,
|
||||||
CancellationToken ct)
|
CancellationToken ct)
|
||||||
@@ -71,7 +72,7 @@ public sealed partial class DiscoveryStore
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public async Task<bool> DeleteTaskAsync(string taskId, CancellationToken ct)
|
async Task<bool> IDiscoveryStore.DeleteTaskAsync(string taskId, CancellationToken ct)
|
||||||
{
|
{
|
||||||
DiscTaskEntity? row = await _dbContext.DiscTasks
|
DiscTaskEntity? row = await _dbContext.DiscTasks
|
||||||
.FirstOrDefaultAsync(task => task.Id == taskId, ct);
|
.FirstOrDefaultAsync(task => task.Id == taskId, ct);
|
||||||
@@ -88,7 +89,7 @@ public sealed partial class DiscoveryStore
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public async Task<bool> SetTaskRunningAsync(
|
async Task<bool> IDiscoveryStore.SetTaskRunningAsync(
|
||||||
string taskId,
|
string taskId,
|
||||||
bool resetProgress,
|
bool resetProgress,
|
||||||
CancellationToken ct)
|
CancellationToken ct)
|
||||||
@@ -117,7 +118,7 @@ public sealed partial class DiscoveryStore
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public async Task<bool> SetTaskPausedAsync(string taskId, CancellationToken ct)
|
async Task<bool> IDiscoveryStore.SetTaskPausedAsync(string taskId, CancellationToken ct)
|
||||||
{
|
{
|
||||||
DiscTaskEntity? row = await _dbContext.DiscTasks
|
DiscTaskEntity? row = await _dbContext.DiscTasks
|
||||||
.FirstOrDefaultAsync(task => task.Id == taskId, ct);
|
.FirstOrDefaultAsync(task => task.Id == taskId, ct);
|
||||||
@@ -133,7 +134,7 @@ public sealed partial class DiscoveryStore
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public async Task<bool> SetTaskDoneAsync(string taskId, CancellationToken ct)
|
async Task<bool> IDiscoveryStore.SetTaskDoneAsync(string taskId, CancellationToken ct)
|
||||||
{
|
{
|
||||||
DiscTaskEntity? row = await _dbContext.DiscTasks
|
DiscTaskEntity? row = await _dbContext.DiscTasks
|
||||||
.FirstOrDefaultAsync(task => task.Id == taskId, ct);
|
.FirstOrDefaultAsync(task => task.Id == taskId, ct);
|
||||||
@@ -149,7 +150,7 @@ public sealed partial class DiscoveryStore
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public async Task<bool> BumpTaskCounterAsync(
|
async Task<bool> IDiscoveryStore.BumpTaskCounterAsync(
|
||||||
string taskId,
|
string taskId,
|
||||||
DiscoveryCounterField field,
|
DiscoveryCounterField field,
|
||||||
int n,
|
int n,
|
||||||
@@ -186,7 +187,7 @@ public sealed partial class DiscoveryStore
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public async Task<bool> AdvanceSearchAsync(
|
async Task<bool> IDiscoveryStore.AdvanceSearchAsync(
|
||||||
string taskId,
|
string taskId,
|
||||||
int nextIndex,
|
int nextIndex,
|
||||||
bool searchDone,
|
bool searchDone,
|
||||||
@@ -207,7 +208,7 @@ public sealed partial class DiscoveryStore
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public async Task<int> SumActivePlanAsync(string? excludeTaskId, CancellationToken ct)
|
async Task<int> IDiscoveryStore.SumActivePlanAsync(string? excludeTaskId, CancellationToken ct)
|
||||||
{
|
{
|
||||||
IQueryable<DiscTaskEntity> query = _dbContext.DiscTasks
|
IQueryable<DiscTaskEntity> query = _dbContext.DiscTasks
|
||||||
.Where(task => task.Status != "done" && task.Status != "failed");
|
.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.Cards.Application.Sources;
|
||||||
using Deal.Modules.Kanban.Application.Models;
|
using Deal.Modules.Kanban.Application.Models;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Deal.Modules.Kanban.Application.Abstractions;
|
||||||
|
|
||||||
namespace Deal.Infrastructure.Persistence.Repositories;
|
namespace Deal.Infrastructure.Persistence.Repositories;
|
||||||
|
|
||||||
@@ -11,7 +12,7 @@ namespace Deal.Infrastructure.Persistence.Repositories;
|
|||||||
public sealed partial class KanbanStore
|
public sealed partial class KanbanStore
|
||||||
{
|
{
|
||||||
/// <inheritdoc />
|
/// <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();
|
IQueryable<CardEntity> queryable = _dbContext.Cards.AsNoTracking();
|
||||||
if (query.Col is null)
|
if (query.Col is null)
|
||||||
@@ -30,7 +31,7 @@ public sealed partial class KanbanStore
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public async Task<IReadOnlyList<CardDto>> SearchCardsAsync(
|
async Task<IReadOnlyList<CardDto>> ICardStore.SearchCardsAsync(
|
||||||
string q,
|
string q,
|
||||||
int limit,
|
int limit,
|
||||||
CancellationToken ct)
|
CancellationToken ct)
|
||||||
@@ -61,7 +62,7 @@ public sealed partial class KanbanStore
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public async Task<CardDto?> GetCardAsync(string cardId, CancellationToken ct)
|
async Task<CardDto?> ICardStore.GetCardAsync(string cardId, CancellationToken ct)
|
||||||
{
|
{
|
||||||
CardEntity? entity = await _dbContext.Cards
|
CardEntity? entity = await _dbContext.Cards
|
||||||
.AsNoTracking()
|
.AsNoTracking()
|
||||||
@@ -76,7 +77,7 @@ public sealed partial class KanbanStore
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public async Task<CardDto?> GetCardBySourceAsync(
|
async Task<CardDto?> ICardStore.GetCardBySourceAsync(
|
||||||
SourceRef source,
|
SourceRef source,
|
||||||
CancellationToken ct)
|
CancellationToken ct)
|
||||||
{
|
{
|
||||||
@@ -104,7 +105,7 @@ public sealed partial class KanbanStore
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public async Task AddCardAsync(CardSnapshot snapshot, CancellationToken ct)
|
async Task ICardStore.AddCardAsync(CardSnapshot snapshot, CancellationToken ct)
|
||||||
{
|
{
|
||||||
// CreatedAt проставляет хранилище (UTC-now) — в snapshot поля нет (см. CardSnapshot).
|
// CreatedAt проставляет хранилище (UTC-now) — в snapshot поля нет (см. CardSnapshot).
|
||||||
_dbContext.Cards.Add(ToCardEntity(snapshot));
|
_dbContext.Cards.Add(ToCardEntity(snapshot));
|
||||||
@@ -131,7 +132,7 @@ public sealed partial class KanbanStore
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <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);
|
CardEntity? entity = await _dbContext.Cards.SingleOrDefaultAsync(card => card.Id == update.CardId, ct);
|
||||||
if (entity is null)
|
if (entity is null)
|
||||||
@@ -152,7 +153,7 @@ public sealed partial class KanbanStore
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <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);
|
CardEntity? entity = await _dbContext.Cards.SingleOrDefaultAsync(card => card.Id == update.CardId, ct);
|
||||||
if (entity is null)
|
if (entity is null)
|
||||||
@@ -182,7 +183,7 @@ public sealed partial class KanbanStore
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public async Task UpdateSeenAsync(
|
async Task ICardStore.UpdateSeenAsync(
|
||||||
string? cardId,
|
string? cardId,
|
||||||
string? col,
|
string? col,
|
||||||
CancellationToken ct)
|
CancellationToken ct)
|
||||||
@@ -201,7 +202,7 @@ public sealed partial class KanbanStore
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <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 using var transaction = await _dbContext.Database.BeginTransactionAsync(ct);
|
||||||
await _dbContext.DedupEntries
|
await _dbContext.DedupEntries
|
||||||
@@ -214,7 +215,7 @@ public sealed partial class KanbanStore
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <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 using var transaction = await _dbContext.Database.BeginTransactionAsync(ct);
|
||||||
await _dbContext.DedupEntries
|
await _dbContext.DedupEntries
|
||||||
@@ -228,7 +229,7 @@ public sealed partial class KanbanStore
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public async Task<IReadOnlyDictionary<string, CardColumnCountDto>> CountCardsByColAsync(CancellationToken ct)
|
async Task<IReadOnlyDictionary<string, CardColumnCountDto>> ICardStore.CountCardsByColAsync(CancellationToken ct)
|
||||||
{
|
{
|
||||||
var rows = await _dbContext.Cards
|
var rows = await _dbContext.Cards
|
||||||
.AsNoTracking()
|
.AsNoTracking()
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ using Deal.Infrastructure.Persistence.Entities;
|
|||||||
using Deal.Modules.Cards.Application.Models;
|
using Deal.Modules.Cards.Application.Models;
|
||||||
using Deal.Modules.Kanban.Application.Models;
|
using Deal.Modules.Kanban.Application.Models;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Deal.Modules.Kanban.Application.Abstractions;
|
||||||
|
|
||||||
namespace Deal.Infrastructure.Persistence.Repositories;
|
namespace Deal.Infrastructure.Persistence.Repositories;
|
||||||
|
|
||||||
@@ -11,7 +12,7 @@ namespace Deal.Infrastructure.Persistence.Repositories;
|
|||||||
public sealed partial class KanbanStore
|
public sealed partial class KanbanStore
|
||||||
{
|
{
|
||||||
/// <inheritdoc />
|
/// <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
|
List<LeadCommentEntity> entities = await _dbContext.LeadComments
|
||||||
.AsNoTracking()
|
.AsNoTracking()
|
||||||
@@ -23,7 +24,7 @@ public sealed partial class KanbanStore
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public async Task AddCommentAsync(
|
async Task ICardStore.AddCommentAsync(
|
||||||
string commentId,
|
string commentId,
|
||||||
string cardId,
|
string cardId,
|
||||||
string by,
|
string by,
|
||||||
@@ -42,7 +43,7 @@ public sealed partial class KanbanStore
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public async Task AddMoveAsync(CardMoveDto move, CancellationToken ct)
|
async Task ICardStore.AddMoveAsync(CardMoveDto move, CancellationToken ct)
|
||||||
{
|
{
|
||||||
_dbContext.CardMoves.Add(new CardMoveEntity
|
_dbContext.CardMoves.Add(new CardMoveEntity
|
||||||
{
|
{
|
||||||
@@ -60,7 +61,7 @@ public sealed partial class KanbanStore
|
|||||||
public Task<int> CountMovesAsync(CancellationToken ct) => _dbContext.CardMoves.CountAsync(ct);
|
public Task<int> CountMovesAsync(CancellationToken ct) => _dbContext.CardMoves.CountAsync(ct);
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <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
|
return await _dbContext.CardMoves
|
||||||
.AsNoTracking()
|
.AsNoTracking()
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ using Deal.Infrastructure.Persistence.Entities;
|
|||||||
using Deal.Modules.Cards.Application.Models;
|
using Deal.Modules.Cards.Application.Models;
|
||||||
using Deal.Modules.Kanban.Application.Models;
|
using Deal.Modules.Kanban.Application.Models;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Deal.Modules.Kanban.Application.Abstractions;
|
||||||
|
|
||||||
namespace Deal.Infrastructure.Persistence.Repositories;
|
namespace Deal.Infrastructure.Persistence.Repositories;
|
||||||
|
|
||||||
@@ -11,7 +12,7 @@ namespace Deal.Infrastructure.Persistence.Repositories;
|
|||||||
public sealed partial class KanbanStore
|
public sealed partial class KanbanStore
|
||||||
{
|
{
|
||||||
/// <inheritdoc />
|
/// <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();
|
IQueryable<ContainerEntity> queryable = _dbContext.Containers.AsNoTracking();
|
||||||
if (space is not null)
|
if (space is not null)
|
||||||
@@ -28,7 +29,7 @@ public sealed partial class KanbanStore
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public async Task<ContainerDto?> GetContainerAsync(string containerId, CancellationToken ct)
|
async Task<ContainerDto?> ICardStore.GetContainerAsync(string containerId, CancellationToken ct)
|
||||||
{
|
{
|
||||||
ContainerEntity? entity = await _dbContext.Containers
|
ContainerEntity? entity = await _dbContext.Containers
|
||||||
.AsNoTracking()
|
.AsNoTracking()
|
||||||
@@ -37,14 +38,14 @@ public sealed partial class KanbanStore
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public async Task CreateContainerAsync(ContainerDto container, CancellationToken ct)
|
async Task ICardStore.CreateContainerAsync(ContainerDto container, CancellationToken ct)
|
||||||
{
|
{
|
||||||
_dbContext.Containers.Add(ToContainerEntity(container));
|
_dbContext.Containers.Add(ToContainerEntity(container));
|
||||||
await _dbContext.SaveChangesAsync(ct);
|
await _dbContext.SaveChangesAsync(ct);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public async Task UpdateContainerAsync(ContainerDto container, CancellationToken ct)
|
async Task ICardStore.UpdateContainerAsync(ContainerDto container, CancellationToken ct)
|
||||||
{
|
{
|
||||||
// Полное обновление строки (сервис читает Get + применяет ContainerPatchDto): JSON-поля пишутся
|
// Полное обновление строки (сервис читает Get + применяет ContainerPatchDto): JSON-поля пишутся
|
||||||
// целиком, CreatedAt не трогаем — одним UPDATE.
|
// целиком, CreatedAt не трогаем — одним UPDATE.
|
||||||
@@ -64,7 +65,7 @@ public sealed partial class KanbanStore
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <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 />
|
/// <inheritdoc />
|
||||||
public async Task ReorderContainersAsync(
|
async Task ICardStore.ReorderContainersAsync(
|
||||||
string space,
|
string space,
|
||||||
IReadOnlyList<string> containerIds,
|
IReadOnlyList<string> containerIds,
|
||||||
CancellationToken ct)
|
CancellationToken ct)
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
using Deal.Infrastructure.Persistence.Entities;
|
using Deal.Infrastructure.Persistence.Entities;
|
||||||
using Deal.Modules.Kanban.Application.Models;
|
using Deal.Modules.Kanban.Application.Models;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Deal.Modules.Kanban.Application.Abstractions;
|
||||||
|
|
||||||
namespace Deal.Infrastructure.Persistence.Repositories;
|
namespace Deal.Infrastructure.Persistence.Repositories;
|
||||||
|
|
||||||
@@ -10,7 +11,7 @@ namespace Deal.Infrastructure.Persistence.Repositories;
|
|||||||
public sealed partial class KanbanStore
|
public sealed partial class KanbanStore
|
||||||
{
|
{
|
||||||
/// <inheritdoc />
|
/// <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();
|
IQueryable<CardEntity> queryable = _dbContext.Cards.AsNoTracking();
|
||||||
if (containerId is not null)
|
if (containerId is not null)
|
||||||
@@ -29,7 +30,7 @@ public sealed partial class KanbanStore
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public async Task<bool> PatchCardAsync(
|
async Task<bool> ICardStore.PatchCardAsync(
|
||||||
string cardId,
|
string cardId,
|
||||||
CardPatch patch,
|
CardPatch patch,
|
||||||
CancellationToken ct)
|
CancellationToken ct)
|
||||||
@@ -68,7 +69,7 @@ public sealed partial class KanbanStore
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public async Task<bool> AddLinkAsync(
|
async Task<bool> ICardStore.AddLinkAsync(
|
||||||
string cardId,
|
string cardId,
|
||||||
CardLinkDto link,
|
CardLinkDto link,
|
||||||
CancellationToken ct)
|
CancellationToken ct)
|
||||||
@@ -85,7 +86,7 @@ public sealed partial class KanbanStore
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public async Task<bool> RemoveLinkAsync(
|
async Task<bool> ICardStore.RemoveLinkAsync(
|
||||||
string cardId,
|
string cardId,
|
||||||
string linkId,
|
string linkId,
|
||||||
CancellationToken ct)
|
CancellationToken ct)
|
||||||
@@ -106,7 +107,7 @@ public sealed partial class KanbanStore
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public async Task<bool> AddFileAsync(
|
async Task<bool> ICardStore.AddFileAsync(
|
||||||
string cardId,
|
string cardId,
|
||||||
CardFileDto file,
|
CardFileDto file,
|
||||||
CancellationToken ct)
|
CancellationToken ct)
|
||||||
@@ -123,7 +124,7 @@ public sealed partial class KanbanStore
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public async Task<bool> RemoveFileAsync(
|
async Task<bool> ICardStore.RemoveFileAsync(
|
||||||
string cardId,
|
string cardId,
|
||||||
string fileId,
|
string fileId,
|
||||||
CancellationToken ct)
|
CancellationToken ct)
|
||||||
@@ -144,7 +145,7 @@ public sealed partial class KanbanStore
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public async Task<bool> MoveCardStageAsync(
|
async Task<bool> ICardStore.MoveCardStageAsync(
|
||||||
string cardId,
|
string cardId,
|
||||||
string containerId,
|
string containerId,
|
||||||
CardHistoryDto historyEntry,
|
CardHistoryDto historyEntry,
|
||||||
@@ -176,7 +177,7 @@ public sealed partial class KanbanStore
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public async Task SetReminderAsync(
|
async Task ICardStore.SetReminderAsync(
|
||||||
string cardId,
|
string cardId,
|
||||||
long atMs,
|
long atMs,
|
||||||
CancellationToken ct)
|
CancellationToken ct)
|
||||||
@@ -191,7 +192,7 @@ public sealed partial class KanbanStore
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public async Task ClearReminderAsync(string cardId, CancellationToken ct)
|
async Task ICardStore.ClearReminderAsync(string cardId, CancellationToken ct)
|
||||||
{
|
{
|
||||||
await _dbContext.Cards
|
await _dbContext.Cards
|
||||||
.Where(card => card.Id == cardId)
|
.Where(card => card.Id == cardId)
|
||||||
@@ -202,7 +203,7 @@ public sealed partial class KanbanStore
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public async Task<int> ClearStageAsync(string containerId, CancellationToken ct)
|
async Task<int> ICardStore.ClearStageAsync(string containerId, CancellationToken ct)
|
||||||
{
|
{
|
||||||
return await _dbContext.Cards
|
return await _dbContext.Cards
|
||||||
.Where(card => card.Col == containerId)
|
.Where(card => card.Col == containerId)
|
||||||
@@ -210,7 +211,7 @@ public sealed partial class KanbanStore
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <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
|
return await _dbContext.Cards
|
||||||
.AsNoTracking()
|
.AsNoTracking()
|
||||||
@@ -224,7 +225,7 @@ public sealed partial class KanbanStore
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public async Task MarkRemindersFiredAsync(IReadOnlyList<string> cardIds, CancellationToken ct)
|
async Task ICardStore.MarkRemindersFiredAsync(IReadOnlyList<string> cardIds, CancellationToken ct)
|
||||||
{
|
{
|
||||||
if (cardIds.Count == 0)
|
if (cardIds.Count == 0)
|
||||||
{
|
{
|
||||||
@@ -237,7 +238,7 @@ public sealed partial class KanbanStore
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public async Task<int> ClearExpiredRemindersAsync(DateTimeOffset now, CancellationToken ct)
|
async Task<int> ICardStore.ClearExpiredRemindersAsync(DateTimeOffset now, CancellationToken ct)
|
||||||
{
|
{
|
||||||
return await _dbContext.Cards
|
return await _dbContext.Cards
|
||||||
.Where(card => card.ReminderAt != null && card.ReminderAt <= now)
|
.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.Cards.Application.Models;
|
||||||
using Deal.Modules.Kanban.Application.Models;
|
using Deal.Modules.Kanban.Application.Models;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Deal.Modules.Kanban.Application.Abstractions;
|
||||||
|
|
||||||
namespace Deal.Infrastructure.Persistence.Repositories;
|
namespace Deal.Infrastructure.Persistence.Repositories;
|
||||||
|
|
||||||
@@ -11,7 +12,7 @@ namespace Deal.Infrastructure.Persistence.Repositories;
|
|||||||
public sealed partial class KanbanStore
|
public sealed partial class KanbanStore
|
||||||
{
|
{
|
||||||
/// <inheritdoc />
|
/// <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
|
List<string> boardIds = await _dbContext.Containers
|
||||||
.AsNoTracking()
|
.AsNoTracking()
|
||||||
@@ -28,7 +29,7 @@ public sealed partial class KanbanStore
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public async Task<int> ArchiveAsync(
|
async Task<int> ICardStore.ArchiveAsync(
|
||||||
IReadOnlyList<string> cardIds,
|
IReadOnlyList<string> cardIds,
|
||||||
DateTimeOffset archivedAt,
|
DateTimeOffset archivedAt,
|
||||||
CancellationToken ct)
|
CancellationToken ct)
|
||||||
@@ -49,7 +50,7 @@ public sealed partial class KanbanStore
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <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
|
return await _dbContext.Cards
|
||||||
.AsNoTracking()
|
.AsNoTracking()
|
||||||
@@ -61,7 +62,7 @@ public sealed partial class KanbanStore
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <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
|
return await _dbContext.Cards
|
||||||
.AsNoTracking()
|
.AsNoTracking()
|
||||||
@@ -71,7 +72,7 @@ public sealed partial class KanbanStore
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <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)
|
if (cardIds.Count == 0)
|
||||||
{
|
{
|
||||||
@@ -90,7 +91,7 @@ public sealed partial class KanbanStore
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public async Task<IReadOnlyList<CardDto>> ListCardsForConversionAsync(CancellationToken ct)
|
async Task<IReadOnlyList<CardDto>> ICardStore.ListCardsForConversionAsync(CancellationToken ct)
|
||||||
{
|
{
|
||||||
List<CardEntity> entities = await _dbContext.Cards
|
List<CardEntity> entities = await _dbContext.Cards
|
||||||
.AsNoTracking()
|
.AsNoTracking()
|
||||||
@@ -102,7 +103,7 @@ public sealed partial class KanbanStore
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public async Task UpdateConversionAsync(
|
async Task ICardStore.UpdateConversionAsync(
|
||||||
string cardId,
|
string cardId,
|
||||||
double? convFrom,
|
double? convFrom,
|
||||||
double? convTo,
|
double? convTo,
|
||||||
@@ -119,7 +120,7 @@ public sealed partial class KanbanStore
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public async Task<IReadOnlyList<CardDto>> ListInboxWithSourceAsync(CancellationToken ct)
|
async Task<IReadOnlyList<CardDto>> ICardStore.ListInboxWithSourceAsync(CancellationToken ct)
|
||||||
{
|
{
|
||||||
List<CardEntity> entities = await _dbContext.Cards
|
List<CardEntity> entities = await _dbContext.Cards
|
||||||
.AsNoTracking()
|
.AsNoTracking()
|
||||||
|
|||||||
@@ -59,17 +59,17 @@ public sealed class TenantLimitStore : ITenantLimitStore
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public async Task<TenantLimitDto> GetOrCreateAsync(
|
async Task<TenantLimitDto> ITenantLimitStore.GetOrCreateAsync(
|
||||||
Guid tenantId,
|
Guid tenantId,
|
||||||
CancellationToken ct,
|
CancellationToken ct,
|
||||||
TokenLimitDefaults? defaults = null)
|
TokenLimitDefaults? defaults)
|
||||||
{
|
{
|
||||||
TenantLimitEntity entity = await LoadOrCreateAsync(tenantId, defaults ?? _defaults, ct);
|
TenantLimitEntity entity = await LoadOrCreateAsync(tenantId, defaults ?? _defaults, ct);
|
||||||
return ToLimitDto(entity);
|
return ToLimitDto(entity);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <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);
|
TenantLimitEntity entity = await LoadOrCreateAsync(tenantId, _defaults, ct);
|
||||||
await ResetIfPeriodExpiredAsync(entity, ct);
|
await ResetIfPeriodExpiredAsync(entity, ct);
|
||||||
@@ -77,7 +77,7 @@ public sealed class TenantLimitStore : ITenantLimitStore
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public async Task<BudgetStateDto> AddUsageAsync(
|
async Task<BudgetStateDto> ITenantLimitStore.AddUsageAsync(
|
||||||
Guid tenantId,
|
Guid tenantId,
|
||||||
long tokens,
|
long tokens,
|
||||||
CancellationToken ct)
|
CancellationToken ct)
|
||||||
@@ -110,7 +110,7 @@ public sealed class TenantLimitStore : ITenantLimitStore
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public async Task<BudgetStateDto> UpdateBudgetAsync(
|
async Task<BudgetStateDto> ITenantLimitStore.UpdateBudgetAsync(
|
||||||
Guid tenantId,
|
Guid tenantId,
|
||||||
long budgetTokens,
|
long budgetTokens,
|
||||||
string period,
|
string period,
|
||||||
@@ -133,7 +133,7 @@ public sealed class TenantLimitStore : ITenantLimitStore
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <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);
|
TenantLimitEntity entity = await LoadOrCreateAsync(tenantId, _defaults, ct);
|
||||||
await ResetIfPeriodExpiredAsync(entity, ct);
|
await ResetIfPeriodExpiredAsync(entity, ct);
|
||||||
@@ -149,7 +149,7 @@ public sealed class TenantLimitStore : ITenantLimitStore
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <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);
|
TenantLimitEntity entity = await LoadOrCreateAsync(tenantId, _defaults, ct);
|
||||||
await ResetIfPeriodExpiredAsync(entity, ct);
|
await ResetIfPeriodExpiredAsync(entity, ct);
|
||||||
@@ -220,7 +220,7 @@ public sealed class TenantLimitStore : ITenantLimitStore
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <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
|
List<TenantLimitEntity> candidates = await _dbContext.TenantLimits
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ public sealed class AesGcmSecretCipher : ISecretCipher
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public string Encrypt(string plainText)
|
string ISecretCipher.Encrypt(string plainText)
|
||||||
{
|
{
|
||||||
if (string.IsNullOrEmpty(plainText))
|
if (string.IsNullOrEmpty(plainText))
|
||||||
{
|
{
|
||||||
@@ -66,7 +66,7 @@ public sealed class AesGcmSecretCipher : ISecretCipher
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public string Decrypt(string cipherText)
|
string ISecretCipher.Decrypt(string cipherText)
|
||||||
{
|
{
|
||||||
if (string.IsNullOrEmpty(cipherText) || !cipherText.StartsWith(EncryptedPrefix, StringComparison.Ordinal))
|
if (string.IsNullOrEmpty(cipherText) || !cipherText.StartsWith(EncryptedPrefix, StringComparison.Ordinal))
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ public sealed class DiscoverySearchErrorCounter : IDiscoverySearchErrorCounter
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public int Next(string taskId)
|
int IDiscoverySearchErrorCounter.Next(string taskId)
|
||||||
{
|
{
|
||||||
EvictExpired();
|
EvictExpired();
|
||||||
Entry fresh = _failures.AddOrUpdate(
|
Entry fresh = _failures.AddOrUpdate(
|
||||||
@@ -56,7 +56,7 @@ public sealed class DiscoverySearchErrorCounter : IDiscoverySearchErrorCounter
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public void Reset(string taskId)
|
void IDiscoverySearchErrorCounter.Reset(string taskId)
|
||||||
{
|
{
|
||||||
EvictExpired();
|
EvictExpired();
|
||||||
_failures.TryRemove(taskId, out _);
|
_failures.TryRemove(taskId, out _);
|
||||||
|
|||||||
@@ -7,6 +7,8 @@ using Deal.SharedKernel.Tenants.Models;
|
|||||||
using Deal.Tests.Unit.Modules.Settings;
|
using Deal.Tests.Unit.Modules.Settings;
|
||||||
using Deal.Tests.Unit.Modules.Tenants;
|
using Deal.Tests.Unit.Modules.Tenants;
|
||||||
using Microsoft.Extensions.Logging.Abstractions;
|
using Microsoft.Extensions.Logging.Abstractions;
|
||||||
|
using Deal.Contracts.Integrations.Abstractions;
|
||||||
|
using Deal.SharedKernel.Tenants.Abstractions;
|
||||||
|
|
||||||
namespace Deal.Tests.Unit.Contracts;
|
namespace Deal.Tests.Unit.Contracts;
|
||||||
|
|
||||||
@@ -147,7 +149,7 @@ public sealed class BudgetedAiClassifierTests
|
|||||||
// Paid: Платный фейк-исполнитель (счётчики/ответы сценария).
|
// Paid: Платный фейк-исполнитель (счётчики/ответы сценария).
|
||||||
// Settings: KV-настройки тенанта (маркеры LocalFieldsParser).
|
// Settings: KV-настройки тенанта (маркеры LocalFieldsParser).
|
||||||
private sealed record Context(
|
private sealed record Context(
|
||||||
BudgetedAiClassifier Decorator,
|
IAiClassifier Decorator,
|
||||||
FakeAiClassifier Paid,
|
FakeAiClassifier Paid,
|
||||||
FakeSettingsStore Settings);
|
FakeSettingsStore Settings);
|
||||||
|
|
||||||
@@ -160,10 +162,10 @@ public sealed class BudgetedAiClassifierTests
|
|||||||
var settings = new FakeSettingsStore();
|
var settings = new FakeSettingsStore();
|
||||||
var limits = new FakeTenantLimitStore();
|
var limits = new FakeTenantLimitStore();
|
||||||
configure?.Invoke(limits);
|
configure?.Invoke(limits);
|
||||||
var tenantContext = new TenantContext();
|
ITenantContext tenantContext = new TenantContext();
|
||||||
tenantContext.SetTenant(new TenantId(TenantIdValue));
|
tenantContext.SetTenant(new TenantId(TenantIdValue));
|
||||||
var paid = new FakeAiClassifier();
|
var paid = new FakeAiClassifier();
|
||||||
var decorator = new BudgetedAiClassifier(
|
IAiClassifier decorator = new BudgetedAiClassifier(
|
||||||
paid,
|
paid,
|
||||||
new LocalAiClassifier(new LocalFieldsParser(settings)),
|
new LocalAiClassifier(new LocalFieldsParser(settings)),
|
||||||
limits,
|
limits,
|
||||||
|
|||||||
@@ -6,6 +6,8 @@ using Deal.Modules.Tenants.Application.Models;
|
|||||||
using Deal.SharedKernel.Tenants.Models;
|
using Deal.SharedKernel.Tenants.Models;
|
||||||
using Deal.Tests.Unit.Modules.Tenants;
|
using Deal.Tests.Unit.Modules.Tenants;
|
||||||
using Microsoft.Extensions.Logging.Abstractions;
|
using Microsoft.Extensions.Logging.Abstractions;
|
||||||
|
using Deal.Contracts.Integrations.Abstractions;
|
||||||
|
using Deal.SharedKernel.Tenants.Abstractions;
|
||||||
|
|
||||||
namespace Deal.Tests.Unit.Contracts;
|
namespace Deal.Tests.Unit.Contracts;
|
||||||
|
|
||||||
@@ -128,7 +130,7 @@ public sealed class BudgetedAiToolsTests
|
|||||||
// Decorator: Декоратор бюджетного гейта.
|
// Decorator: Декоратор бюджетного гейта.
|
||||||
// Paid: Платный фейк-исполнитель (счётчик/ответы сценария).
|
// Paid: Платный фейк-исполнитель (счётчик/ответы сценария).
|
||||||
private sealed record Context(
|
private sealed record Context(
|
||||||
BudgetedAiTools Decorator,
|
IAiTools Decorator,
|
||||||
FakeAiTools Paid);
|
FakeAiTools Paid);
|
||||||
|
|
||||||
// Собирает контекст: платный фейк + фейк лимитов и tenant-контекст (как регистрирует
|
// Собирает контекст: платный фейк + фейк лимитов и tenant-контекст (как регистрирует
|
||||||
@@ -139,10 +141,10 @@ public sealed class BudgetedAiToolsTests
|
|||||||
{
|
{
|
||||||
var limits = new FakeTenantLimitStore();
|
var limits = new FakeTenantLimitStore();
|
||||||
configure?.Invoke(limits);
|
configure?.Invoke(limits);
|
||||||
var tenantContext = new TenantContext();
|
ITenantContext tenantContext = new TenantContext();
|
||||||
tenantContext.SetTenant(new TenantId(TenantIdValue));
|
tenantContext.SetTenant(new TenantId(TenantIdValue));
|
||||||
var paid = new FakeAiTools();
|
var paid = new FakeAiTools();
|
||||||
var decorator = new BudgetedAiTools(
|
IAiTools decorator = new BudgetedAiTools(
|
||||||
paid,
|
paid,
|
||||||
limits,
|
limits,
|
||||||
tenantContext,
|
tenantContext,
|
||||||
|
|||||||
@@ -13,6 +13,8 @@ using Deal.Tests.Unit.Modules.Settings;
|
|||||||
using Deal.Tests.Unit.Modules.Tenants;
|
using Deal.Tests.Unit.Modules.Tenants;
|
||||||
using Deal.Tests.Unit.Support;
|
using Deal.Tests.Unit.Support;
|
||||||
using Microsoft.Extensions.Logging.Abstractions;
|
using Microsoft.Extensions.Logging.Abstractions;
|
||||||
|
using Deal.Contracts.Integrations.Abstractions;
|
||||||
|
using Deal.SharedKernel.Tenants.Abstractions;
|
||||||
|
|
||||||
namespace Deal.Tests.Unit.Contracts;
|
namespace Deal.Tests.Unit.Contracts;
|
||||||
|
|
||||||
@@ -40,7 +42,7 @@ public sealed class GrpcAiToolsTests
|
|||||||
FakeSecretCipher cipher = new();
|
FakeSecretCipher cipher = new();
|
||||||
FakeTenantLimitStore limits = 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);
|
AiGenerateKeywordsResultDto result = await tools.GenerateKeywordsAsync("Бэкенд-разработка на Python", CancellationToken.None);
|
||||||
|
|
||||||
Assert.True(result.Ok);
|
Assert.True(result.Ok);
|
||||||
@@ -63,7 +65,7 @@ public sealed class GrpcAiToolsTests
|
|||||||
await AiGrpcTestHost.RunAsync(AiGrpcTestHost.DefaultToken, new RecordingAiService(), async (port, service) =>
|
await AiGrpcTestHost.RunAsync(AiGrpcTestHost.DefaultToken, new RecordingAiService(), async (port, service) =>
|
||||||
{
|
{
|
||||||
service.KeywordsUnavailable = true;
|
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);
|
AiGenerateKeywordsResultDto result = await tools.GenerateKeywordsAsync("описание", CancellationToken.None);
|
||||||
|
|
||||||
@@ -86,7 +88,7 @@ public sealed class GrpcAiToolsTests
|
|||||||
Usage = new Usage { Prompt = 200, Completion = 10, Total = 210 },
|
Usage = new Usage { Prompt = 200, Completion = 10, Total = 210 },
|
||||||
};
|
};
|
||||||
FakeTenantLimitStore limits = new();
|
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(
|
AiEvaluateFitResultDto result = await tools.EvaluateFitAsync(
|
||||||
"Ищу дизайнера для лендинга", "Разработка сайтов на Python", new[] { "python", "бэкенд" }, CancellationToken.None);
|
"Ищу дизайнера для лендинга", "Разработка сайтов на Python", new[] { "python", "бэкенд" }, CancellationToken.None);
|
||||||
@@ -110,7 +112,7 @@ public sealed class GrpcAiToolsTests
|
|||||||
await AiGrpcTestHost.RunAsync(AiGrpcTestHost.DefaultToken, new RecordingAiService(), async (port, service) =>
|
await AiGrpcTestHost.RunAsync(AiGrpcTestHost.DefaultToken, new RecordingAiService(), async (port, service) =>
|
||||||
{
|
{
|
||||||
service.FitUnavailable = true;
|
service.FitUnavailable = true;
|
||||||
GrpcAiTools tools = CreateTools(port, new FakeSettingsStore(), new FakeSecretCipher());
|
IAiTools tools = CreateTools(port, new FakeSettingsStore(), new FakeSecretCipher());
|
||||||
|
|
||||||
await Assert.ThrowsAsync<AiUnavailableException>(
|
await Assert.ThrowsAsync<AiUnavailableException>(
|
||||||
() => tools.EvaluateFitAsync("текст", "описание", new[] { "ключ" }, CancellationToken.None));
|
() => tools.EvaluateFitAsync("текст", "описание", new[] { "ключ" }, CancellationToken.None));
|
||||||
@@ -129,7 +131,7 @@ public sealed class GrpcAiToolsTests
|
|||||||
FakeSecretCipher cipher,
|
FakeSecretCipher cipher,
|
||||||
FakeTenantLimitStore? limits = null)
|
FakeTenantLimitStore? limits = null)
|
||||||
{
|
{
|
||||||
var tenantContext = new TenantContext();
|
ITenantContext tenantContext = new TenantContext();
|
||||||
tenantContext.SetTenant(new TenantId(TenantIdValue));
|
tenantContext.SetTenant(new TenantId(TenantIdValue));
|
||||||
var connection = new AiGrpcConnection(new AiServiceOptions { UseLocal = false, Endpoint = $"http://127.0.0.1:{port}" });
|
var connection = new AiGrpcConnection(new AiServiceOptions { UseLocal = false, Endpoint = $"http://127.0.0.1:{port}" });
|
||||||
return new GrpcAiTools(
|
return new GrpcAiTools(
|
||||||
|
|||||||
@@ -15,6 +15,8 @@ using Deal.Tests.Unit.Modules.Settings;
|
|||||||
using Deal.Tests.Unit.Modules.Tenants;
|
using Deal.Tests.Unit.Modules.Tenants;
|
||||||
using Deal.Tests.Unit.Support;
|
using Deal.Tests.Unit.Support;
|
||||||
using Microsoft.Extensions.Logging.Abstractions;
|
using Microsoft.Extensions.Logging.Abstractions;
|
||||||
|
using Deal.Contracts.Integrations.Abstractions;
|
||||||
|
using Deal.SharedKernel.Tenants.Abstractions;
|
||||||
|
|
||||||
namespace Deal.Tests.Unit.Contracts;
|
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 },
|
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);
|
MlPredictResultDto result = await client.PredictAsync("нужен middle python разработчик", CancellationToken.None);
|
||||||
|
|
||||||
Assert.True(result.Take);
|
Assert.True(result.Take);
|
||||||
@@ -78,7 +80,7 @@ public sealed class GrpcMlClientTests
|
|||||||
{
|
{
|
||||||
service.PredictUnavailable = true;
|
service.PredictUnavailable = true;
|
||||||
|
|
||||||
GrpcMlClient client = CreateClient(port);
|
IMlClient client = CreateClient(port);
|
||||||
MlPredictResultDto result = await client.PredictAsync("текст", CancellationToken.None);
|
MlPredictResultDto result = await client.PredictAsync("текст", CancellationToken.None);
|
||||||
|
|
||||||
Assert.False(result.Take);
|
Assert.False(result.Take);
|
||||||
@@ -113,7 +115,7 @@ public sealed class GrpcMlClientTests
|
|||||||
settings.Preload(SettingsKeys.AiDecisions, "3");
|
settings.Preload(SettingsKeys.AiDecisions, "3");
|
||||||
var learning = new FakeMlLearningStore { LearningCount = 9 };
|
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);
|
MlStatusResponseDto status = await client.StatusAsync(CancellationToken.None);
|
||||||
|
|
||||||
Assert.True(status.Enabled); // mlEnabled не задан — дефолт true
|
Assert.True(status.Enabled); // mlEnabled не задан — дефолт true
|
||||||
@@ -139,7 +141,7 @@ public sealed class GrpcMlClientTests
|
|||||||
{
|
{
|
||||||
DateTimeOffset now = DateTimeOffset.UtcNow;
|
DateTimeOffset now = DateTimeOffset.UtcNow;
|
||||||
var cache = new MlStatusCache(() => now);
|
var cache = new MlStatusCache(() => now);
|
||||||
GrpcMlClient client = CreateClient(port, cache: cache);
|
IMlClient client = CreateClient(port, cache: cache);
|
||||||
|
|
||||||
service.StatusUnavailable = true;
|
service.StatusUnavailable = true;
|
||||||
MlStatusResponseDto down = await client.StatusAsync(CancellationToken.None);
|
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) =>
|
await MlGrpcTestHost.RunAsync(MlGrpcTestHost.DefaultToken, new RecordingMlService(), async (port, service) =>
|
||||||
{
|
{
|
||||||
DateTimeOffset now = DateTimeOffset.UtcNow;
|
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);
|
_ = await client.StatusAsync(CancellationToken.None);
|
||||||
now = now.AddSeconds(5); // в пределах TTL 15 с — повторный вызов не ходит в сервис
|
now = now.AddSeconds(5); // в пределах TTL 15 с — повторный вызов не ходит в сервис
|
||||||
@@ -196,7 +198,7 @@ public sealed class GrpcMlClientTests
|
|||||||
var learning = new FakeMlLearningStore();
|
var learning = new FakeMlLearningStore();
|
||||||
learning.SeedOutbox("mle_1", "текст 1", "b_a", 1.0);
|
learning.SeedOutbox("mle_1", "текст 1", "b_a", 1.0);
|
||||||
learning.SeedOutbox("mle_2", "текст 2", "spam", 1.0);
|
learning.SeedOutbox("mle_2", "текст 2", "spam", 1.0);
|
||||||
GrpcMlClient client = CreateClient(port, learning: learning);
|
IMlClient client = CreateClient(port, learning: learning);
|
||||||
|
|
||||||
// Прогреть кэш статуса (до сброса — 1 вызов Status), затем сброс.
|
// Прогреть кэш статуса (до сброса — 1 вызов Status), затем сброс.
|
||||||
_ = await client.StatusAsync(CancellationToken.None);
|
_ = await client.StatusAsync(CancellationToken.None);
|
||||||
@@ -221,7 +223,7 @@ public sealed class GrpcMlClientTests
|
|||||||
var learning = new FakeMlLearningStore();
|
var learning = new FakeMlLearningStore();
|
||||||
learning.SeedOutbox("mle_1", "текст", "b_a", 1.0);
|
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);
|
MlResetResultDto reset = await client.ResetAsync(CancellationToken.None);
|
||||||
|
|
||||||
Assert.False(reset.Ok);
|
Assert.False(reset.Ok);
|
||||||
@@ -239,7 +241,7 @@ public sealed class GrpcMlClientTests
|
|||||||
var learning = new FakeMlLearningStore();
|
var learning = new FakeMlLearningStore();
|
||||||
learning.SeedOutbox("mle_1", "текст", "spam", 1.0);
|
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);
|
MlResetResultDto reset = await client.ResetAsync(CancellationToken.None);
|
||||||
|
|
||||||
Assert.False(reset.Ok);
|
Assert.False(reset.Ok);
|
||||||
@@ -255,7 +257,7 @@ public sealed class GrpcMlClientTests
|
|||||||
await MlGrpcTestHost.RunAsync(MlGrpcTestHost.DefaultToken, new RecordingMlService(), async (port, service) =>
|
await MlGrpcTestHost.RunAsync(MlGrpcTestHost.DefaultToken, new RecordingMlService(), async (port, service) =>
|
||||||
{
|
{
|
||||||
var learning = new FakeMlLearningStore();
|
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);
|
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) =>
|
await MlGrpcTestHost.RunAsync(MlGrpcTestHost.DefaultToken, new RecordingMlService(), async (port, service) =>
|
||||||
{
|
{
|
||||||
GrpcMlClient client = CreateClient(port);
|
IMlClient client = CreateClient(port);
|
||||||
var items = new[]
|
var items = new[]
|
||||||
{
|
{
|
||||||
new MlOutboxEntryDto("mle_1", "текст 1", "b_a", 1.0),
|
new MlOutboxEntryDto("mle_1", "текст 1", "b_a", 1.0),
|
||||||
@@ -311,7 +313,7 @@ public sealed class GrpcMlClientTests
|
|||||||
FakeMlLearningStore? learning = null,
|
FakeMlLearningStore? learning = null,
|
||||||
MlStatusCache? cache = null)
|
MlStatusCache? cache = null)
|
||||||
{
|
{
|
||||||
var tenantContext = new TenantContext();
|
ITenantContext tenantContext = new TenantContext();
|
||||||
tenantContext.SetTenant(new TenantId(TenantIdValue));
|
tenantContext.SetTenant(new TenantId(TenantIdValue));
|
||||||
var options = new MlServiceOptions { UseLocal = false, Endpoint = $"http://127.0.0.1:{port}" };
|
var options = new MlServiceOptions { UseLocal = false, Endpoint = $"http://127.0.0.1:{port}" };
|
||||||
return new GrpcMlClient(
|
return new GrpcMlClient(
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ using Deal.Tests.Unit.Grpc;
|
|||||||
using Deal.Tests.Unit.Support;
|
using Deal.Tests.Unit.Support;
|
||||||
using Grpc.Core;
|
using Grpc.Core;
|
||||||
using Microsoft.Extensions.Logging.Abstractions;
|
using Microsoft.Extensions.Logging.Abstractions;
|
||||||
|
using Deal.SharedKernel.Tenants.Abstractions;
|
||||||
|
|
||||||
namespace Deal.Tests.Unit.Contracts;
|
namespace Deal.Tests.Unit.Contracts;
|
||||||
|
|
||||||
@@ -235,7 +236,7 @@ public sealed class GrpcTelegramClientTests
|
|||||||
});
|
});
|
||||||
using (connection)
|
using (connection)
|
||||||
{
|
{
|
||||||
var gateway = new GrpcTelegramClient(
|
ITelegramGateway gateway = new GrpcTelegramClient(
|
||||||
new TenantContext(), connection, NullLogger<GrpcTelegramClient>.Instance);
|
new TenantContext(), connection, NullLogger<GrpcTelegramClient>.Instance);
|
||||||
await Assert.ThrowsAsync<InvalidOperationException>(() => gateway.StatusAsync(CancellationToken.None));
|
await Assert.ThrowsAsync<InvalidOperationException>(() => gateway.StatusAsync(CancellationToken.None));
|
||||||
}
|
}
|
||||||
@@ -247,7 +248,7 @@ public sealed class GrpcTelegramClientTests
|
|||||||
// Возвращает: Гейт (транспорт живёт на время сценария).
|
// Возвращает: Гейт (транспорт живёт на время сценария).
|
||||||
private static GrpcTelegramClient CreateClient(int port)
|
private static GrpcTelegramClient CreateClient(int port)
|
||||||
{
|
{
|
||||||
var tenantContext = new TenantContext();
|
ITenantContext tenantContext = new TenantContext();
|
||||||
tenantContext.SetTenant(new TenantId(Guid.NewGuid().ToString("N")));
|
tenantContext.SetTenant(new TenantId(Guid.NewGuid().ToString("N")));
|
||||||
var connection = new TelegramGrpcConnection(new TelegramServiceOptions
|
var connection = new TelegramGrpcConnection(new TelegramServiceOptions
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -141,7 +141,7 @@ public sealed class IntegrationsDiTests
|
|||||||
TelegramServiceOptions telegramOptions)
|
TelegramServiceOptions telegramOptions)
|
||||||
{
|
{
|
||||||
var services = new ServiceCollection();
|
var services = new ServiceCollection();
|
||||||
var tenantContext = new TenantContext();
|
ITenantContext tenantContext = new TenantContext();
|
||||||
tenantContext.SetTenant(new TenantId(Guid.NewGuid().ToString("N")));
|
tenantContext.SetTenant(new TenantId(Guid.NewGuid().ToString("N")));
|
||||||
services.AddSingleton<ITenantContext>(tenantContext);
|
services.AddSingleton<ITenantContext>(tenantContext);
|
||||||
services.AddScoped<ISettingsStore>(_ => new FakeSettingsStore());
|
services.AddScoped<ISettingsStore>(_ => new FakeSettingsStore());
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
using Deal.Infrastructure.Integrations.Services;
|
using Deal.Infrastructure.Integrations.Services;
|
||||||
|
using Deal.Contracts.Integrations.Abstractions;
|
||||||
|
|
||||||
namespace Deal.Tests.Unit.Contracts;
|
namespace Deal.Tests.Unit.Contracts;
|
||||||
|
|
||||||
@@ -7,7 +8,7 @@ namespace Deal.Tests.Unit.Contracts;
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public sealed class LocalAiToolsTests
|
public sealed class LocalAiToolsTests
|
||||||
{
|
{
|
||||||
private readonly LocalAiTools _tools = new();
|
private readonly IAiTools _tools = new LocalAiTools();
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task GenerateKeywordsAsync_AlwaysThrowsNotSupported()
|
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.Modules.Tenants;
|
||||||
using Deal.Tests.Unit.Support;
|
using Deal.Tests.Unit.Support;
|
||||||
using Microsoft.Extensions.Logging.Abstractions;
|
using Microsoft.Extensions.Logging.Abstractions;
|
||||||
|
using Deal.SharedKernel.Tenants.Abstractions;
|
||||||
|
|
||||||
namespace Deal.Tests.Unit.Contracts;
|
namespace Deal.Tests.Unit.Contracts;
|
||||||
|
|
||||||
@@ -169,7 +170,7 @@ public sealed class PipelineWorkerGrpcAiTests
|
|||||||
var rules = new IncomingRules(settings);
|
var rules = new IncomingRules(settings);
|
||||||
var fieldsParser = new LocalFieldsParser(settings);
|
var fieldsParser = new LocalFieldsParser(settings);
|
||||||
|
|
||||||
var tenantContext = new TenantContext();
|
ITenantContext tenantContext = new TenantContext();
|
||||||
tenantContext.SetTenant(new TenantId(TenantIdValue));
|
tenantContext.SetTenant(new TenantId(TenantIdValue));
|
||||||
var connection = new AiGrpcConnection(
|
var connection = new AiGrpcConnection(
|
||||||
new AiServiceOptions { UseLocal = false, Endpoint = $"http://127.0.0.1:{port}" });
|
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.Models;
|
||||||
using Deal.Modules.Tenants.Application.Services;
|
using Deal.Modules.Tenants.Application.Services;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Deal.Modules.Tenants.Application.Abstractions;
|
||||||
|
|
||||||
namespace Deal.Tests.Unit.Infrastructure;
|
namespace Deal.Tests.Unit.Infrastructure;
|
||||||
|
|
||||||
@@ -18,7 +19,7 @@ public sealed class TenantLimitStoreTests
|
|||||||
[Fact]
|
[Fact]
|
||||||
public async Task GetOrCreateAsync_NoRow_CreatesDefaultBudget()
|
public async Task GetOrCreateAsync_NoRow_CreatesDefaultBudget()
|
||||||
{
|
{
|
||||||
(DealDbContext db, TenantLimitStore store) = CreateStore();
|
(DealDbContext db, ITenantLimitStore store) = CreateStore();
|
||||||
var tenantId = Guid.NewGuid();
|
var tenantId = Guid.NewGuid();
|
||||||
AddTenant(db, tenantId, TenantStatuses.Active);
|
AddTenant(db, tenantId, TenantStatuses.Active);
|
||||||
|
|
||||||
@@ -39,7 +40,7 @@ public sealed class TenantLimitStoreTests
|
|||||||
[Fact]
|
[Fact]
|
||||||
public async Task GetOrCreateAsync_WithExplicitDefaults_UsesThem()
|
public async Task GetOrCreateAsync_WithExplicitDefaults_UsesThem()
|
||||||
{
|
{
|
||||||
(DealDbContext db, TenantLimitStore store) = CreateStore();
|
(DealDbContext db, ITenantLimitStore store) = CreateStore();
|
||||||
var tenantId = Guid.NewGuid();
|
var tenantId = Guid.NewGuid();
|
||||||
AddTenant(db, tenantId, TenantStatuses.Active);
|
AddTenant(db, tenantId, TenantStatuses.Active);
|
||||||
|
|
||||||
@@ -54,7 +55,7 @@ public sealed class TenantLimitStoreTests
|
|||||||
public async Task GetStateAsync_ReturnsStatusAndAllowed()
|
public async Task GetStateAsync_ReturnsStatusAndAllowed()
|
||||||
{
|
{
|
||||||
var tenantId = Guid.NewGuid();
|
var tenantId = Guid.NewGuid();
|
||||||
(DealDbContext db, TenantLimitStore store) = CreateStore();
|
(DealDbContext db, ITenantLimitStore store) = CreateStore();
|
||||||
|
|
||||||
// Активный тенант, расход ниже бюджета → Allowed=true.
|
// Активный тенант, расход ниже бюджета → Allowed=true.
|
||||||
AddTenant(db, tenantId, TenantStatuses.Active);
|
AddTenant(db, tenantId, TenantStatuses.Active);
|
||||||
@@ -75,7 +76,7 @@ public sealed class TenantLimitStoreTests
|
|||||||
public async Task AddUsageAsync_WhenPeriodExpired_ResetsUsedAndStartsNewPeriod()
|
public async Task AddUsageAsync_WhenPeriodExpired_ResetsUsedAndStartsNewPeriod()
|
||||||
{
|
{
|
||||||
var tenantId = Guid.NewGuid();
|
var tenantId = Guid.NewGuid();
|
||||||
(DealDbContext db, TenantLimitStore store) = CreateStore(clock: () => Now);
|
(DealDbContext db, ITenantLimitStore store) = CreateStore(clock: () => Now);
|
||||||
AddTenant(db, tenantId, TenantStatuses.Active);
|
AddTenant(db, tenantId, TenantStatuses.Active);
|
||||||
AddLimit(db, tenantId, budgetTokens: 1000, TenantLimitPeriods.Month, periodStart: Now.AddMonths(-2), usedTokens: 700, warned80: true, notifiedExhausted: true);
|
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()
|
public async Task AddUsageAsync_WithinPeriod_Accumulates()
|
||||||
{
|
{
|
||||||
var tenantId = Guid.NewGuid();
|
var tenantId = Guid.NewGuid();
|
||||||
(DealDbContext db, TenantLimitStore store) = CreateStore(clock: () => Now);
|
(DealDbContext db, ITenantLimitStore store) = CreateStore(clock: () => Now);
|
||||||
AddTenant(db, tenantId, TenantStatuses.Active);
|
AddTenant(db, tenantId, TenantStatuses.Active);
|
||||||
AddLimit(db, tenantId, budgetTokens: 1000, TenantLimitPeriods.Month, Now, usedTokens: 300);
|
AddLimit(db, tenantId, budgetTokens: 1000, TenantLimitPeriods.Month, Now, usedTokens: 300);
|
||||||
|
|
||||||
@@ -107,7 +108,7 @@ public sealed class TenantLimitStoreTests
|
|||||||
public async Task AddUsageAsync_Crossing80Percent_DoesNotSetWarned80()
|
public async Task AddUsageAsync_Crossing80Percent_DoesNotSetWarned80()
|
||||||
{
|
{
|
||||||
var tenantId = Guid.NewGuid();
|
var tenantId = Guid.NewGuid();
|
||||||
(DealDbContext db, TenantLimitStore store) = CreateStore(clock: () => Now);
|
(DealDbContext db, ITenantLimitStore store) = CreateStore(clock: () => Now);
|
||||||
AddTenant(db, tenantId, TenantStatuses.Active);
|
AddTenant(db, tenantId, TenantStatuses.Active);
|
||||||
AddLimit(db, tenantId, budgetTokens: 1000, TenantLimitPeriods.Month, Now, usedTokens: 700);
|
AddLimit(db, tenantId, budgetTokens: 1000, TenantLimitPeriods.Month, Now, usedTokens: 700);
|
||||||
|
|
||||||
@@ -123,7 +124,7 @@ public sealed class TenantLimitStoreTests
|
|||||||
public async Task AddUsageAsync_Exhaustion_DoesNotSetFlagsButDisallows()
|
public async Task AddUsageAsync_Exhaustion_DoesNotSetFlagsButDisallows()
|
||||||
{
|
{
|
||||||
var tenantId = Guid.NewGuid();
|
var tenantId = Guid.NewGuid();
|
||||||
(DealDbContext db, TenantLimitStore store) = CreateStore(clock: () => Now);
|
(DealDbContext db, ITenantLimitStore store) = CreateStore(clock: () => Now);
|
||||||
AddTenant(db, tenantId, TenantStatuses.Active);
|
AddTenant(db, tenantId, TenantStatuses.Active);
|
||||||
AddLimit(db, tenantId, budgetTokens: 1000, TenantLimitPeriods.Month, Now, usedTokens: 950);
|
AddLimit(db, tenantId, budgetTokens: 1000, TenantLimitPeriods.Month, Now, usedTokens: 950);
|
||||||
|
|
||||||
@@ -140,7 +141,7 @@ public sealed class TenantLimitStoreTests
|
|||||||
{
|
{
|
||||||
// Нулевой расход не инкрементирует, но строка лимита заводится лениво (закрыт путь записи).
|
// Нулевой расход не инкрементирует, но строка лимита заводится лениво (закрыт путь записи).
|
||||||
var tenantId = Guid.NewGuid();
|
var tenantId = Guid.NewGuid();
|
||||||
(DealDbContext db, TenantLimitStore store) = CreateStore(clock: () => Now);
|
(DealDbContext db, ITenantLimitStore store) = CreateStore(clock: () => Now);
|
||||||
AddTenant(db, tenantId, TenantStatuses.Active);
|
AddTenant(db, tenantId, TenantStatuses.Active);
|
||||||
|
|
||||||
BudgetStateDto state = await store.AddUsageAsync(tenantId, tokens: 0, CancellationToken.None);
|
BudgetStateDto state = await store.AddUsageAsync(tenantId, tokens: 0, CancellationToken.None);
|
||||||
@@ -153,7 +154,7 @@ public sealed class TenantLimitStoreTests
|
|||||||
public async Task UpdateBudgetAsync_AppliesNewBudgetAndPeriod_AndResetsFlags()
|
public async Task UpdateBudgetAsync_AppliesNewBudgetAndPeriod_AndResetsFlags()
|
||||||
{
|
{
|
||||||
var tenantId = Guid.NewGuid();
|
var tenantId = Guid.NewGuid();
|
||||||
(DealDbContext db, TenantLimitStore store) = CreateStore(clock: () => Now);
|
(DealDbContext db, ITenantLimitStore store) = CreateStore(clock: () => Now);
|
||||||
AddTenant(db, tenantId, TenantStatuses.Active);
|
AddTenant(db, tenantId, TenantStatuses.Active);
|
||||||
AddLimit(db, tenantId, budgetTokens: 1000, TenantLimitPeriods.Month, Now, usedTokens: 900, warned80: true, notifiedExhausted: true);
|
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()
|
public async Task UpdateBudgetAsync_InvalidArguments_Throw()
|
||||||
{
|
{
|
||||||
var tenantId = Guid.NewGuid();
|
var tenantId = Guid.NewGuid();
|
||||||
(DealDbContext db, TenantLimitStore store) = CreateStore(clock: () => Now);
|
(DealDbContext db, ITenantLimitStore store) = CreateStore(clock: () => Now);
|
||||||
AddTenant(db, tenantId, TenantStatuses.Active);
|
AddTenant(db, tenantId, TenantStatuses.Active);
|
||||||
|
|
||||||
await Assert.ThrowsAsync<ArgumentOutOfRangeException>(
|
await Assert.ThrowsAsync<ArgumentOutOfRangeException>(
|
||||||
@@ -183,7 +184,7 @@ public sealed class TenantLimitStoreTests
|
|||||||
public async Task TryMarkWarnedAsync_MarksOnceAtThreshold()
|
public async Task TryMarkWarnedAsync_MarksOnceAtThreshold()
|
||||||
{
|
{
|
||||||
var tenantId = Guid.NewGuid();
|
var tenantId = Guid.NewGuid();
|
||||||
(DealDbContext db, TenantLimitStore store) = CreateStore(clock: () => Now);
|
(DealDbContext db, ITenantLimitStore store) = CreateStore(clock: () => Now);
|
||||||
AddTenant(db, tenantId, TenantStatuses.Active);
|
AddTenant(db, tenantId, TenantStatuses.Active);
|
||||||
AddLimit(db, tenantId, budgetTokens: 1000, TenantLimitPeriods.Month, Now, usedTokens: 800);
|
AddLimit(db, tenantId, budgetTokens: 1000, TenantLimitPeriods.Month, Now, usedTokens: 800);
|
||||||
|
|
||||||
@@ -203,7 +204,7 @@ public sealed class TenantLimitStoreTests
|
|||||||
public async Task TryMarkNotifiedExhaustedAsync_MarksOnceAtExhaustion()
|
public async Task TryMarkNotifiedExhaustedAsync_MarksOnceAtExhaustion()
|
||||||
{
|
{
|
||||||
var tenantId = Guid.NewGuid();
|
var tenantId = Guid.NewGuid();
|
||||||
(DealDbContext db, TenantLimitStore store) = CreateStore(clock: () => Now);
|
(DealDbContext db, ITenantLimitStore store) = CreateStore(clock: () => Now);
|
||||||
AddTenant(db, tenantId, TenantStatuses.Active);
|
AddTenant(db, tenantId, TenantStatuses.Active);
|
||||||
AddLimit(db, tenantId, budgetTokens: 1000, TenantLimitPeriods.Month, Now, usedTokens: 1000);
|
AddLimit(db, tenantId, budgetTokens: 1000, TenantLimitPeriods.Month, Now, usedTokens: 1000);
|
||||||
|
|
||||||
@@ -218,7 +219,7 @@ public sealed class TenantLimitStoreTests
|
|||||||
[Fact]
|
[Fact]
|
||||||
public async Task ResetExpiredPeriodsAsync_ResetsOnlyExpiredPeriods()
|
public async Task ResetExpiredPeriodsAsync_ResetsOnlyExpiredPeriods()
|
||||||
{
|
{
|
||||||
(DealDbContext db, TenantLimitStore store) = CreateStore();
|
(DealDbContext db, ITenantLimitStore store) = CreateStore();
|
||||||
var expiredTenant = Guid.NewGuid();
|
var expiredTenant = Guid.NewGuid();
|
||||||
var currentTenant = Guid.NewGuid();
|
var currentTenant = Guid.NewGuid();
|
||||||
// Период месяца начат за 2 месяца назад → истёк к Now; расход и флаги должны обнулиться.
|
// Период месяца начат за 2 месяца назад → истёк к Now; расход и флаги должны обнулиться.
|
||||||
@@ -244,13 +245,13 @@ public sealed class TenantLimitStoreTests
|
|||||||
// Создаёт адаптер на уникальной InMemory-БД с фиксированными/системными часами.
|
// Создаёт адаптер на уникальной InMemory-БД с фиксированными/системными часами.
|
||||||
// clock: Источник «сейчас» (null — системные часы).
|
// clock: Источник «сейчас» (null — системные часы).
|
||||||
// Возвращает: Кортеж (контекст для seed/проверок, адаптер).
|
// Возвращает: Кортеж (контекст для 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>()
|
var options = new DbContextOptionsBuilder<DealDbContext>()
|
||||||
.UseInMemoryDatabase(Guid.NewGuid().ToString("N"))
|
.UseInMemoryDatabase(Guid.NewGuid().ToString("N"))
|
||||||
.Options;
|
.Options;
|
||||||
var db = new DealDbContext(options);
|
var db = new DealDbContext(options);
|
||||||
var store = new TenantLimitStore(
|
ITenantLimitStore store = new TenantLimitStore(
|
||||||
db, TokenBudgetDefaults.Default, new TokenBudgetService(), clock ?? (() => DateTimeOffset.UtcNow));
|
db, TokenBudgetDefaults.Default, new TokenBudgetService(), clock ?? (() => DateTimeOffset.UtcNow));
|
||||||
return (db, store);
|
return (db, store);
|
||||||
}
|
}
|
||||||
|
|||||||
+5
-4
@@ -1,4 +1,5 @@
|
|||||||
using Deal.Modules.Discovery.Application.Services;
|
using Deal.Modules.Discovery.Application.Services;
|
||||||
|
using Deal.Modules.Discovery.Application.Abstractions;
|
||||||
|
|
||||||
namespace Deal.Tests.Unit.Modules.Discovery;
|
namespace Deal.Tests.Unit.Modules.Discovery;
|
||||||
|
|
||||||
@@ -10,7 +11,7 @@ public sealed class DiscoverySearchErrorCounterTests
|
|||||||
[Fact]
|
[Fact]
|
||||||
public void Next_IncrementsPerTask_ResetClears()
|
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(1, counter.Next("dt_1"));
|
||||||
Assert.Equal(2, counter.Next("dt_1"));
|
Assert.Equal(2, counter.Next("dt_1"));
|
||||||
@@ -26,7 +27,7 @@ public sealed class DiscoverySearchErrorCounterTests
|
|||||||
public void Next_ExpiredEntry_EvictsAndStartsFromOne()
|
public void Next_ExpiredEntry_EvictsAndStartsFromOne()
|
||||||
{
|
{
|
||||||
var now = new DateTimeOffset(2026, 9, 9, 12, 0, 0, TimeSpan.Zero);
|
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(1, counter.Next("dt_1"));
|
||||||
Assert.Equal(2, counter.Next("dt_1"));
|
Assert.Equal(2, counter.Next("dt_1"));
|
||||||
@@ -41,7 +42,7 @@ public sealed class DiscoverySearchErrorCounterTests
|
|||||||
public void Next_FreshEntry_NotEvictedAfterTtlReset()
|
public void Next_FreshEntry_NotEvictedAfterTtlReset()
|
||||||
{
|
{
|
||||||
var now = new DateTimeOffset(2026, 9, 9, 12, 0, 0, TimeSpan.Zero);
|
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(1, counter.Next("dt_1"));
|
||||||
|
|
||||||
@@ -57,7 +58,7 @@ public sealed class DiscoverySearchErrorCounterTests
|
|||||||
public void Reset_EvictsExpiredOfOtherTasks()
|
public void Reset_EvictsExpiredOfOtherTasks()
|
||||||
{
|
{
|
||||||
var now = new DateTimeOffset(2026, 9, 9, 12, 0, 0, TimeSpan.Zero);
|
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_1"); // устареет
|
||||||
counter.Next("dt_2"); // свежая
|
counter.Next("dt_2"); // свежая
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
using System.Net;
|
using System.Net;
|
||||||
using Deal.Infrastructure.Integrations.Services;
|
using Deal.Infrastructure.Integrations.Services;
|
||||||
using Deal.Modules.Settings.Application.Models;
|
using Deal.Modules.Settings.Application.Models;
|
||||||
|
using Deal.Modules.Settings.Application.Abstractions;
|
||||||
|
|
||||||
namespace Deal.Tests.Unit.Support;
|
namespace Deal.Tests.Unit.Support;
|
||||||
|
|
||||||
@@ -15,7 +16,7 @@ public sealed class AiConnectionCheckerTests
|
|||||||
public async Task CheckAsync_CloudProviderWithoutKey_ReturnsNoKeyMessageAndNoHttp()
|
public async Task CheckAsync_CloudProviderWithoutKey_ReturnsNoKeyMessageAndNoHttp()
|
||||||
{
|
{
|
||||||
StubHttpMessageHandler handler = CreateHandler(HttpStatusCode.OK);
|
StubHttpMessageHandler handler = CreateHandler(HttpStatusCode.OK);
|
||||||
AiConnectionChecker checker = CreateChecker(handler);
|
IAiConnectionChecker checker = CreateChecker(handler);
|
||||||
|
|
||||||
AiCheckResultDto result = await checker.CheckAsync(CloudDeepSeekRequest(), CancellationToken.None);
|
AiCheckResultDto result = await checker.CheckAsync(CloudDeepSeekRequest(), CancellationToken.None);
|
||||||
|
|
||||||
@@ -33,7 +34,7 @@ public sealed class AiConnectionCheckerTests
|
|||||||
public async Task CheckAsync_LocalProvider_ReturnsOkLocalServerMessageAndNoHttp()
|
public async Task CheckAsync_LocalProvider_ReturnsOkLocalServerMessageAndNoHttp()
|
||||||
{
|
{
|
||||||
StubHttpMessageHandler handler = CreateHandler(HttpStatusCode.OK);
|
StubHttpMessageHandler handler = CreateHandler(HttpStatusCode.OK);
|
||||||
AiConnectionChecker checker = CreateChecker(handler);
|
IAiConnectionChecker checker = CreateChecker(handler);
|
||||||
var request = new AiCheckRequest(
|
var request = new AiCheckRequest(
|
||||||
ProviderId: "ollama",
|
ProviderId: "ollama",
|
||||||
BaseUrl: "http://localhost:11434/v1",
|
BaseUrl: "http://localhost:11434/v1",
|
||||||
@@ -57,7 +58,7 @@ public sealed class AiConnectionCheckerTests
|
|||||||
public async Task CheckAsync_UnknownProvider_ReturnsProviderNotAllowedAndNoHttp()
|
public async Task CheckAsync_UnknownProvider_ReturnsProviderNotAllowedAndNoHttp()
|
||||||
{
|
{
|
||||||
StubHttpMessageHandler handler = CreateHandler(HttpStatusCode.OK);
|
StubHttpMessageHandler handler = CreateHandler(HttpStatusCode.OK);
|
||||||
AiConnectionChecker checker = CreateChecker(handler);
|
IAiConnectionChecker checker = CreateChecker(handler);
|
||||||
var request = new AiCheckRequest(
|
var request = new AiCheckRequest(
|
||||||
ProviderId: "unknown-provider",
|
ProviderId: "unknown-provider",
|
||||||
BaseUrl: "https://example.com",
|
BaseUrl: "https://example.com",
|
||||||
@@ -77,7 +78,7 @@ public sealed class AiConnectionCheckerTests
|
|||||||
public async Task CheckAsync_NonHttpBaseUrl_ReturnsInvalidBaseUrlAndNoHttp()
|
public async Task CheckAsync_NonHttpBaseUrl_ReturnsInvalidBaseUrlAndNoHttp()
|
||||||
{
|
{
|
||||||
StubHttpMessageHandler handler = CreateHandler(HttpStatusCode.OK);
|
StubHttpMessageHandler handler = CreateHandler(HttpStatusCode.OK);
|
||||||
AiConnectionChecker checker = CreateChecker(handler);
|
IAiConnectionChecker checker = CreateChecker(handler);
|
||||||
|
|
||||||
AiCheckResultDto result = await checker.CheckAsync(
|
AiCheckResultDto result = await checker.CheckAsync(
|
||||||
CloudDeepSeekRequest(baseUrl: "ftp://api.deepseek.com", key: "sk-1234567890ab"),
|
CloudDeepSeekRequest(baseUrl: "ftp://api.deepseek.com", key: "sk-1234567890ab"),
|
||||||
@@ -94,7 +95,7 @@ public sealed class AiConnectionCheckerTests
|
|||||||
public async Task CheckAsync_SuccessfulHttp_ReturnsOkConnectedWithStatusAndMaskedKey()
|
public async Task CheckAsync_SuccessfulHttp_ReturnsOkConnectedWithStatusAndMaskedKey()
|
||||||
{
|
{
|
||||||
StubHttpMessageHandler handler = CreateHandler(HttpStatusCode.OK);
|
StubHttpMessageHandler handler = CreateHandler(HttpStatusCode.OK);
|
||||||
AiConnectionChecker checker = CreateChecker(handler);
|
IAiConnectionChecker checker = CreateChecker(handler);
|
||||||
AiCheckRequest request = CloudDeepSeekRequest(
|
AiCheckRequest request = CloudDeepSeekRequest(
|
||||||
baseUrl: "https://api.deepseek.com/",
|
baseUrl: "https://api.deepseek.com/",
|
||||||
key: "sk-1234567890ab");
|
key: "sk-1234567890ab");
|
||||||
@@ -121,7 +122,7 @@ public sealed class AiConnectionCheckerTests
|
|||||||
public async Task CheckAsync_KeyRejectedStatuses_ReturnsKeyRejectedMessage(int statusCode)
|
public async Task CheckAsync_KeyRejectedStatuses_ReturnsKeyRejectedMessage(int statusCode)
|
||||||
{
|
{
|
||||||
StubHttpMessageHandler handler = CreateHandler((HttpStatusCode)statusCode);
|
StubHttpMessageHandler handler = CreateHandler((HttpStatusCode)statusCode);
|
||||||
AiConnectionChecker checker = CreateChecker(handler);
|
IAiConnectionChecker checker = CreateChecker(handler);
|
||||||
|
|
||||||
AiCheckResultDto result = await checker.CheckAsync(
|
AiCheckResultDto result = await checker.CheckAsync(
|
||||||
CloudDeepSeekRequest(key: "sk-1234567890ab"),
|
CloudDeepSeekRequest(key: "sk-1234567890ab"),
|
||||||
@@ -136,7 +137,7 @@ public sealed class AiConnectionCheckerTests
|
|||||||
public async Task CheckAsync_OtherHttpError_ReturnsHttpCheckMessage()
|
public async Task CheckAsync_OtherHttpError_ReturnsHttpCheckMessage()
|
||||||
{
|
{
|
||||||
StubHttpMessageHandler handler = CreateHandler(HttpStatusCode.InternalServerError);
|
StubHttpMessageHandler handler = CreateHandler(HttpStatusCode.InternalServerError);
|
||||||
AiConnectionChecker checker = CreateChecker(handler);
|
IAiConnectionChecker checker = CreateChecker(handler);
|
||||||
|
|
||||||
AiCheckResultDto result = await checker.CheckAsync(
|
AiCheckResultDto result = await checker.CheckAsync(
|
||||||
CloudDeepSeekRequest(key: "sk-1234567890ab"),
|
CloudDeepSeekRequest(key: "sk-1234567890ab"),
|
||||||
@@ -152,7 +153,7 @@ public sealed class AiConnectionCheckerTests
|
|||||||
{
|
{
|
||||||
StubHttpMessageHandler handler = new((_, _) =>
|
StubHttpMessageHandler handler = new((_, _) =>
|
||||||
Task.FromException<HttpResponseMessage>(new HttpRequestException("Connection refused")));
|
Task.FromException<HttpResponseMessage>(new HttpRequestException("Connection refused")));
|
||||||
AiConnectionChecker checker = CreateChecker(handler);
|
IAiConnectionChecker checker = CreateChecker(handler);
|
||||||
|
|
||||||
AiCheckResultDto result = await checker.CheckAsync(
|
AiCheckResultDto result = await checker.CheckAsync(
|
||||||
CloudDeepSeekRequest(key: "sk-1234567890ab"),
|
CloudDeepSeekRequest(key: "sk-1234567890ab"),
|
||||||
@@ -169,7 +170,7 @@ public sealed class AiConnectionCheckerTests
|
|||||||
public async Task CheckAsync_Anthropic_UsesV1ModelsPathAndApiKeyHeaders()
|
public async Task CheckAsync_Anthropic_UsesV1ModelsPathAndApiKeyHeaders()
|
||||||
{
|
{
|
||||||
StubHttpMessageHandler handler = CreateHandler(HttpStatusCode.OK);
|
StubHttpMessageHandler handler = CreateHandler(HttpStatusCode.OK);
|
||||||
AiConnectionChecker checker = CreateChecker(handler);
|
IAiConnectionChecker checker = CreateChecker(handler);
|
||||||
var request = new AiCheckRequest(
|
var request = new AiCheckRequest(
|
||||||
ProviderId: "anthropic",
|
ProviderId: "anthropic",
|
||||||
BaseUrl: "https://api.anthropic.com",
|
BaseUrl: "https://api.anthropic.com",
|
||||||
@@ -197,7 +198,7 @@ public sealed class AiConnectionCheckerTests
|
|||||||
public async Task CheckAsync_PrivateBaseUrl_ReturnsSsrfBlockedAndNoHttp()
|
public async Task CheckAsync_PrivateBaseUrl_ReturnsSsrfBlockedAndNoHttp()
|
||||||
{
|
{
|
||||||
StubHttpMessageHandler handler = CreateHandler(HttpStatusCode.OK);
|
StubHttpMessageHandler handler = CreateHandler(HttpStatusCode.OK);
|
||||||
AiConnectionChecker checker = CreateChecker(handler);
|
IAiConnectionChecker checker = CreateChecker(handler);
|
||||||
|
|
||||||
// SSRF-гейт (Security review): приватные/локальные адреса для проверки не-local провайдера запрещены
|
// SSRF-гейт (Security review): приватные/локальные адреса для проверки не-local провайдера запрещены
|
||||||
// (метаданные облака 169.254.169.254, loopback, приватные подсети) — HTTP не выполняется вовсе.
|
// (метаданные облака 169.254.169.254, loopback, приватные подсети) — HTTP не выполняется вовсе.
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ using System.Net;
|
|||||||
using System.Text;
|
using System.Text;
|
||||||
using Deal.Infrastructure.Integrations.Services;
|
using Deal.Infrastructure.Integrations.Services;
|
||||||
using Microsoft.Extensions.Logging.Abstractions;
|
using Microsoft.Extensions.Logging.Abstractions;
|
||||||
|
using Deal.Modules.Settings.Application.Abstractions;
|
||||||
|
|
||||||
namespace Deal.Tests.Unit.Support;
|
namespace Deal.Tests.Unit.Support;
|
||||||
|
|
||||||
@@ -16,7 +17,7 @@ public sealed class CbrRateSourceTests
|
|||||||
public async Task FetchAsync_SamplePayload_ReturnsRatesToRub()
|
public async Task FetchAsync_SamplePayload_ReturnsRatesToRub()
|
||||||
{
|
{
|
||||||
StubHttpMessageHandler handler = CreateJsonHandler(SamplePayload);
|
StubHttpMessageHandler handler = CreateJsonHandler(SamplePayload);
|
||||||
CbrRateSource source = CreateSource(handler);
|
IRatesSource source = CreateSource(handler);
|
||||||
|
|
||||||
Dictionary<string, double>? rates = await source.FetchAsync(CancellationToken.None);
|
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}}}""";
|
string payload = """{"Valute":{"XCD":{"Nominal":3,"Value":10.0},"TRL":{"Nominal":1,"Value":1.23456789}}}""";
|
||||||
StubHttpMessageHandler handler = CreateJsonHandler(payload);
|
StubHttpMessageHandler handler = CreateJsonHandler(payload);
|
||||||
CbrRateSource source = CreateSource(handler);
|
IRatesSource source = CreateSource(handler);
|
||||||
|
|
||||||
Dictionary<string, double>? rates = await source.FetchAsync(CancellationToken.None);
|
Dictionary<string, double>? rates = await source.FetchAsync(CancellationToken.None);
|
||||||
|
|
||||||
@@ -52,7 +53,7 @@ public sealed class CbrRateSourceTests
|
|||||||
[Fact]
|
[Fact]
|
||||||
public async Task FetchAsync_HttpErrorStatus_ReturnsNull()
|
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);
|
Dictionary<string, double>? rates = await source.FetchAsync(CancellationToken.None);
|
||||||
|
|
||||||
@@ -64,7 +65,7 @@ public sealed class CbrRateSourceTests
|
|||||||
{
|
{
|
||||||
StubHttpMessageHandler handler = new((_, _) =>
|
StubHttpMessageHandler handler = new((_, _) =>
|
||||||
Task.FromException<HttpResponseMessage>(new HttpRequestException("Connection refused")));
|
Task.FromException<HttpResponseMessage>(new HttpRequestException("Connection refused")));
|
||||||
CbrRateSource source = CreateSource(handler);
|
IRatesSource source = CreateSource(handler);
|
||||||
|
|
||||||
Dictionary<string, double>? rates = await source.FetchAsync(CancellationToken.None);
|
Dictionary<string, double>? rates = await source.FetchAsync(CancellationToken.None);
|
||||||
|
|
||||||
@@ -75,7 +76,7 @@ public sealed class CbrRateSourceTests
|
|||||||
public async Task FetchAsync_InvalidJsonBody_ReturnsNull()
|
public async Task FetchAsync_InvalidJsonBody_ReturnsNull()
|
||||||
{
|
{
|
||||||
StubHttpMessageHandler handler = CreateJsonHandler("{это не JSON");
|
StubHttpMessageHandler handler = CreateJsonHandler("{это не JSON");
|
||||||
CbrRateSource source = CreateSource(handler);
|
IRatesSource source = CreateSource(handler);
|
||||||
|
|
||||||
Dictionary<string, double>? rates = await source.FetchAsync(CancellationToken.None);
|
Dictionary<string, double>? rates = await source.FetchAsync(CancellationToken.None);
|
||||||
|
|
||||||
@@ -86,7 +87,7 @@ public sealed class CbrRateSourceTests
|
|||||||
public async Task FetchAsync_MissingValuteObject_ReturnsNull()
|
public async Task FetchAsync_MissingValuteObject_ReturnsNull()
|
||||||
{
|
{
|
||||||
StubHttpMessageHandler handler = CreateJsonHandler("""{"Date":"2026-09-05T11:30:00+03:00"}""");
|
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);
|
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}}}""";
|
string payload = """{"Valute":{"USD":{"Nominal":1,"Value":"abc"},"EUR":{"Nominal":1,"Value":99.9}}}""";
|
||||||
StubHttpMessageHandler handler = CreateJsonHandler(payload);
|
StubHttpMessageHandler handler = CreateJsonHandler(payload);
|
||||||
CbrRateSource source = CreateSource(handler);
|
IRatesSource source = CreateSource(handler);
|
||||||
|
|
||||||
Dictionary<string, double>? rates = await source.FetchAsync(CancellationToken.None);
|
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}}}""";
|
string payload = """{"Valute":{"USD":{"Nominal":0,"Value":92.5}}}""";
|
||||||
StubHttpMessageHandler handler = CreateJsonHandler(payload);
|
StubHttpMessageHandler handler = CreateJsonHandler(payload);
|
||||||
CbrRateSource source = CreateSource(handler);
|
IRatesSource source = CreateSource(handler);
|
||||||
|
|
||||||
Dictionary<string, double>? rates = await source.FetchAsync(CancellationToken.None);
|
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.Settings;
|
||||||
using Deal.Tests.Unit.Modules.Tenants;
|
using Deal.Tests.Unit.Modules.Tenants;
|
||||||
using Microsoft.Extensions.Logging.Abstractions;
|
using Microsoft.Extensions.Logging.Abstractions;
|
||||||
|
using Deal.Contracts.Integrations.Abstractions;
|
||||||
|
using Deal.SharedKernel.Tenants.Abstractions;
|
||||||
|
|
||||||
namespace Deal.Tests.Unit.Support;
|
namespace Deal.Tests.Unit.Support;
|
||||||
|
|
||||||
@@ -54,7 +56,7 @@ public sealed class GrpcAiClassifierTests
|
|||||||
settings.Preload(SettingsKeys.DomainKeywords, Json(TestKeywords));
|
settings.Preload(SettingsKeys.DomainKeywords, Json(TestKeywords));
|
||||||
|
|
||||||
FakeTenantLimitStore limits = new();
|
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);
|
AiFilterResultDto result = await classifier.FilterAsync("Купите телеграм-канал", CancellationToken.None);
|
||||||
|
|
||||||
Assert.False(result.Pass);
|
Assert.False(result.Pass);
|
||||||
@@ -85,7 +87,7 @@ public sealed class GrpcAiClassifierTests
|
|||||||
{
|
{
|
||||||
service.FilterUnavailable = true;
|
service.FilterUnavailable = true;
|
||||||
(FakeSettingsStore settings, FakeSecretCipher cipher, FakeKanjStore kanjStore) = Context(port);
|
(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>(
|
await Assert.ThrowsAsync<AiUnavailableException>(
|
||||||
() => classifier.FilterAsync("текст", CancellationToken.None));
|
() => classifier.FilterAsync("текст", CancellationToken.None));
|
||||||
@@ -99,7 +101,7 @@ public sealed class GrpcAiClassifierTests
|
|||||||
{
|
{
|
||||||
string text = new string('а', 5000);
|
string text = new string('а', 5000);
|
||||||
(FakeSettingsStore settings, FakeSecretCipher cipher, FakeKanjStore kanjStore) = Context(port);
|
(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);
|
await classifier.FilterAsync(text, CancellationToken.None);
|
||||||
|
|
||||||
@@ -149,7 +151,7 @@ public sealed class GrpcAiClassifierTests
|
|||||||
});
|
});
|
||||||
|
|
||||||
FakeTenantLimitStore limits = new();
|
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);
|
AiParsedCardDto parsed = await classifier.ClassifyAsync("Нужен Python-разработчик, оплата от 2000$", CancellationToken.None);
|
||||||
|
|
||||||
Assert.Equal("Middle Python в команду", parsed.Title);
|
Assert.Equal("Middle Python в команду", parsed.Title);
|
||||||
@@ -190,7 +192,7 @@ public sealed class GrpcAiClassifierTests
|
|||||||
(FakeSettingsStore settings, FakeSecretCipher cipher, FakeKanjStore kanjStore) = Context(port);
|
(FakeSettingsStore settings, FakeSecretCipher cipher, FakeKanjStore kanjStore) = Context(port);
|
||||||
|
|
||||||
FakeTenantLimitStore limits = new();
|
FakeTenantLimitStore limits = new();
|
||||||
GrpcAiClassifier classifier = CreateClassifier(port, settings, cipher, kanjStore, limits);
|
IAiClassifier classifier = CreateClassifier(port, settings, cipher, kanjStore, limits);
|
||||||
|
|
||||||
// ok=false (модель без JSON) → AiUnavailableException: воркер падает в локальный разбор (aiFail).
|
// ok=false (модель без JSON) → AiUnavailableException: воркер падает в локальный разбор (aiFail).
|
||||||
await Assert.ThrowsAsync<AiUnavailableException>(
|
await Assert.ThrowsAsync<AiUnavailableException>(
|
||||||
@@ -209,7 +211,7 @@ public sealed class GrpcAiClassifierTests
|
|||||||
{
|
{
|
||||||
service.ClassifyUnavailable = true;
|
service.ClassifyUnavailable = true;
|
||||||
(FakeSettingsStore settings, FakeSecretCipher cipher, FakeKanjStore kanjStore) = Context(port);
|
(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>(
|
await Assert.ThrowsAsync<AiUnavailableException>(
|
||||||
() => classifier.ClassifyAsync("текст", CancellationToken.None));
|
() => classifier.ClassifyAsync("текст", CancellationToken.None));
|
||||||
@@ -236,7 +238,7 @@ public sealed class GrpcAiClassifierTests
|
|||||||
},
|
},
|
||||||
}.ToJsonString());
|
}.ToJsonString());
|
||||||
|
|
||||||
GrpcAiClassifier classifier = CreateClassifier(port, settings, cipher, kanjStore);
|
IAiClassifier classifier = CreateClassifier(port, settings, cipher, kanjStore);
|
||||||
await classifier.ClassifyAsync("текст", CancellationToken.None);
|
await classifier.ClassifyAsync("текст", CancellationToken.None);
|
||||||
|
|
||||||
// ProviderConfig из настроек: id/base/model — переопределение, apiKey расшифрован, api_style — из каталога.
|
// 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}""" };
|
service.ClassifyReply = new ClassifyReply { Ok = true, Json = """{"title":"Т","stack":[],"is_spam":false}""" };
|
||||||
string text = new string('б', 6000);
|
string text = new string('б', 6000);
|
||||||
(FakeSettingsStore settings, FakeSecretCipher cipher, FakeKanjStore kanjStore) = Context(port);
|
(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);
|
await classifier.ClassifyAsync(text, CancellationToken.None);
|
||||||
|
|
||||||
@@ -285,7 +287,7 @@ public sealed class GrpcAiClassifierTests
|
|||||||
FakeKanjStore kanjStore,
|
FakeKanjStore kanjStore,
|
||||||
FakeTenantLimitStore? limits = null)
|
FakeTenantLimitStore? limits = null)
|
||||||
{
|
{
|
||||||
var tenantContext = new TenantContext();
|
ITenantContext tenantContext = new TenantContext();
|
||||||
tenantContext.SetTenant(new TenantId(TenantIdValue));
|
tenantContext.SetTenant(new TenantId(TenantIdValue));
|
||||||
var connection = new AiGrpcConnection(new AiServiceOptions { UseLocal = false, Endpoint = $"http://127.0.0.1:{port}" });
|
var connection = new AiGrpcConnection(new AiServiceOptions { UseLocal = false, Endpoint = $"http://127.0.0.1:{port}" });
|
||||||
return new GrpcAiClassifier(
|
return new GrpcAiClassifier(
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
using System.Text;
|
using System.Text;
|
||||||
using Deal.Contracts.Integrations.Models;
|
using Deal.Contracts.Integrations.Models;
|
||||||
using Deal.Infrastructure.Integrations.Storage.Services;
|
using Deal.Infrastructure.Integrations.Storage.Services;
|
||||||
|
using Deal.Contracts.Integrations.Abstractions;
|
||||||
|
|
||||||
namespace Deal.Tests.Unit.Support;
|
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 static readonly byte[] SampleContent = Encoding.UTF8.GetBytes("файл-вложение-deal-123");
|
||||||
|
|
||||||
private readonly string _tempRoot;
|
private readonly string _tempRoot;
|
||||||
private readonly LocalFileStorage _storage;
|
private readonly IFileStorage _storage;
|
||||||
|
|
||||||
public LocalFileStorageTests()
|
public LocalFileStorageTests()
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ using Deal.Modules.Tenants.Application.Services;
|
|||||||
using Deal.SharedKernel.Tenants.Models;
|
using Deal.SharedKernel.Tenants.Models;
|
||||||
using Deal.Tests.Unit.Modules.Settings;
|
using Deal.Tests.Unit.Modules.Settings;
|
||||||
using Deal.Tests.Unit.Modules.Tenants;
|
using Deal.Tests.Unit.Modules.Tenants;
|
||||||
|
using Deal.SharedKernel.Tenants.Abstractions;
|
||||||
|
|
||||||
namespace Deal.Tests.Unit.Support;
|
namespace Deal.Tests.Unit.Support;
|
||||||
|
|
||||||
@@ -96,7 +97,7 @@ public sealed class TokenUsageRecorderTests
|
|||||||
{
|
{
|
||||||
var settings = new FakeSettingsStore();
|
var settings = new FakeSettingsStore();
|
||||||
var limits = new FakeTenantLimitStore();
|
var limits = new FakeTenantLimitStore();
|
||||||
var tenantContext = new TenantContext(); // без SetTenant — списание вне tenant-контекста невозможно.
|
ITenantContext tenantContext = new TenantContext(); // без SetTenant — списание вне tenant-контекста невозможно.
|
||||||
var recorder = new TokenUsageRecorder(
|
var recorder = new TokenUsageRecorder(
|
||||||
settings, limits, tenantContext, new TokenUsageEventService(new FakeTokenUsageEventStore()));
|
settings, limits, tenantContext, new TokenUsageEventService(new FakeTokenUsageEventStore()));
|
||||||
|
|
||||||
@@ -144,7 +145,7 @@ public sealed class TokenUsageRecorderTests
|
|||||||
var settings = new FakeSettingsStore();
|
var settings = new FakeSettingsStore();
|
||||||
var limits = new FakeTenantLimitStore();
|
var limits = new FakeTenantLimitStore();
|
||||||
var events = new FakeTokenUsageEventStore();
|
var events = new FakeTokenUsageEventStore();
|
||||||
var tenantContext = new TenantContext();
|
ITenantContext tenantContext = new TenantContext();
|
||||||
tenantContext.SetTenant(new TenantId(TenantIdValue));
|
tenantContext.SetTenant(new TenantId(TenantIdValue));
|
||||||
var recorder = new TokenUsageRecorder(settings, limits, tenantContext, new TokenUsageEventService(events));
|
var recorder = new TokenUsageRecorder(settings, limits, tenantContext, new TokenUsageEventService(events));
|
||||||
return (settings, limits, events, recorder);
|
return (settings, limits, events, recorder);
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ public sealed class CoreIngressClientTests
|
|||||||
(string endpoint, RecordingIngressService server, WebApplication app) = await FakeIngressServer.StartAsync();
|
(string endpoint, RecordingIngressService server, WebApplication app) = await FakeIngressServer.StartAsync();
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
CoreIngressClient client = CreateClient(endpoint);
|
ICoreIngressClient client = CreateClient(endpoint);
|
||||||
|
|
||||||
PushSourceReply reply = await client.PushSourceAsync(
|
PushSourceReply reply = await client.PushSourceAsync(
|
||||||
TenantId,
|
TenantId,
|
||||||
@@ -65,7 +65,7 @@ public sealed class CoreIngressClientTests
|
|||||||
try
|
try
|
||||||
{
|
{
|
||||||
server.MonitoredIds.Add("-1001234567890");
|
server.MonitoredIds.Add("-1001234567890");
|
||||||
CoreIngressClient client = CreateClient(endpoint);
|
ICoreIngressClient client = CreateClient(endpoint);
|
||||||
var entries = new List<DialogEntry>
|
var entries = new List<DialogEntry>
|
||||||
{
|
{
|
||||||
new() { Id = "-1001234567890", Name = "IT Канал", Kind = "channel" },
|
new() { Id = "-1001234567890", Name = "IT Канал", Kind = "channel" },
|
||||||
@@ -91,7 +91,7 @@ public sealed class CoreIngressClientTests
|
|||||||
public async Task PushSource_UnreachableCore_ThrowsSessionException()
|
public async Task PushSource_UnreachableCore_ThrowsSessionException()
|
||||||
{
|
{
|
||||||
// Порт без сервера: соединение отклоняется — вызов падает до deadline (RpcTimeout 15 с).
|
// Порт без сервера: соединение отклоняется — вызов падает до 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>(
|
SessionException exception = await Assert.ThrowsAsync<SessionException>(
|
||||||
() => client.PushSourceAsync(
|
() => client.PushSourceAsync(
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ using Deal.Grpc.Telegram;
|
|||||||
using Deal.Telegram.Sessions;
|
using Deal.Telegram.Sessions;
|
||||||
using Grpc.Core;
|
using Grpc.Core;
|
||||||
using Grpc.Net.Client;
|
using Grpc.Net.Client;
|
||||||
|
using Deal.Telegram.Core;
|
||||||
|
|
||||||
namespace Deal.Telegram.Core;
|
namespace Deal.Telegram.Core;
|
||||||
|
|
||||||
@@ -44,7 +45,7 @@ public sealed class CoreIngressClient : ICoreIngressClient
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public async Task<PushSourceReply> PushSourceAsync(
|
async Task<PushSourceReply> ICoreIngressClient.PushSourceAsync(
|
||||||
string tenantId,
|
string tenantId,
|
||||||
PushSourceRequest request,
|
PushSourceRequest request,
|
||||||
CancellationToken cancellationToken)
|
CancellationToken cancellationToken)
|
||||||
@@ -61,7 +62,7 @@ public sealed class CoreIngressClient : ICoreIngressClient
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public async Task<IReadOnlyList<string>> SyncDialogsAsync(
|
async Task<IReadOnlyList<string>> ICoreIngressClient.SyncDialogsAsync(
|
||||||
string tenantId,
|
string tenantId,
|
||||||
IReadOnlyList<DialogEntry> entries,
|
IReadOnlyList<DialogEntry> entries,
|
||||||
CancellationToken cancellationToken)
|
CancellationToken cancellationToken)
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
using Deal.Telegram.Dialogs;
|
||||||
namespace Deal.Telegram.Dialogs;
|
namespace Deal.Telegram.Dialogs;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -6,7 +7,7 @@ namespace Deal.Telegram.Dialogs;
|
|||||||
public sealed class RandomBackfillPacer : IBackfillPacer
|
public sealed class RandomBackfillPacer : IBackfillPacer
|
||||||
{
|
{
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public async Task WaitAsync(
|
async Task IBackfillPacer.WaitAsync(
|
||||||
double minSeconds,
|
double minSeconds,
|
||||||
double maxSeconds,
|
double maxSeconds,
|
||||||
CancellationToken cancellationToken)
|
CancellationToken cancellationToken)
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
using Deal.Telegram.Telegram;
|
||||||
namespace Deal.Telegram.Telegram;
|
namespace Deal.Telegram.Telegram;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -6,7 +7,7 @@ namespace Deal.Telegram.Telegram;
|
|||||||
public sealed class ClientFactory : ITelegramClientFactory
|
public sealed class ClientFactory : ITelegramClientFactory
|
||||||
{
|
{
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public ISessionClient Create(
|
ISessionClient ITelegramClientFactory.Create(
|
||||||
int apiId,
|
int apiId,
|
||||||
string apiHash,
|
string apiHash,
|
||||||
byte[]? storedSession)
|
byte[]? storedSession)
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ using Grpc.Core;
|
|||||||
using TL;
|
using TL;
|
||||||
using WTelegram;
|
using WTelegram;
|
||||||
using RpcException = TL.RpcException;
|
using RpcException = TL.RpcException;
|
||||||
|
using Deal.Telegram.Telegram;
|
||||||
|
|
||||||
namespace Deal.Telegram.Telegram;
|
namespace Deal.Telegram.Telegram;
|
||||||
|
|
||||||
@@ -82,7 +83,7 @@ public sealed class WTelegramSessionClient : ISessionClient
|
|||||||
public byte[]? SessionBytes => Volatile.Read(ref _latestSessionBytes);
|
public byte[]? SessionBytes => Volatile.Read(ref _latestSessionBytes);
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public async Task ConnectAsync(CancellationToken cancellationToken)
|
async Task ISessionClient.ConnectAsync(CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
if (IsConnected)
|
if (IsConnected)
|
||||||
{
|
{
|
||||||
@@ -94,7 +95,7 @@ public sealed class WTelegramSessionClient : ISessionClient
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public async Task RequestCodeAsync(string phone, CancellationToken cancellationToken)
|
async Task ISessionClient.RequestCodeAsync(string phone, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
_phone = phone;
|
_phone = phone;
|
||||||
_phoneCodeHash = null;
|
_phoneCodeHash = null;
|
||||||
@@ -118,7 +119,7 @@ public sealed class WTelegramSessionClient : ISessionClient
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public async Task<string?> SubmitCodeAsync(string code, CancellationToken cancellationToken)
|
async Task<string?> ISessionClient.SubmitCodeAsync(string code, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
if (_phoneAlreadyAuthorized)
|
if (_phoneAlreadyAuthorized)
|
||||||
{
|
{
|
||||||
@@ -158,7 +159,7 @@ public sealed class WTelegramSessionClient : ISessionClient
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public async Task SubmitPasswordAsync(string password, CancellationToken cancellationToken)
|
async Task ISessionClient.SubmitPasswordAsync(string password, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
@@ -178,7 +179,7 @@ public sealed class WTelegramSessionClient : ISessionClient
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public async Task StartQrAsync(Action<string> onQrUrl, CancellationToken cancellationToken)
|
async Task ISessionClient.StartQrAsync(Action<string> onQrUrl, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
@@ -196,13 +197,13 @@ public sealed class WTelegramSessionClient : ISessionClient
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public async Task LogOutAsync(CancellationToken cancellationToken)
|
async Task ISessionClient.LogOutAsync(CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
await _client.Auth_LogOut().WaitAsync(cancellationToken).ConfigureAwait(false);
|
await _client.Auth_LogOut().WaitAsync(cancellationToken).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <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);
|
UserBase[] users = await _client.Users_GetUsers(InputUser.Self).WaitAsync(cancellationToken).ConfigureAwait(false);
|
||||||
string username = users.OfType<User>().FirstOrDefault()?.username ?? string.Empty;
|
string username = users.OfType<User>().FirstOrDefault()?.username ?? string.Empty;
|
||||||
@@ -221,7 +222,7 @@ public sealed class WTelegramSessionClient : ISessionClient
|
|||||||
public event Func<TelegramMessage, Task>? MessageReceived;
|
public event Func<TelegramMessage, Task>? MessageReceived;
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <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);
|
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);
|
(DialogBase[] dialogs, Dictionary<long, ChatBase> chats, Dictionary<long, User> users) = UnpackDialogs(result);
|
||||||
@@ -241,7 +242,7 @@ public sealed class WTelegramSessionClient : ISessionClient
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public async Task<IReadOnlyList<TelegramMessage>> GetMessagesAsync(
|
async Task<IReadOnlyList<TelegramMessage>> ISessionClient.GetMessagesAsync(
|
||||||
string dialogId,
|
string dialogId,
|
||||||
int limit,
|
int limit,
|
||||||
CancellationToken cancellationToken)
|
CancellationToken cancellationToken)
|
||||||
@@ -265,7 +266,7 @@ public sealed class WTelegramSessionClient : ISessionClient
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public async Task<TelegramMessage?> GetMessageAsync(
|
async Task<TelegramMessage?> ISessionClient.GetMessageAsync(
|
||||||
string dialogId,
|
string dialogId,
|
||||||
long msgId,
|
long msgId,
|
||||||
CancellationToken cancellationToken)
|
CancellationToken cancellationToken)
|
||||||
@@ -295,7 +296,7 @@ public sealed class WTelegramSessionClient : ISessionClient
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <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);
|
InputPeer peer = await ResolvePeerAsync(dialogId, cancellationToken).ConfigureAwait(false);
|
||||||
await RunTlCallAsync(() => _client.ReadHistory(peer), cancellationToken).ConfigureAwait(false);
|
await RunTlCallAsync(() => _client.ReadHistory(peer), cancellationToken).ConfigureAwait(false);
|
||||||
@@ -303,7 +304,7 @@ public sealed class WTelegramSessionClient : ISessionClient
|
|||||||
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public async Task<IReadOnlyList<TelegramDialog>> SearchAsync(
|
async Task<IReadOnlyList<TelegramDialog>> ISessionClient.SearchAsync(
|
||||||
string query,
|
string query,
|
||||||
int limit,
|
int limit,
|
||||||
CancellationToken cancellationToken)
|
CancellationToken cancellationToken)
|
||||||
@@ -327,7 +328,7 @@ public sealed class WTelegramSessionClient : ISessionClient
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public async Task<TelegramSourceInfo> GetInfoAsync(string dialogId, CancellationToken cancellationToken)
|
async Task<TelegramSourceInfo> ISessionClient.GetInfoAsync(string dialogId, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
TelegramSourceInfo unknown = DefaultSourceInfo(dialogId);
|
TelegramSourceInfo unknown = DefaultSourceInfo(dialogId);
|
||||||
if (!TryParseSignedId(dialogId, out bool isChannel, out bool isChat, out bool isUser, out long rawId))
|
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 />
|
/// <inheritdoc />
|
||||||
public async Task<DiscoveryReadResult> ReadForEvalAsync(
|
async Task<DiscoveryReadResult> ISessionClient.ReadForEvalAsync(
|
||||||
string dialogId,
|
string dialogId,
|
||||||
int limit,
|
int limit,
|
||||||
CancellationToken cancellationToken)
|
CancellationToken cancellationToken)
|
||||||
@@ -410,7 +411,7 @@ public sealed class WTelegramSessionClient : ISessionClient
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <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);
|
Contacts_ResolvedPeer resolved = await RunTlCallAsync(() => _client.Contacts_ResolveUsername(username), cancellationToken).ConfigureAwait(false);
|
||||||
CacheEntities(resolved.chats.Values, resolved.users.Values);
|
CacheEntities(resolved.chats.Values, resolved.users.Values);
|
||||||
@@ -425,7 +426,7 @@ public sealed class WTelegramSessionClient : ISessionClient
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <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)
|
if (!TryParseSignedId(dialogId, out bool isChannel, out _, out _, out long rawId) || !isChannel)
|
||||||
{
|
{
|
||||||
@@ -923,7 +924,7 @@ public sealed class WTelegramSessionClient : ISessionClient
|
|||||||
|
|
||||||
// Канал/пользователь без access_hash (рестарт/новый источник): доливаем сущности
|
// Канал/пользователь без access_hash (рестарт/новый источник): доливаем сущности
|
||||||
// первой страницей списка диалогов — каталог ядра строится из неё же (лимит 500, как refresh).
|
// первой страницей списка диалогов — каталог ядра строится из неё же (лимит 500, как refresh).
|
||||||
await GetDialogsAsync(DialogResolvePageSize, cancellationToken).ConfigureAwait(false);
|
await ((ISessionClient)this).GetDialogsAsync(DialogResolvePageSize, cancellationToken).ConfigureAwait(false);
|
||||||
return BuildPeer(isChannel, isChat, isUser, rawId)
|
return BuildPeer(isChannel, isChat, isUser, rawId)
|
||||||
?? throw new SessionException(StatusCode.InvalidArgument, SessionErrorMessages.UnknownDialog);
|
?? 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)
|
lock (_entityCacheGate)
|
||||||
{
|
{
|
||||||
return _entityAccessHashes.TryGetValue(dialogId, out long accessHash)
|
return _entityAccessHashes.TryGetValue(dialogId, out long accessHash)
|
||||||
|
|||||||
Reference in New Issue
Block a user