diff --git a/tests/food-market.UnitTests/ApiReferenceDocsJobTests.cs b/tests/food-market.UnitTests/ApiReferenceDocsJobTests.cs
new file mode 100644
index 0000000..a448f7a
--- /dev/null
+++ b/tests/food-market.UnitTests/ApiReferenceDocsJobTests.cs
@@ -0,0 +1,81 @@
+using System.Reflection;
+using FluentAssertions;
+using foodmarket.Api.Background;
+using Microsoft.AspNetCore.Hosting;
+using Microsoft.Extensions.Logging.Abstractions;
+using Xunit;
+
+namespace foodmarket.UnitTests;
+
+/// Sprint 28: lock-down тест для регекса .
+/// До Sprint 28 регекс return-type'a матчил только 1-level generic, поэтому
+/// контроллер с Task<ActionResult<PagedResult<Dto>>> терял endpoint'ы.
+/// Этот тест ловит регрессию через scan in-memory C# source-кода и проверку
+/// что нужные endpoint'ы найдены.
+public class ApiReferenceDocsJobTests
+{
+ /// Tiny test-only env для GenerateAsync. Возвращает временный
+ /// каталог как ContentRoot — внутри которого мы кладём наши тестовые
+ /// Controllers/.
+ private sealed class TestEnv : IWebHostEnvironment
+ {
+ public string EnvironmentName { get; set; } = "Test";
+ public string ApplicationName { get; set; } = "Test";
+ public string ContentRootPath { get; set; } = string.Empty;
+ public string WebRootPath { get; set; } = string.Empty;
+ public Microsoft.Extensions.FileProviders.IFileProvider WebRootFileProvider { get; set; } = null!;
+ public Microsoft.Extensions.FileProviders.IFileProvider ContentRootFileProvider { get; set; } = null!;
+ }
+
+ private const string DoubleNestedController = @"
+using Microsoft.AspNetCore.Mvc;
+
+namespace foodmarket.Api.Controllers.Catalog;
+
+[Route(""api/test"")]
+public class TestController : ControllerBase
+{
+ [HttpGet]
+ public async Task>> List() => Ok(null);
+
+ [HttpPost(""create""), RequiresPermission(""TestEdit"")]
+ public async Task> Create() => Ok(null);
+
+ [HttpDelete(""{id:guid}"")]
+ public async Task Delete(Guid id) => NoContent();
+}
+";
+
+ [Fact]
+ public async Task Scans_endpoints_with_doubly_nested_generic_return()
+ {
+ var tmp = Directory.CreateTempSubdirectory("api-ref-test-");
+ try
+ {
+ var ctrlDir = Path.Combine(tmp.FullName, "Controllers");
+ Directory.CreateDirectory(ctrlDir);
+ await File.WriteAllTextAsync(Path.Combine(ctrlDir, "TestController.cs"), DoubleNestedController);
+
+ var env = new TestEnv { ContentRootPath = tmp.FullName };
+ var job = new ApiReferenceDocsJob(env, NullLogger.Instance);
+
+ var count = await job.GenerateAsync();
+ count.Should().Be(3, "three endpoints: GET / POST create / DELETE {id}");
+
+ // Проверяем, что output-файл содержит все три route'а.
+ var outFile = Path.Combine(tmp.FullName, "api-reference-generated.md");
+ File.Exists(outFile).Should().BeTrue();
+ var content = await File.ReadAllTextAsync(outFile);
+ content.Should().Contain("/api/test/create");
+ content.Should().Contain("/api/test/{id:guid}");
+ // base-route — fallback на просто "GET /api/test".
+ content.Should().Contain("/api/test");
+ // RequiresPermission('TestEdit') должен попасть в Permission колонку.
+ content.Should().Contain("TestEdit");
+ }
+ finally
+ {
+ Directory.Delete(tmp.FullName, recursive: true);
+ }
+ }
+}