Перевести «не найдено» ML/Pipeline на NotFoundException

MlReviewService.ApplyAsync (исходное сообщение) и PipelineProcessingService.ReturnAsync (запись отсева) бросают NotFoundException; эндпоинты без 404-проверок.
This commit is contained in:
2026-09-13 18:44:33 +03:00
parent f93fb0fdd3
commit 891a894aed
6 changed files with 29 additions and 37 deletions
+1 -8
View File
@@ -37,8 +37,6 @@ public static class MlEndpoints
private const string EnterTextDetail = "Введите текст";
private const string MessageNotFoundDetail = "Исходное сообщение не найдено";
/// <summary>
/// Регистрирует группу /api/ml
/// </summary>
@@ -141,12 +139,7 @@ public static class MlEndpoints
}
MlReviewService review = context.RequestServices.GetRequiredService<MlReviewService>();
MlApplyResult? result = await review.ApplyAsync(body.DialogId, body.MsgId, body.Action, ct);
if (result is null)
{
return EndpointResults.NotFound(MessageNotFoundDetail);
}
MlApplyResult result = await review.ApplyAsync(body.DialogId, body.MsgId, body.Action, ct);
if (result.Error is not null)
{
return EndpointResults.BadRequest(result.Error);
@@ -33,8 +33,6 @@ public static class PipelineEndpoints
// Путь возврата записи отсева в обработку (POST).
private const string RejectedReturnPath = "/rejected/{rejId}/return";
private const string RejectedNotFoundDetail = "Запись не найдена";
private const int DefaultPageSize = 100;
/// <summary>
@@ -140,11 +138,7 @@ public static class PipelineEndpoints
}
PipelineProcessingService processing = context.RequestServices.GetRequiredService<PipelineProcessingService>();
RejectReturnResultDto? result = await processing.ReturnAsync(rejId, body.Reason ?? string.Empty, ct);
if (result is null)
{
return EndpointResults.NotFound(RejectedNotFoundDetail);
}
RejectReturnResultDto result = await processing.ReturnAsync(rejId, body.Reason ?? string.Empty, ct);
return result.Error is not null
? EndpointResults.BadRequest(result.Error)
@@ -8,6 +8,7 @@ using Deal.Modules.Kanban.Application.Models;
using Deal.Modules.Kanban.Application.Services;
using Deal.Modules.Pipeline.Application.Abstractions;
using Deal.Modules.Pipeline.Application.Models;
using Deal.SharedKernel.Errors;
namespace Deal.Modules.Pipeline.Application.Services;
@@ -34,6 +35,9 @@ public sealed class MlReviewService(
// Размер одного чтения из очереди/отсева при объединении кандидатов.
private const int MaxScan = 500;
// Имя сущности для текста ошибки «не найдено».
private const string MessageEntityName = "Исходное сообщение";
private const int TextPreviewLength = 600;
/// <summary>
@@ -157,8 +161,9 @@ public sealed class MlReviewService(
/// <param name="dialogId">Оригинал источника (OriginRef) записи.</param>
/// <param name="msgId">Внешний id записи в источнике.</param>
/// <param name="action">Действие: <c>skip</c> | <c>spam</c> | <c>board:&lt;id&gt;</c>.</param>
/// <returns>Результат решения; null — исходная запись не найдена (404-семантика эндпоинта).</returns>
public async Task<MlApplyResult?> ApplyAsync(
/// <returns>Результат решения.</returns>
/// <exception cref="NotFoundException">Исходная запись не найдена.</exception>
public async Task<MlApplyResult> ApplyAsync(
string dialogId,
long msgId,
string? action,
@@ -168,17 +173,14 @@ public sealed class MlReviewService(
string dialog = (dialogId ?? string.Empty).Trim();
string externalId = msgId.ToString(CultureInfo.InvariantCulture);
SourceRef? source = await ResolveSourceAsync(dialog, externalId, ct);
if (source is null)
{
return null; // 404: исходная запись не найдена
}
SourceRef source = await ResolveSourceAsync(dialog, externalId, ct)
?? throw new NotFoundException(MessageEntityName, externalId);
CardDto? card = await cardStore.GetCardBySourceAsync(source, ct);
string? text = await FindTextAsync(dialog, externalId, card, ct);
if (string.IsNullOrWhiteSpace(text))
{
return null; // 404: исходная запись не найдена
throw new NotFoundException(MessageEntityName, externalId);
}
if (normalized == ActionSkip)
@@ -4,6 +4,7 @@ using Deal.Modules.Cards.Application.Sources;
using Deal.Modules.Pipeline.Application.Abstractions;
using Deal.Modules.Pipeline.Application.Models;
using Deal.Modules.Pipeline.Application.Parse;
using Deal.SharedKernel.Errors;
namespace Deal.Modules.Pipeline.Application.Services;
@@ -48,6 +49,9 @@ public sealed class PipelineProcessingService(
private const string DuplicateSource = "dup";
// Имя сущности для текста ошибки «не найдено».
private const string RejectedEntityName = "Запись отсева";
private static readonly HashSet<string> SpamStages = new(StringComparer.Ordinal)
{
"spam_ml",
@@ -154,17 +158,15 @@ public sealed class PipelineProcessingService(
/// </summary>
/// <param name="rejectedId">Id записи отсева (<c>r_...</c>).</param>
/// <param name="reason">Причина возврата (trim, ≤500; пишется на запись для аудита).</param>
/// <returns>null — записи нет (404); иначе результат: Error (400) либо {id, returned:true, returnedAt}.</returns>
public async Task<RejectReturnResultDto?> ReturnAsync(
/// <returns>Результат: Error (400) либо {id, returned:true, returnedAt}.</returns>
/// <exception cref="NotFoundException">Запись отсева не найдена.</exception>
public async Task<RejectReturnResultDto> ReturnAsync(
string rejectedId,
string reason,
CancellationToken ct)
{
RejectedItemDto? row = await store.GetAsync(rejectedId, ct);
if (row is null)
{
return null;
}
RejectedItemDto row = await store.GetAsync(rejectedId, ct)
?? throw new NotFoundException(RejectedEntityName, rejectedId);
if (row.Returned)
{
@@ -4,6 +4,7 @@ using Deal.Modules.Kanban.Application.Models;
using Deal.Modules.Kanban.Application.Services;
using Deal.Modules.Pipeline.Application.Models;
using Deal.Modules.Pipeline.Application.Services;
using Deal.SharedKernel.Errors;
using Deal.Tests.Unit.Contracts;
using Deal.Tests.Unit.Modules.Settings;
using Deal.Tests.Unit.Support;
@@ -241,12 +242,11 @@ public sealed class MlReviewServiceTests
}
[Fact]
public async Task Apply_MessageNotFound_ReturnsNull()
public async Task Apply_MessageNotFound_ThrowsNotFound()
{
MlReviewService service = Create(new TestPipelineStore(), new TestKanjStore(), new TestMlClient(), out _);
MlApplyResult? result = await service.ApplyAsync(Dialog, 999, MlReviewService.ActionSpam, CancellationToken.None);
Assert.Null(result);
await Assert.ThrowsAsync<NotFoundException>(
() => service.ApplyAsync(Dialog, 999, MlReviewService.ActionSpam, CancellationToken.None));
}
}
@@ -2,6 +2,7 @@ using System.Globalization;
using Deal.Modules.Cards.Application.Sources;
using Deal.Modules.Pipeline.Application.Models;
using Deal.Modules.Pipeline.Application.Services;
using Deal.SharedKernel.Errors;
using Deal.Tests.Unit.Contracts;
using Deal.Tests.Unit.Support;
@@ -165,13 +166,13 @@ public sealed class PipelineProcessingServiceTests
[Fact]
public async Task ReturnAsync_RecordNotFound_ReturnsNull()
public async Task ReturnAsync_RecordNotFound_ThrowsNotFound()
{
(PipelineProcessingService service, TestPipelineStore store, TestMlClient ml) = Create();
RejectReturnResultDto? result = await service.ReturnAsync("r_missing", string.Empty, CancellationToken.None);
await Assert.ThrowsAsync<NotFoundException>(
() => service.ReturnAsync("r_missing", string.Empty, CancellationToken.None));
Assert.Null(result); // эндпоинт ответит 404 «Запись не найдена» (Ruling 10)
Assert.Empty(store.Queue);
Assert.Empty(ml.Pushed);
}