Implement template export/import endpoints and enhance grid generation logic
Added new endpoints for exporting and importing grid configurations in TemplateEndpoints, allowing for better management of template data. Enhanced the GenerateGridCommandHandler to support seasonal layers in grid generation, improving scheduling accuracy during holiday periods. Updated related classes and records to accommodate these changes, ensuring a cohesive integration of new features. Improved documentation for clarity and maintainability.
This commit is contained in:
@@ -107,7 +107,9 @@ public class GenerateGridTests
|
||||
return (channel.Id, template);
|
||||
}
|
||||
|
||||
private static GridPlanner Planner(Common.Interfaces.IAppDbContext db) =>
|
||||
private static GridPlanner Planner(Common.Interfaces.IAppDbContext db) => new(db, Catalog(db));
|
||||
|
||||
private static GroupCatalog Catalog(Common.Interfaces.IAppDbContext db) =>
|
||||
new(db, GroupServices.Dynamic(db), new GroupElementResolver(db));
|
||||
|
||||
private static async Task<GridPlan> PlanAsync(
|
||||
@@ -149,10 +151,15 @@ public class GenerateGridTests
|
||||
Assert.Equal(plan.FreeMinutes, plan.CoveredMinutes);
|
||||
// Полосы без ротации свободны во все семь дней — они идут «каждый день», а не семью копиями.
|
||||
Assert.All(
|
||||
plan.Slots.Where(s => s.Start < new TimeOnly(20, 0)),
|
||||
plan.Slots.Where(s => s.Start < new TimeOnly(14, 0)),
|
||||
slot => Assert.Null(slot.Weekday)
|
||||
);
|
||||
// Прайм (с 20:00) чередуется по дням недели, поэтому у него дни проставлены явно.
|
||||
// Дневной блок и прайм чередуются по дням недели: иначе неделя выглядит одним повторяющимся
|
||||
// днём. У них дни проставлены явно.
|
||||
Assert.All(
|
||||
plan.Slots.Where(s => s.Start >= new TimeOnly(14, 0) && s.Start < new TimeOnly(18, 0)),
|
||||
slot => Assert.NotNull(slot.Weekday)
|
||||
);
|
||||
Assert.All(
|
||||
plan.Slots.Where(s => s.Start == new TimeOnly(20, 0)),
|
||||
slot => Assert.NotNull(slot.Weekday)
|
||||
@@ -163,6 +170,35 @@ public class GenerateGridTests
|
||||
);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task NeighbouringSlots_DoNotShareTheSameGroup()
|
||||
{
|
||||
// Две группы и целые сутки полос: раскладка обязана их чередовать, а не ставить одну
|
||||
// и ту же встык — два блока подряд из одной группы читаются как один длинный.
|
||||
var fixture = new TestDb();
|
||||
var first = Playable("Первый сериал", ShowKind.Series, 60, 25);
|
||||
var second = Playable("Второй сериал", ShowKind.Series, 60, 25);
|
||||
var (channelId, _) = await SeedChannelAsync(
|
||||
fixture,
|
||||
[first.Show, second.Show],
|
||||
[.. first.Assets, .. second.Assets],
|
||||
[GroupOf("Первые", first.Show.Id), GroupOf("Вторые", second.Show.Id)]
|
||||
);
|
||||
|
||||
var plan = await PlanAsync(fixture, channelId);
|
||||
|
||||
var byDay = plan
|
||||
.Slots.Where(s => s.GroupId is not null)
|
||||
.GroupBy(s => s.Weekday)
|
||||
.Select(g => g.OrderBy(s => s.Start).ToList());
|
||||
|
||||
foreach (var day in byDay)
|
||||
{
|
||||
for (var i = 1; i < day.Count; i++)
|
||||
Assert.NotEqual(day[i - 1].GroupId, day[i].GroupId);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task PrimeGetsFeature_AndMorningGetsSeries()
|
||||
{
|
||||
@@ -322,10 +358,117 @@ public class GenerateGridTests
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task SlotLength_FollowsContent_NotProfileConstant()
|
||||
public async Task Rebuild_BuildsSeasonLayer_WithAnnualApplicability()
|
||||
{
|
||||
// Серии по 25 минут: двухчасовая полоса должна разложиться на блоки, кратные серии,
|
||||
// а не отдать десятки минут фону.
|
||||
var fixture = new TestDb();
|
||||
var series = Playable("Сериал", ShowKind.Series, 40, 25);
|
||||
var films = Films(8, 100);
|
||||
var (channelId, _) = await SeedChannelAsync(
|
||||
fixture,
|
||||
[series.Show, .. films.Shows],
|
||||
[.. series.Assets, .. films.Assets],
|
||||
[
|
||||
GroupOf("Сериалы", series.Show.Id),
|
||||
GroupOf("Фильмы", [.. films.Shows.Select(s => s.Id)]),
|
||||
]
|
||||
);
|
||||
|
||||
await using (var db = fixture.New())
|
||||
{
|
||||
var result = await new GenerateGridCommandHandler(
|
||||
db,
|
||||
Planner(db),
|
||||
new SlotWriter(db)
|
||||
).Handle(
|
||||
new GenerateGridCommand(
|
||||
channelId,
|
||||
GridProfileKind.Mixed,
|
||||
GridGenerationMode.Rebuild
|
||||
),
|
||||
CancellationToken.None
|
||||
);
|
||||
|
||||
Assert.True(result.IsSuccess);
|
||||
await db.SaveChangesAsync(CancellationToken.None);
|
||||
}
|
||||
|
||||
await using var check = fixture.New();
|
||||
var stored = await check
|
||||
.ScheduleTemplates.Include(t => t.Layers)
|
||||
.ThenInclude(l => l.Slots)
|
||||
.FirstAsync(CancellationToken.None);
|
||||
|
||||
var season = GridProfiles.Get(GridProfileKind.Mixed).Season;
|
||||
Assert.NotNull(season);
|
||||
var layer = Assert.Single(stored.Layers, l => l.Name == season.Name);
|
||||
|
||||
// Праздничный слой обязан перекрывать и будни, и выходные — иначе 1 января пройдёт как
|
||||
// обычное воскресенье.
|
||||
var otherPriority = stored.Layers.Where(l => l.Id != layer.Id).Max(l => l.Priority);
|
||||
Assert.True(layer.Priority > otherPriority);
|
||||
Assert.NotEmpty(layer.Slots);
|
||||
|
||||
// Слой действует по ежегодному периоду: заводить его заново каждый декабрь не нужно.
|
||||
var applicability = LayerApplicability.FromJson(layer.ApplicabilityJson);
|
||||
Assert.NotNull(applicability);
|
||||
Assert.Equal([season.Range], applicability.AnnualRanges);
|
||||
Assert.True(applicability.Covers(new DateOnly(2027, 1, 1)));
|
||||
Assert.False(applicability.Covers(new DateOnly(2027, 3, 1)));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Season_DoesNotSpendTheWeeksBudget()
|
||||
{
|
||||
// Праздничная неделя идёт вместо обычной, а не вдобавок к ней: её слоты не должны попадать
|
||||
// в подсчёт закрытого времени, иначе генератор решит, что неделя перекрыта с запасом.
|
||||
var fixture = new TestDb();
|
||||
var series = Playable("Сериал", ShowKind.Series, 40, 25);
|
||||
var films = Films(8, 100);
|
||||
var (channelId, _) = await SeedChannelAsync(
|
||||
fixture,
|
||||
[series.Show, .. films.Shows],
|
||||
[.. series.Assets, .. films.Assets],
|
||||
[
|
||||
GroupOf("Сериалы", series.Show.Id),
|
||||
GroupOf("Фильмы", [.. films.Shows.Select(s => s.Id)]),
|
||||
]
|
||||
);
|
||||
|
||||
var plan = await PlanAsync(
|
||||
fixture,
|
||||
channelId,
|
||||
GridProfileKind.Mixed,
|
||||
GridGenerationMode.Rebuild
|
||||
);
|
||||
|
||||
Assert.Contains(plan.Slots, s => s.Layer == GridPlanLayer.Season);
|
||||
Assert.All(
|
||||
plan.Slots.Where(s => s.Layer == GridPlanLayer.Season),
|
||||
s => Assert.Null(s.Weekday)
|
||||
);
|
||||
Assert.True(plan.CoveredMinutes <= plan.FreeMinutes);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Sitcoms_AirInPairs()
|
||||
{
|
||||
// Ситком — это пара серий и рекламный шов между парами. Три-четыре подряд читаются как
|
||||
// марафон, а марафон у ситкомового канала бывает по выходным, а не каждый будний день.
|
||||
var sitcom = GridProfiles.Get(GridProfileKind.Sitcom);
|
||||
var blocks = sitcom
|
||||
.Weekdays.Where(b => b.PreferKind == ShowKind.Series && b.UnitsPerBlock > 0)
|
||||
.ToList();
|
||||
|
||||
Assert.NotEmpty(blocks);
|
||||
Assert.All(blocks, b => Assert.Equal(2, b.UnitsPerBlock));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task SlotLength_FollowsContent_RoundedToTheGrid()
|
||||
{
|
||||
// Серии по 25 минут: двухчасовая полоса должна разложиться на блоки под серии, а не
|
||||
// отдать десятки минут фону. Длина при этом округляется вверх до четверти часа —
|
||||
// программа передач состоит из круглых времён, а разницу занимает рекламный шов.
|
||||
var fixture = new TestDb();
|
||||
var series = Playable("Сериал", ShowKind.Series, 60, 25);
|
||||
var (channelId, _) = await SeedChannelAsync(
|
||||
@@ -342,8 +485,12 @@ public class GenerateGridTests
|
||||
morning,
|
||||
slot =>
|
||||
{
|
||||
// Слот вмещает ровно столько серий, сколько в нём заявлено, и с точностью до пяти минут.
|
||||
Assert.Equal(slot.DurationMinutes, slot.BlockValue * 25);
|
||||
var content = slot.BlockValue * 25;
|
||||
// Слот не короче своего контента — иначе блок перелезет через объявленное время.
|
||||
Assert.True(slot.DurationMinutes >= content);
|
||||
// И не длиннее его больше, чем на шаг решётки: остаток — это шов, а не пустота.
|
||||
Assert.True(slot.DurationMinutes - content < 15);
|
||||
Assert.Equal(0, slot.DurationMinutes % 15);
|
||||
Assert.Equal(SlotBlockMode.Count, slot.BlockMode);
|
||||
}
|
||||
);
|
||||
|
||||
@@ -116,7 +116,10 @@ public class GridPlanPreviewTests
|
||||
|
||||
await using var db = fixture.New();
|
||||
var result = await new PreviewGeneratedGridQueryHandler(
|
||||
new GridPlanner(db, GroupServices.Dynamic(db), new GroupElementResolver(db))
|
||||
new GridPlanner(
|
||||
db,
|
||||
new GroupCatalog(db, GroupServices.Dynamic(db), new GroupElementResolver(db))
|
||||
)
|
||||
).Handle(
|
||||
new PreviewGeneratedGridQuery(
|
||||
channelId,
|
||||
@@ -143,7 +146,10 @@ public class GridPlanPreviewTests
|
||||
await using var db = fixture.New();
|
||||
|
||||
var result = await new PreviewGeneratedGridQueryHandler(
|
||||
new GridPlanner(db, GroupServices.Dynamic(db), new GroupElementResolver(db))
|
||||
new GridPlanner(
|
||||
db,
|
||||
new GroupCatalog(db, GroupServices.Dynamic(db), new GroupElementResolver(db))
|
||||
)
|
||||
).Handle(
|
||||
new PreviewGeneratedGridQuery(
|
||||
Guid.NewGuid(),
|
||||
|
||||
@@ -0,0 +1,378 @@
|
||||
using System.Text.Json;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using TeleWave.Application.Common.Interfaces;
|
||||
using TeleWave.Application.Programming.Groups;
|
||||
using TeleWave.Application.Programming.Templates;
|
||||
using TeleWave.Application.Programming.Templates.Generate;
|
||||
using TeleWave.Application.Programming.Templates.Transfer;
|
||||
using TeleWave.Application.Tests.Support;
|
||||
using TeleWave.Domain.Broadcast;
|
||||
using TeleWave.Domain.Library;
|
||||
using TeleWave.Domain.Media;
|
||||
using TeleWave.Domain.Programming;
|
||||
using Xunit;
|
||||
|
||||
namespace TeleWave.Application.Tests.Programming;
|
||||
|
||||
/// <summary>
|
||||
/// Обмен конфигурацией сетки: выгрузка файлом, загрузка чужого файла и сборка запроса к ИИ.
|
||||
/// Главное свойство — ссылки в файле именные: он должен переноситься между каналами и установками,
|
||||
/// где тех же идентификаторов нет.
|
||||
/// </summary>
|
||||
public class GridTransferTests
|
||||
{
|
||||
private static readonly DateTimeOffset T0 = new(2026, 1, 1, 0, 0, 0, TimeSpan.Zero);
|
||||
|
||||
private static (Show Show, MediaAsset Asset) Playable(string name, int minutes)
|
||||
{
|
||||
var show = Show.Create(name, ShowKind.Series);
|
||||
var asset = MediaAsset.Register($"{name}.mkv", ".mkv", MediaSource.Upload);
|
||||
asset.MarkProcessing();
|
||||
asset.MarkReady(
|
||||
new MediaReadyInfo(
|
||||
TimeSpan.FromMinutes(minutes),
|
||||
6,
|
||||
minutes * 10,
|
||||
1920,
|
||||
1080,
|
||||
"h264",
|
||||
"aac",
|
||||
$"assets/{name}"
|
||||
)
|
||||
);
|
||||
show.AddEpisode(asset.Id);
|
||||
return (show, asset);
|
||||
}
|
||||
|
||||
/// <summary>Канал с одной группой, одним стыком и слоем «Основная сетка» из одного слота.</summary>
|
||||
private static async Task<Guid> SeedAsync(TestDb fixture, bool withSlot = true)
|
||||
{
|
||||
var (show, asset) = Playable("Сериал", 25);
|
||||
var group = Group.Create("Сериалы");
|
||||
group.AddElement(GroupElementKind.Show, show.Id);
|
||||
|
||||
var junction = JunctionTemplate.Create("Рекламный шов");
|
||||
|
||||
var channel = Channel.Create("Первый", "first", T0);
|
||||
var template = ScheduleTemplate.Create(channel.Id, "Сетка");
|
||||
channel.SetTemplate(template.Id);
|
||||
template.SetFallbackGroup(group.Id);
|
||||
template.SetDefaultJunction(junction.Id);
|
||||
|
||||
var layer = template.AddLayer("Основная сетка", 10);
|
||||
if (withSlot)
|
||||
{
|
||||
var slot = layer.AddSlot("Вечерний блок", new TimeOnly(20, 0), 120, Daypart.Prime);
|
||||
slot.UpdateTiming(null, new TimeOnly(20, 0), 120, Daypart.Prime, true, 5, 15);
|
||||
slot.UpdateContent(
|
||||
new SlotContent(
|
||||
"Вечерний блок",
|
||||
SlotKind.Content,
|
||||
group.Id,
|
||||
new SlotStrategy(SlotStrategyType.Sequential).ToJson(),
|
||||
null,
|
||||
SlotBlockMode.Count,
|
||||
4,
|
||||
OverflowPolicy.ContinueNext,
|
||||
null,
|
||||
junction.Id
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
await using var seed = fixture.New();
|
||||
seed.MediaAssets.Add(asset);
|
||||
seed.Shows.Add(show);
|
||||
seed.Groups.Add(group);
|
||||
seed.JunctionTemplates.Add(junction);
|
||||
seed.Channels.Add(channel);
|
||||
seed.ScheduleTemplates.Add(template);
|
||||
await seed.SaveChangesAsync(CancellationToken.None);
|
||||
|
||||
return channel.Id;
|
||||
}
|
||||
|
||||
private static async Task<GridConfig> ExportAsync(TestDb fixture, Guid channelId)
|
||||
{
|
||||
await using var db = fixture.New();
|
||||
var result = await new ExportGridQueryHandler(db).Handle(
|
||||
new ExportGridQuery(channelId),
|
||||
CancellationToken.None
|
||||
);
|
||||
Assert.True(result.IsSuccess);
|
||||
return result.Value;
|
||||
}
|
||||
|
||||
private static async Task<GridImportResultDto> ImportAsync(
|
||||
TestDb fixture,
|
||||
Guid channelId,
|
||||
GridConfig config,
|
||||
bool replace = true
|
||||
)
|
||||
{
|
||||
await using var db = fixture.New();
|
||||
var result = await new ImportGridCommandHandler(db, new SlotWriter(db)).Handle(
|
||||
new ImportGridCommand(channelId, config, replace),
|
||||
CancellationToken.None
|
||||
);
|
||||
Assert.True(result.IsSuccess);
|
||||
await db.SaveChangesAsync(CancellationToken.None);
|
||||
return result.Value;
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Export_ReferencesGroupsAndJunctionsByName()
|
||||
{
|
||||
var fixture = new TestDb();
|
||||
var channelId = await SeedAsync(fixture);
|
||||
|
||||
var config = await ExportAsync(fixture, channelId);
|
||||
|
||||
Assert.Equal(GridConfig.CurrentFormat, config.Format);
|
||||
Assert.Equal(GridConfig.CurrentVersion, config.Version);
|
||||
Assert.Equal("Сериалы", config.FallbackGroup);
|
||||
Assert.Equal("Рекламный шов", config.DefaultJunction);
|
||||
Assert.Equal("Первый", config.Channel?.Name);
|
||||
|
||||
var layer = Assert.Single(config.Layers, l => !l.IsBackground);
|
||||
var slot = Assert.Single(layer.Slots);
|
||||
Assert.Equal("Сериалы", slot.Group);
|
||||
Assert.Equal("Рекламный шов", slot.JunctionAfter);
|
||||
Assert.True(slot.IsAnchor);
|
||||
Assert.Equal(15, slot.SnapToMinutes);
|
||||
|
||||
// Файл должен быть читаемым JSON — его правят руками и отдают модели.
|
||||
Assert.Contains("\"format\": \"telewave.grid\"", config.ToJson(), StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Import_RestoresTheSameGrid_AfterExport()
|
||||
{
|
||||
var fixture = new TestDb();
|
||||
var channelId = await SeedAsync(fixture);
|
||||
var config = await ExportAsync(fixture, channelId);
|
||||
|
||||
// Через сериализацию — так файл и приходит обратно, а не объектом в памяти.
|
||||
var roundTripped = JsonSerializer.Deserialize<GridConfig>(
|
||||
config.ToJson(),
|
||||
GridConfig.Options
|
||||
);
|
||||
Assert.NotNull(roundTripped);
|
||||
|
||||
var imported = await ImportAsync(fixture, channelId, roundTripped);
|
||||
|
||||
Assert.Equal(1, imported.Slots);
|
||||
Assert.Equal(0, imported.Skipped);
|
||||
Assert.Equal(1, imported.Removed);
|
||||
Assert.Empty(imported.Warnings);
|
||||
|
||||
var restored = await ExportAsync(fixture, channelId);
|
||||
Assert.Equal(config.Layers.Count, restored.Layers.Count);
|
||||
Assert.Equal(
|
||||
config.Layers.SelectMany(l => l.Slots),
|
||||
restored.Layers.SelectMany(l => l.Slots)
|
||||
);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Import_ReusesLayerByName_AndDoesNotDuplicateIt()
|
||||
{
|
||||
var fixture = new TestDb();
|
||||
var channelId = await SeedAsync(fixture);
|
||||
var config = await ExportAsync(fixture, channelId);
|
||||
|
||||
await ImportAsync(fixture, channelId, config);
|
||||
await ImportAsync(fixture, channelId, config);
|
||||
|
||||
await using var db = fixture.New();
|
||||
var template = await db
|
||||
.ScheduleTemplates.Include(t => t.Layers)
|
||||
.ThenInclude(l => l.Slots)
|
||||
.FirstAsync(CancellationToken.None);
|
||||
|
||||
Assert.Single(template.Layers, l => l.Name == "Основная сетка");
|
||||
Assert.Equal(1, template.Layers.Sum(l => l.Slots.Count));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Import_SkipsUnknownGroup_AndSaysSo()
|
||||
{
|
||||
var fixture = new TestDb();
|
||||
var channelId = await SeedAsync(fixture, withSlot: false);
|
||||
|
||||
var config = new GridConfig([
|
||||
new GridConfigLayer(
|
||||
"Основная сетка",
|
||||
10,
|
||||
[
|
||||
new GridConfigSlot(
|
||||
"Чужой блок",
|
||||
new TimeOnly(20, 0),
|
||||
120,
|
||||
Daypart.Prime,
|
||||
Group: "Сериалы с другого канала"
|
||||
),
|
||||
new GridConfigSlot(
|
||||
"Свой блок",
|
||||
new TimeOnly(22, 0),
|
||||
60,
|
||||
Daypart.Night,
|
||||
Group: "Сериалы"
|
||||
),
|
||||
]
|
||||
),
|
||||
]);
|
||||
|
||||
var imported = await ImportAsync(fixture, channelId, config);
|
||||
|
||||
// Одна незнакомая ссылка не должна ронять весь файл — иначе админ чинит JSON руками.
|
||||
Assert.Equal(1, imported.Slots);
|
||||
Assert.Equal(1, imported.Skipped);
|
||||
Assert.Contains(
|
||||
imported.Warnings,
|
||||
w => w.Contains("Сериалы с другого канала", StringComparison.Ordinal)
|
||||
);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Import_SkipsOverlappingSlot_AndKeepsTheGridValid()
|
||||
{
|
||||
var fixture = new TestDb();
|
||||
var channelId = await SeedAsync(fixture, withSlot: false);
|
||||
|
||||
var config = new GridConfig([
|
||||
new GridConfigLayer(
|
||||
"Основная сетка",
|
||||
10,
|
||||
[
|
||||
new GridConfigSlot(
|
||||
"Первый",
|
||||
new TimeOnly(20, 0),
|
||||
120,
|
||||
Daypart.Prime,
|
||||
Group: "Сериалы"
|
||||
),
|
||||
new GridConfigSlot(
|
||||
"Наложился",
|
||||
new TimeOnly(21, 0),
|
||||
60,
|
||||
Daypart.Prime,
|
||||
Group: "Сериалы"
|
||||
),
|
||||
]
|
||||
),
|
||||
]);
|
||||
|
||||
var imported = await ImportAsync(fixture, channelId, config);
|
||||
|
||||
Assert.Equal(1, imported.Slots);
|
||||
Assert.Equal(1, imported.Skipped);
|
||||
Assert.NotEmpty(imported.Warnings);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Import_MarksTemplateChanged_ButDoesNotTouchAir()
|
||||
{
|
||||
var fixture = new TestDb();
|
||||
var channelId = await SeedAsync(fixture);
|
||||
var config = await ExportAsync(fixture, channelId);
|
||||
|
||||
await using (var applied = fixture.New())
|
||||
{
|
||||
var template = await applied.ScheduleTemplates.FirstAsync(CancellationToken.None);
|
||||
template.MarkApplied();
|
||||
await applied.SaveChangesAsync(CancellationToken.None);
|
||||
}
|
||||
|
||||
await ImportAsync(fixture, channelId, config);
|
||||
|
||||
await using var db = fixture.New();
|
||||
var stored = await db.ScheduleTemplates.FirstAsync(CancellationToken.None);
|
||||
Assert.True(stored.HasPendingChanges);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Prompt_ListsGroupsAndSchema()
|
||||
{
|
||||
var fixture = new TestDb();
|
||||
var channelId = await SeedAsync(fixture);
|
||||
|
||||
await using var db = fixture.New();
|
||||
var result = await new BuildGridPromptQueryHandler(db, Catalog(db)).Handle(
|
||||
new BuildGridPromptQuery(
|
||||
channelId,
|
||||
["2×2", "Paramount Comedy"],
|
||||
"Хочу ситкомы вечером"
|
||||
),
|
||||
CancellationToken.None
|
||||
);
|
||||
|
||||
Assert.True(result.IsSuccess);
|
||||
var prompt = result.Value.Prompt;
|
||||
|
||||
// Модель обязана видеть, чем канал располагает и в каком виде ждут ответ.
|
||||
Assert.Contains("Сериалы", prompt, StringComparison.Ordinal);
|
||||
Assert.Contains("2×2", prompt, StringComparison.Ordinal);
|
||||
Assert.Contains("Хочу ситкомы вечером", prompt, StringComparison.Ordinal);
|
||||
Assert.Contains("Рекламный шов", prompt, StringComparison.Ordinal);
|
||||
Assert.Contains(GridConfig.CurrentFormat, prompt, StringComparison.Ordinal);
|
||||
Assert.Equal(1, result.Value.Groups);
|
||||
Assert.Equal(1, result.Value.Shows);
|
||||
Assert.Equal(prompt.Length, result.Value.Characters);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Prompt_SchemaSurvivesImport()
|
||||
{
|
||||
// Схема в запросе и импорт обязаны быть одним форматом: пример из запроса должен читаться
|
||||
// импортом без правок, иначе ответ модели придётся чинить руками.
|
||||
var fixture = new TestDb();
|
||||
var channelId = await SeedAsync(fixture, withSlot: false);
|
||||
|
||||
var answer = """
|
||||
{
|
||||
"format": "telewave.grid",
|
||||
"version": 1,
|
||||
"fallbackGroup": "Сериалы",
|
||||
"layers": [
|
||||
{
|
||||
"name": "Основная сетка",
|
||||
"priority": 10,
|
||||
"slots": [
|
||||
{
|
||||
"title": "Вечерний блок",
|
||||
"start": "20:00",
|
||||
"durationMinutes": 120,
|
||||
"daypart": "Prime",
|
||||
"kind": "Content",
|
||||
"group": "Сериалы",
|
||||
"blockMode": "Count",
|
||||
"blockValue": 4,
|
||||
"strategy": { "type": "Sequential", "restartOnEnd": true },
|
||||
"isAnchor": true,
|
||||
"snapToMinutes": 15
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
""";
|
||||
|
||||
var config = JsonSerializer.Deserialize<GridConfig>(answer, GridConfig.Options);
|
||||
Assert.NotNull(config);
|
||||
|
||||
var imported = await ImportAsync(fixture, channelId, config);
|
||||
|
||||
Assert.Equal(1, imported.Slots);
|
||||
Assert.Empty(imported.Warnings);
|
||||
|
||||
var stored = await ExportAsync(fixture, channelId);
|
||||
var slot = Assert.Single(stored.Layers.SelectMany(l => l.Slots));
|
||||
Assert.Equal("Вечерний блок", slot.Title);
|
||||
Assert.Equal(SlotBlockMode.Count, slot.BlockMode);
|
||||
Assert.True(slot.IsAnchor);
|
||||
}
|
||||
|
||||
private static GroupCatalog Catalog(IAppDbContext db) =>
|
||||
new(db, GroupServices.Dynamic(db), new GroupElementResolver(db));
|
||||
}
|
||||
Reference in New Issue
Block a user