feat: align legacy operator and playout behavior

This commit is contained in:
2026-07-15 21:04:05 +09:00
parent 06a16e5741
commit 83398044c6
77 changed files with 6377 additions and 391 deletions

View File

@@ -237,6 +237,38 @@ public sealed class CandleSceneBuilderTests
value == item.ExpectedPrice);
}
[Theory]
[InlineData(CandleMarketTarget.KospiIndex)]
[InlineData(CandleMarketTarget.KosdaqIndex)]
[InlineData(CandleMarketTarget.Kospi200Index)]
[InlineData(CandleMarketTarget.Krx100Index)]
[InlineData(CandleMarketTarget.FuturesIndex)]
[InlineData(CandleMarketTarget.KospiIndustry)]
[InlineData(CandleMarketTarget.KosdaqIndustry)]
[InlineData(CandleMarketTarget.OverseasIndex)]
public void IndexAndIndustryTargetsForceMovingAveragePathsAndGuideOff(
CandleMarketTarget target)
{
var data = new S8010SceneData(
CandleCutAlias.Cut8061,
target,
CandleChartMode.Price,
"Target",
new CandleQuoteData(100, 0, 0, ScenePriceDirection.Flat),
[Row(1, 100, 110, 90, 100, 10, 101, 102)],
ShowMovingAverage5: true,
ShowMovingAverage20: true);
var mutations = new S8010SceneMutationBuilder().Build(data);
Assert.Empty(mutations.OfType<PlayoutSetPathPoints>()
.Last(path => path.ObjectName == "ma5").Points);
Assert.Empty(mutations.OfType<PlayoutSetPathPoints>()
.Last(path => path.ObjectName == "ma20").Points);
Assert.False(mutations.OfType<PlayoutSetVisible>()
.Last(visible => visible.ObjectName == "guide").IsVisible);
}
[Fact]
public void Equal_range_zero_volume_and_single_partial_path_are_finite()
{

View File

@@ -46,6 +46,159 @@ public sealed class LegacyPlayoutWorkflowTests
], provider.Calls);
}
[Fact]
public async Task S8010SamplesCurrentMovingAveragesForPrepareTakeInRefreshAndNext()
{
var engine = new RecordingEngine();
var provider = new RecordingProvider(new Dictionary<string, (int, ScenePageSize?)>
{
["8051"] = (1, null),
["8040"] = (1, null)
});
var source = new MutableLegacySceneMovingAverageSelectionSource(
new LegacySceneMovingAverageSelection(false, false));
var workflow = new LegacyPlayoutWorkflow(engine, provider);
LegacyPlaylistEntry[] playlist =
[
CandleEntry("first", "8051", source),
CandleEntry("second", "8040", source)
];
Assert.True((await workflow.PrepareAsync(playlist, 0)).IsSuccess);
source.Update(new LegacySceneMovingAverageSelection(true, false));
Assert.True((await workflow.TakeInAsync()).IsSuccess);
source.Update(new LegacySceneMovingAverageSelection(false, true));
Assert.True((await workflow.RefreshOnAirAsync()).IsSuccess);
source.Update(new LegacySceneMovingAverageSelection(true, true));
Assert.True((await workflow.NextAsync()).IsSuccess);
Assert.Equal(["", "MA5", "MA20", "MA5,MA20"],
provider.SelectionGraphicTypes);
Assert.Equal("second", workflow.State.CurrentEntryId);
}
[Fact]
public async Task OperatorPageCountRemainsFrozenAcrossGrowthAndRefresh()
{
var definitions = new Dictionary<string, (int Count, ScenePageSize? Size)>
{
["5074"] = (6, ScenePageSize.Five),
["5001"] = (1, null)
};
var engine = new RecordingEngine();
var provider = new RecordingProvider(definitions);
var workflow = new LegacyPlayoutWorkflow(engine, provider);
LegacyPlaylistEntry[] playlist =
[
new(
"paged",
"5074",
PageNavigation: new(ScenePageSize.Five, FrozenPageCount: 2)),
new("ordinary", "5001", PageNavigation: LegacyPlaylistPageNavigation.Disabled)
];
Assert.True((await workflow.TakeSelectedAsync(playlist, 0)).IsSuccess);
Assert.Equal(2, workflow.State.PageCount);
Assert.True(workflow.State.IsPageNavigationEnabled);
definitions["5074"] = (16, ScenePageSize.Five);
Assert.True((await workflow.RefreshOnAirAsync()).IsSuccess);
Assert.Equal(2, workflow.State.PageCount);
Assert.Equal(LegacyWorkflowNextKind.PageNext, workflow.State.NextKind);
Assert.True((await workflow.NextAsync()).IsSuccess);
Assert.Equal(1, workflow.State.PageIndexZeroBased);
Assert.Equal(2, workflow.State.PageCount);
Assert.True(workflow.State.IsLastPage);
Assert.Equal(LegacyWorkflowNextKind.PlaylistNext, workflow.State.NextKind);
definitions["5074"] = (101, ScenePageSize.Five);
Assert.True((await workflow.RefreshOnAirAsync()).IsSuccess);
Assert.Equal(2, workflow.State.PageCount);
Assert.Equal(5, workflow.State.CurrentPageItemCount);
Assert.Equal(LegacyWorkflowNextKind.PlaylistNext, workflow.State.NextKind);
}
[Fact]
public async Task OperatorPageCountRemainsFrozenAfterShrinkAndMissingNextPageFailsClosed()
{
var definitions = new Dictionary<string, (int Count, ScenePageSize? Size)>
{
["5074"] = (6, ScenePageSize.Five)
};
var engine = new RecordingEngine();
var provider = new RecordingProvider(definitions);
var workflow = new LegacyPlayoutWorkflow(engine, provider);
LegacyPlaylistEntry[] playlist =
[
new(
"paged",
"5074",
PageNavigation: new(ScenePageSize.Five, FrozenPageCount: 2))
];
Assert.True((await workflow.TakeSelectedAsync(playlist, 0)).IsSuccess);
definitions["5074"] = (5, ScenePageSize.Five);
Assert.True((await workflow.RefreshOnAirAsync()).IsSuccess);
Assert.Equal(2, workflow.State.PageCount);
Assert.Equal(LegacyWorkflowNextKind.PageNext, workflow.State.NextKind);
var engineCallCount = engine.Calls.Count;
await Assert.ThrowsAsync<LegacySceneDataException>(() => workflow.NextAsync());
Assert.Equal(engineCallCount, engine.Calls.Count);
Assert.Equal(0, workflow.State.PageIndexZeroBased);
Assert.Equal(2, workflow.State.PageCount);
Assert.Equal(LegacyWorkflowNextKind.PageNext, workflow.State.NextKind);
}
[Fact]
public async Task ExplicitlyNonPagedOperatorEntryMovesToNextPlaylistRowDespitePagedProvider()
{
var engine = new RecordingEngine();
var provider = new RecordingProvider(new Dictionary<string, (int, ScenePageSize?)>
{
["5074"] = (11, ScenePageSize.Five),
["5001"] = (1, null)
});
var workflow = new LegacyPlayoutWorkflow(engine, provider);
LegacyPlaylistEntry[] playlist =
[
new("noncanonical", "5074", PageNavigation: LegacyPlaylistPageNavigation.Disabled),
new("ordinary", "5001", PageNavigation: LegacyPlaylistPageNavigation.Disabled)
];
Assert.True((await workflow.TakeSelectedAsync(playlist, 0)).IsSuccess);
Assert.False(workflow.State.IsPageNavigationEnabled);
Assert.Equal(1, workflow.State.PageCount);
Assert.Equal(LegacyWorkflowNextKind.PlaylistNext, workflow.State.NextKind);
Assert.True((await workflow.NextAsync()).IsSuccess);
Assert.Equal("ordinary", workflow.State.CurrentEntryId);
Assert.DoesNotContain("5074:1", provider.Calls);
Assert.Equal("Next:5001:p0", engine.Calls[^1]);
}
[Fact]
public async Task DirectCoreEntryWithoutNavigationMetadataRetainsFreshPlanBehavior()
{
var definitions = new Dictionary<string, (int Count, ScenePageSize? Size)>
{
["5074"] = (6, ScenePageSize.Five)
};
var workflow = new LegacyPlayoutWorkflow(
new RecordingEngine(),
new RecordingProvider(definitions));
Assert.True((await workflow.TakeSelectedAsync([new("direct", "5074")], 0)).IsSuccess);
Assert.Equal(2, workflow.State.PageCount);
definitions["5074"] = (11, ScenePageSize.Five);
Assert.True((await workflow.RefreshOnAirAsync()).IsSuccess);
Assert.Equal(3, workflow.State.PageCount);
Assert.True(workflow.State.IsPageNavigationEnabled);
}
[Fact]
public async Task Next_AtTheEndDoesNotWrapOrDispatchTheEngine()
{
@@ -181,7 +334,7 @@ public sealed class LegacyPlayoutWorkflowTests
var workflow = new LegacyPlayoutWorkflow(engine, provider);
await Assert.ThrowsAsync<ArgumentException>(() => workflow.PrepareAsync(
[new LegacyPlaylistEntry("entry", "5001", FadeDuration: 61)],
[new LegacyPlaylistEntry("entry", "5001", FadeDuration: 20)],
0));
Assert.Empty(provider.Calls);
@@ -243,7 +396,7 @@ public sealed class LegacyPlayoutWorkflowTests
}
[Fact]
public async Task PrepareAndTakeIn_LoadsOnceAndCallsPrepareThenTakeIn()
public async Task TakeSelected_LoadsOnceAndCallsPrepareThenTakeIn()
{
var engine = new RecordingEngine();
var provider = new RecordingProvider(new Dictionary<string, (int, ScenePageSize?)>
@@ -252,7 +405,7 @@ public sealed class LegacyPlayoutWorkflowTests
});
var workflow = new LegacyPlayoutWorkflow(engine, provider);
var result = await workflow.PrepareAndTakeInAsync(
var result = await workflow.TakeSelectedAsync(
[new("disabled", "5006", false), new("selected", "5001")],
0);
@@ -267,7 +420,7 @@ public sealed class LegacyPlayoutWorkflowTests
}
[Fact]
public async Task PrepareAndTakeIn_WhenTakeInFails_RetainsPreparedStateWithoutRetry()
public async Task TakeSelected_WhenTakeInFails_RetainsPreparedStateWithoutRetry()
{
var engine = new RecordingEngine();
engine.TakeInResults.Enqueue(PlayoutResultCode.OutcomeUnknown);
@@ -277,7 +430,7 @@ public sealed class LegacyPlayoutWorkflowTests
});
var workflow = new LegacyPlayoutWorkflow(engine, provider);
var result = await workflow.PrepareAndTakeInAsync([new("selected", "5074")], 0);
var result = await workflow.TakeSelectedAsync([new("selected", "5074")], 0);
Assert.Equal(PlayoutResultCode.OutcomeUnknown, result.Code);
Assert.Equal(["5074:0"], provider.Calls);
@@ -288,6 +441,198 @@ public sealed class LegacyPlayoutWorkflowTests
Assert.Equal(LegacyWorkflowNextKind.PageNext, workflow.State.NextKind);
}
[Fact]
public async Task TakeSelected_WhileProgram_PreparesAndPlaysTheCurrentOperatorSelection()
{
var engine = new RecordingEngine();
var provider = new RecordingProvider(new Dictionary<string, (int, ScenePageSize?)>
{
["5001"] = (1, null),
["5006"] = (1, null),
["5074"] = (1, null)
});
var workflow = new LegacyPlayoutWorkflow(engine, provider);
LegacyPlaylistEntry[] playlist =
[
new("program-a", "5001"),
new("between", "5006"),
new("candidate-d", "5074")
];
Assert.True((await workflow.TakeSelectedAsync(playlist, 0)).IsSuccess);
var switchResult = await workflow.TakeSelectedAsync(playlist, 2);
Assert.True(switchResult.IsSuccess);
Assert.Equal(
["Prepare:5001:p0", "TakeIn", "Prepare:5074:p0", "TakeIn"],
engine.Calls);
Assert.Equal(["5001:0", "5074:0"], provider.Calls);
Assert.Equal(2, workflow.State.CurrentCueIndexZeroBased);
Assert.Equal("candidate-d", workflow.State.CurrentEntryId);
Assert.Null(workflow.State.PreparedCutCode);
Assert.Equal("5074", workflow.State.OnAirCutCode);
}
[Fact]
public async Task ManualCandidateRemainsIndependentAcrossNextUntilTheNextF8()
{
var engine = new RecordingEngine();
var provider = new RecordingProvider(new Dictionary<string, (int, ScenePageSize?)>
{
["5001"] = (1, null),
["5006"] = (1, null),
["5074"] = (1, null)
});
var workflow = new LegacyPlayoutWorkflow(engine, provider);
LegacyPlaylistEntry[] playlist =
[
new("program-a", "5001"),
new("next-b", "5006"),
new("candidate-d", "5074")
];
Assert.True((await workflow.TakeSelectedAsync(playlist, 0)).IsSuccess);
const int operatorCandidateIndex = 2;
Assert.True((await workflow.NextAsync()).IsSuccess);
Assert.Equal(1, workflow.State.CurrentCueIndexZeroBased);
Assert.Equal("next-b", workflow.State.CurrentEntryId);
Assert.Equal(2, operatorCandidateIndex);
Assert.True((await workflow.TakeSelectedAsync(
playlist,
operatorCandidateIndex)).IsSuccess);
Assert.Equal(2, workflow.State.CurrentCueIndexZeroBased);
Assert.Equal("candidate-d", workflow.State.CurrentEntryId);
Assert.Equal(
[
"Prepare:5001:p0", "TakeIn",
"Next:5006:p0",
"Prepare:5074:p0", "TakeIn"
],
engine.Calls);
}
[Fact]
public async Task TakeSelected_AfterExplicitPrepare_UsesTheLaterRowSelection()
{
var engine = new RecordingEngine();
var provider = new RecordingProvider(new Dictionary<string, (int, ScenePageSize?)>
{
["5001"] = (1, null),
["5074"] = (1, null)
});
var workflow = new LegacyPlayoutWorkflow(engine, provider);
LegacyPlaylistEntry[] playlist =
[
new("prepared-a", "5001"),
new("selected-d", "5074")
];
Assert.True((await workflow.PrepareAsync(playlist, 0)).IsSuccess);
var result = await workflow.TakeSelectedAsync(playlist, 1);
Assert.True(result.IsSuccess);
Assert.Equal(
["Prepare:5001:p0", "Prepare:5074:p0", "TakeIn"],
engine.Calls);
Assert.Equal(["5001:0", "5074:0"], provider.Calls);
Assert.Equal(1, workflow.State.CurrentCueIndexZeroBased);
Assert.Equal("selected-d", workflow.State.CurrentEntryId);
Assert.Null(workflow.State.PreparedCutCode);
Assert.Equal("5074", workflow.State.OnAirCutCode);
}
[Fact]
public async Task TakeSelected_WhileProgram_WhenPrepareFails_PreservesCurrentOnAirRow()
{
var engine = new RecordingEngine();
var provider = new RecordingProvider(new Dictionary<string, (int, ScenePageSize?)>
{
["5001"] = (1, null),
["5074"] = (1, null)
});
var workflow = new LegacyPlayoutWorkflow(engine, provider);
LegacyPlaylistEntry[] playlist =
[
new("program-a", "5001"),
new("candidate-d", "5074")
];
Assert.True((await workflow.TakeSelectedAsync(playlist, 0)).IsSuccess);
engine.PrepareResults.Enqueue(PlayoutResultCode.Rejected);
var result = await workflow.TakeSelectedAsync(playlist, 1);
Assert.Equal(PlayoutResultCode.Rejected, result.Code);
Assert.Equal(
["Prepare:5001:p0", "TakeIn", "Prepare:5074:p0"],
engine.Calls);
Assert.Equal(0, workflow.State.CurrentCueIndexZeroBased);
Assert.Equal("program-a", workflow.State.CurrentEntryId);
Assert.Null(workflow.State.PreparedCutCode);
Assert.Equal("5001", workflow.State.OnAirCutCode);
}
[Fact]
public async Task TakeSelected_WhileProgram_WhenPlayIsUnknown_KeepsOldOnAirAndNewPreparedState()
{
var engine = new RecordingEngine();
var provider = new RecordingProvider(new Dictionary<string, (int, ScenePageSize?)>
{
["5001"] = (1, null),
["5074"] = (1, null)
});
var workflow = new LegacyPlayoutWorkflow(engine, provider);
LegacyPlaylistEntry[] playlist =
[
new("program-a", "5001"),
new("candidate-d", "5074")
];
Assert.True((await workflow.TakeSelectedAsync(playlist, 0)).IsSuccess);
engine.TakeInResults.Enqueue(PlayoutResultCode.OutcomeUnknown);
var result = await workflow.TakeSelectedAsync(playlist, 1);
Assert.Equal(PlayoutResultCode.OutcomeUnknown, result.Code);
Assert.Equal(
["Prepare:5001:p0", "TakeIn", "Prepare:5074:p0", "TakeIn"],
engine.Calls);
Assert.Equal(0, workflow.State.CurrentCueIndexZeroBased);
Assert.Equal("program-a", workflow.State.CurrentEntryId);
Assert.Equal("5074", workflow.State.PreparedCutCode);
Assert.Equal("5001", workflow.State.OnAirCutCode);
}
[Fact]
public async Task TakeSelected_WhilePlayCompletionIsPending_DoesNotQueryOrDispatch()
{
var engine = new RecordingEngine();
var provider = new RecordingProvider(new Dictionary<string, (int, ScenePageSize?)>
{
["5001"] = (1, null),
["5074"] = (1, null)
});
var workflow = new LegacyPlayoutWorkflow(engine, provider);
LegacyPlaylistEntry[] playlist =
[
new("program-a", "5001"),
new("candidate-d", "5074")
];
Assert.True((await workflow.TakeSelectedAsync(playlist, 0)).IsSuccess);
engine.Status = engine.Status with { IsPlayCompletionPending = true };
var providerCallCount = provider.Calls.Count;
var engineCallCount = engine.Calls.Count;
var result = await workflow.TakeSelectedAsync(playlist, 1);
Assert.Equal(PlayoutResultCode.Rejected, result.Code);
Assert.Equal(providerCallCount, provider.Calls.Count);
Assert.Equal(engineCallCount, engine.Calls.Count);
Assert.Equal(0, workflow.State.CurrentCueIndexZeroBased);
Assert.Equal("5001", workflow.State.OnAirCutCode);
}
[Fact]
public async Task TakeIn_WhenFreshPrepareIsOutcomeUnknown_DoesNotPlayOrReplacePreparedState()
{
@@ -312,7 +657,7 @@ public sealed class LegacyPlayoutWorkflowTests
}
[Fact]
public async Task RefreshOnAir_RequeriesTheCurrentPagedWindowAndUsesGetPlayingSceneEnginePath()
public async Task RefreshOnAir_RequeriesTheCurrentPagedWindowAndUsesOnAirUpdatePath()
{
var engine = new RecordingEngine();
var provider = new RecordingProvider(new Dictionary<string, (int, ScenePageSize?)>
@@ -530,6 +875,8 @@ public sealed class LegacyPlayoutWorkflowTests
public List<string> PlanCalls { get; } = [];
public List<string?> SelectionGraphicTypes { get; } = [];
public Func<string, int, string>? SceneNameOverride { get; init; }
public string? BuilderKey { get; init; }
@@ -543,6 +890,7 @@ public sealed class LegacyPlayoutWorkflowTests
{
cancellationToken.ThrowIfCancellationRequested();
Calls.Add($"{entry.CutCode}:{pageIndexZeroBased}");
SelectionGraphicTypes.Add(entry.Selection?.GraphicType);
var definition = definitions[entry.CutCode];
var name = SceneNameOverride?.Invoke(entry.CutCode, pageIndexZeroBased) ?? entry.CutCode;
return Task.FromResult(new LegacySceneCuePage(
@@ -577,6 +925,21 @@ public sealed class LegacyPlayoutWorkflowTests
}
}
private static LegacyPlaylistEntry CandleEntry(
string entryId,
string cutCode,
ILegacySceneMovingAverageSelectionSource source) =>
new(
entryId,
cutCode,
Selection: new LegacySceneSelection(
"KOSPI_STOCK",
"Samsung Electronics",
"persisted-candle-identity",
"PRICE",
"005930"),
MovingAverageSelectionSource: source);
private sealed class RecordingEngine : IPlayoutEngine
{
public List<string> Calls { get; } = [];
@@ -589,7 +952,7 @@ public sealed class LegacyPlayoutWorkflowTests
public Action? PrepareCallback { get; set; }
public PlayoutStatus Status { get; } = new(
public PlayoutStatus Status { get; set; } = new(
PlayoutMode.DryRun,
PlayoutConnectionState.DryRunReady,
true,

View File

@@ -23,7 +23,7 @@ public sealed class LegacyScenePreviewTests
new PlayoutSetPathPoints("line", [new(1, 2), new(3, 4, 5)]),
new PlayoutSetPathShapePoints("shape", [new(6, 7)]),
new PlayoutSetBackgroundTexture(secretAsset),
new PlayoutSetBackgroundVideo(secretAsset, 2004, true),
new PlayoutSetBackgroundVideo(secretAsset, 2004, 10),
new PlayoutUseBackground(true)
]);
@@ -42,7 +42,7 @@ public sealed class LegacyScenePreviewTests
value => Assert.Equal(new("path-shape", "shape", "count=1;6,7,0"), value),
value => Assert.Equal(new("background-texture", "scene", "configured"), value),
value => Assert.Equal(
new("background-video", "scene", "configured;loop=2004;infinite=true"),
new("background-video", "scene", "configured;loop=2004;loop-mode=10"),
value),
value => Assert.Equal(new("background", "scene", "enabled"), value));
Assert.DoesNotContain(secretAsset, string.Join("|", preview.Select(value => value.Value)));

View File

@@ -9,9 +9,8 @@ public sealed class LegacySceneRefreshPolicyTests
{ "5001", 2 }, { "N5001", 2 }, { "5006", 5 }, { "5011", 5 },
{ "5016", 5 }, { "50160", 30 }, { "5023", 30 }, { "5024", 60 },
{ "5025", 30 }, { "5026", 8 }, { "5029", 7 }, { "5032", 5 },
{ "5037", 5 }, { "5074", 7 }, { "5076", 3_000_000 }, { "5077", 7 },
{ "5078", 30 }, { "5079", 3_000_000 }, { "5080", 3_000_000 },
{ "5081", 3_000_000 }, { "5082", 6 }, { "5083", 60 }, { "5084", 5 },
{ "5037", 5 }, { "5074", 7 }, { "5077", 7 }, { "5078", 30 },
{ "5082", 6 }, { "5083", 60 }, { "5084", 5 },
{ "5085", 30 }, { "5086", 7 }, { "50860", 7 }, { "5087", 7 },
{ "5088", 7 }, { "6001", 30 }, { "6067", 30 }, { "8001", 120 },
{ "8002", 120 }, { "8003", 8 }, { "8018", 5 }, { "8032", 5 },
@@ -28,6 +27,18 @@ public sealed class LegacySceneRefreshPolicyTests
Assert.Equal(TimeSpan.FromSeconds(seconds), interval);
}
[Theory]
[InlineData("5076")]
[InlineData("5079")]
[InlineData("5080")]
[InlineData("5081")]
public void OverflowedMainFormTimerAliasesDoNotArmAutomaticRefresh(string alias)
{
Assert.True(LegacySceneRefreshPolicy.ContainsAlias(alias));
Assert.True(LegacySceneRefreshPolicy.IsDisabledByLegacyTimerOverflow(alias));
Assert.False(LegacySceneRefreshPolicy.TryGetInterval(alias, out _));
}
[Fact]
public void PolicyCoversExactlyTheCatalogsActiveAliases()
{
@@ -39,7 +50,7 @@ public sealed class LegacySceneRefreshPolicyTests
Assert.Equal(45, aliases.Length);
Assert.Equal(aliases.Length, LegacySceneRefreshPolicy.Count);
Assert.All(aliases, alias =>
Assert.True(LegacySceneRefreshPolicy.TryGetInterval(alias, out _), alias));
Assert.True(LegacySceneRefreshPolicy.ContainsAlias(alias), alias));
Assert.False(LegacySceneRefreshPolicy.TryGetInterval("8086", out _));
Assert.False(LegacySceneRefreshPolicy.TryGetInterval("../5001", out _));
Assert.Equal(TimeSpan.FromSeconds(3), LegacySceneRefreshPolicy.RecurringInterval);

View File

@@ -147,7 +147,7 @@ public sealed class PlayoutBridgeProtocolTests
[InlineData("{\"requestId\":\"REQ\",\"command\":\"next\",\"playlist\":[],\"selectedIndex\":0}")]
[InlineData("{\"requestId\":\"REQ\",\"command\":\"prepare\",\"playlist\":[],\"selectedIndex\":0}")]
[InlineData("{\"requestId\":\"REQ\",\"command\":\"prepare\",\"playlist\":[{\"id\":\"same\",\"code\":\"5001\",\"enabled\":true},{\"id\":\"same\",\"code\":\"5006\",\"enabled\":true}],\"selectedIndex\":0}")]
[InlineData("{\"requestId\":\"REQ\",\"command\":\"prepare\",\"playlist\":[{\"id\":\"one\",\"code\":\"5001\",\"enabled\":true,\"fadeDuration\":61}],\"selectedIndex\":0}")]
[InlineData("{\"requestId\":\"REQ\",\"command\":\"prepare\",\"playlist\":[{\"id\":\"one\",\"code\":\"5001\",\"enabled\":true,\"fadeDuration\":20}],\"selectedIndex\":0}")]
[InlineData("{\"requestId\":\"REQ\",\"command\":\"prepare\",\"playlist\":[{\"id\":\"one\",\"code\":\"5001\",\"enabled\":true,\"selection\":{\"groupCode\":\"코스피\",\"subject\":\"bad\\nvalue\",\"graphicType\":\"\",\"subtype\":\"\",\"dataCode\":\"005930\"}}],\"selectedIndex\":0}")]
[InlineData("{\"requestId\":\"REQ\",\"command\":\"prepare\",\"playlist\":[{\"id\":\"one\",\"code\":\"5001\",\"enabled\":true,\"selection\":{\"groupCode\":\"코스피\",\"subject\":\"삼성전자\",\"graphicType\":\"\",\"subtype\":\"\",\"dataCode\":\"005930\",\"objectName\":\"title\"}}],\"selectedIndex\":0}")]
public void ParseWorkflowCommand_RejectsCueOverrideDuplicateOrUnsafeShapes(string json)

View File

@@ -546,7 +546,7 @@ public sealed class ReadOnlyPanelSceneBuildersTests
PlayoutSetFaceColor color =>
$"C:{color.ObjectName}:{color.Red}:{color.Green}:{color.Blue}:{color.Alpha}",
PlayoutSetBackgroundVideo video =>
$"B:{video.AssetPath}:{video.LoopCount}:{(video.LoopInfinite ? 1 : 0)}",
$"B:{video.AssetPath}:{video.LoopCount}:{video.LoopInfinite}",
_ => mutation.GetType().Name
};
}

View File

@@ -54,6 +54,30 @@ public sealed class RegisteredLegacySceneCueProviderTests
page.Cue.Mutations!.Skip(2));
}
[Fact]
public async Task CreatePage_CommonVideoBackgroundUsesOriginalRawK3dLoopValueTen()
{
var provider = new RegisteredLegacySceneCueProvider(
new StubDataSource(new LegacySceneDataPage(
"s5085",
new S5085SceneData(
[
new ProgramTradingMetricData(ProgramTradingMetric.NetBuy, 0d)
]),
1)),
options: new LegacySceneCueCompositionOptions(
6,
LegacySceneBackgroundKind.Video,
"Video\\studio.vrv"));
var page = await provider.CreatePageAsync(new("entry", "5085"), 0);
Assert.Equal(
new PlayoutSetBackgroundVideo("Video\\studio.vrv", 2004, 10),
page.Cue.Mutations![0]);
Assert.Equal(new PlayoutUseBackground(true), page.Cue.Mutations[1]);
}
[Fact]
public async Task CreatePagePlan_UsesDedicatedDtoRouteWithoutBuildingACue()
{