diff --git a/Directory.Packages.props b/Directory.Packages.props
index 7d382d41..aa4cfe84 100644
--- a/Directory.Packages.props
+++ b/Directory.Packages.props
@@ -9,6 +9,7 @@
+
diff --git a/GenWave.sln b/GenWave.sln
index 9b377d0b..d93f92bb 100644
--- a/GenWave.sln
+++ b/GenWave.sln
@@ -57,6 +57,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ExamplePlugin", "examples\g
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GenWave.Ads", "src\GenWave.Ads\GenWave.Ads.csproj", "{60CD65BA-8A92-41D5-BB1A-0D39AA42184B}"
EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GenWave.TestSupport", "tests\GenWave.TestSupport\GenWave.TestSupport.csproj", "{9F21C208-B1B7-4203-8034-79493F06BA1C}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -331,6 +333,18 @@ Global
{60CD65BA-8A92-41D5-BB1A-0D39AA42184B}.Release|x64.Build.0 = Release|Any CPU
{60CD65BA-8A92-41D5-BB1A-0D39AA42184B}.Release|x86.ActiveCfg = Release|Any CPU
{60CD65BA-8A92-41D5-BB1A-0D39AA42184B}.Release|x86.Build.0 = Release|Any CPU
+ {9F21C208-B1B7-4203-8034-79493F06BA1C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {9F21C208-B1B7-4203-8034-79493F06BA1C}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {9F21C208-B1B7-4203-8034-79493F06BA1C}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {9F21C208-B1B7-4203-8034-79493F06BA1C}.Debug|x64.Build.0 = Debug|Any CPU
+ {9F21C208-B1B7-4203-8034-79493F06BA1C}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {9F21C208-B1B7-4203-8034-79493F06BA1C}.Debug|x86.Build.0 = Debug|Any CPU
+ {9F21C208-B1B7-4203-8034-79493F06BA1C}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {9F21C208-B1B7-4203-8034-79493F06BA1C}.Release|Any CPU.Build.0 = Release|Any CPU
+ {9F21C208-B1B7-4203-8034-79493F06BA1C}.Release|x64.ActiveCfg = Release|Any CPU
+ {9F21C208-B1B7-4203-8034-79493F06BA1C}.Release|x64.Build.0 = Release|Any CPU
+ {9F21C208-B1B7-4203-8034-79493F06BA1C}.Release|x86.ActiveCfg = Release|Any CPU
+ {9F21C208-B1B7-4203-8034-79493F06BA1C}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -359,5 +373,6 @@ Global
{59CC0FFE-92F4-E0A7-1AC9-3F1F42E6F62C} = {B36A84DF-456D-A817-6EDD-3EC3E7F6E11F}
{E9101478-ACF8-4253-96B7-2D3070BC3F23} = {59CC0FFE-92F4-E0A7-1AC9-3F1F42E6F62C}
{60CD65BA-8A92-41D5-BB1A-0D39AA42184B} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B}
+ {9F21C208-B1B7-4203-8034-79493F06BA1C} = {0AB3BF05-4346-4AA6-1389-037BE0695223}
EndGlobalSection
EndGlobal
diff --git a/src/GenWave.Orchestration/OrchestrationServiceCollectionExtensions.cs b/src/GenWave.Orchestration/OrchestrationServiceCollectionExtensions.cs
index 1c6b92d1..60326a61 100644
--- a/src/GenWave.Orchestration/OrchestrationServiceCollectionExtensions.cs
+++ b/src/GenWave.Orchestration/OrchestrationServiceCollectionExtensions.cs
@@ -1,5 +1,6 @@
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
+using Microsoft.Extensions.Logging;
using GenWave.Core.Abstractions;
namespace GenWave.Orchestration;
@@ -118,6 +119,44 @@ public static IServiceCollection AddGenWaveOrchestration(this IServiceCollection
// constructor parameter's own remarks).
services.AddSingleton();
- return services.AddSingleton();
+ // The production construction site (SPEC F184.3/F184.5, STORY-451, T514). The only other
+ // `new Orchestrator(` is GenWave.TestSupport's OrchestratorBuilder; Story451_ConstructionPins
+ // pins the pair. A factory, not AddSingleton(): every seam is
+ // read INSIDE the lambda, i.e. at first resolve of INextItemProvider, never at this call.
+ // That is load-bearing — Program.cs registers IPersonaPickProvider, IRequestFulfillmentSource,
+ // IStationEventSink, IContextSettingsProvider and IAdSpotVend AFTER AddGenWaveOrchestration so
+ // they beat the TryAdd defaults above; a resolve-time read sees the last registration, exactly
+ // as constructor injection did. Optional seams with a NoOp type coalesce to it; the other ten
+ // pass GetService's null through to the Orchestrator's own null handling, unchanged.
+ services.AddSingleton(sp => new Orchestrator(
+ sp.GetRequiredService(),
+ sp.GetRequiredService(),
+ sp.GetRequiredService(),
+ sp.GetRequiredService(),
+ sp.GetRequiredService(),
+ sp.GetRequiredService(),
+ sp.GetRequiredService(),
+ sp.GetRequiredService>(),
+ sp.GetRequiredService(),
+ sp.GetRequiredService(),
+ sp.GetRequiredService(),
+ sp.GetRequiredService(),
+ scheduleResolver: sp.GetService(),
+ personaStore: sp.GetService(),
+ events: sp.GetService() ?? NoOpStationEventSink.Instance,
+ stationClock: sp.GetService(),
+ patterEstimator: sp.GetService(),
+ contextSettings: sp.GetService() ?? NoOpContextSettingsProvider.Instance,
+ catalog: sp.GetService(),
+ imagingSettings: sp.GetService() ?? NoOpStationImagingSettingsProvider.Instance,
+ crosstalkPlanner: sp.GetService(),
+ announcementSource: sp.GetService(),
+ announcementRenderer: sp.GetService(),
+ voiceLister: sp.GetService(),
+ announcementCopyWriter: sp.GetService(),
+ adCadenceProvider: sp.GetService() ?? NoOpAdCadenceProvider.Instance,
+ adSpotVend: sp.GetService() ?? NoOpAdSpotVend.Instance));
+
+ return services;
}
}
diff --git a/tests/GenWave.Architecture.Tests/GenWave.Architecture.Tests.csproj b/tests/GenWave.Architecture.Tests/GenWave.Architecture.Tests.csproj
index 56b2023e..ef4b3762 100644
--- a/tests/GenWave.Architecture.Tests/GenWave.Architecture.Tests.csproj
+++ b/tests/GenWave.Architecture.Tests/GenWave.Architecture.Tests.csproj
@@ -76,6 +76,7 @@
+
diff --git a/tests/GenWave.Architecture.Tests/Specs/Story451_ConstructionPins.cs b/tests/GenWave.Architecture.Tests/Specs/Story451_ConstructionPins.cs
new file mode 100644
index 00000000..44ea65d0
--- /dev/null
+++ b/tests/GenWave.Architecture.Tests/Specs/Story451_ConstructionPins.cs
@@ -0,0 +1,106 @@
+// STORY-451 — One construction path — the pins (gh-#401 · SPEC F184.3–F184.5 · PLAN T510, T513, T514)
+//
+// BDD specification — xUnit. AC4 scans src/ and tests/ for the Orchestrator's own construction call;
+// AC5 reads Host.Tests' csproj and file list; AC8 reflects the TestSupport assembly for [Fact] methods.
+//
+// GREEN at T514: T513 already un-skipped ScenarioHostTestsAfterTheMove's three facts.
+// ScenarioTheTextScanForConstruction's three facts land here, now that AddGenWaveOrchestration's own
+// factory registration is the second (and last) construction site alongside OrchestratorBuilder.
+
+using System.Reflection;
+using System.Xml.Linq;
+using GenWave.Architecture.Tests.Support;
+
+namespace GenWave.Architecture.Tests.Specs;
+
+public static class FeatureConstructionPins
+{
+ // ---------------------------------------------------------------------
+ // HAPPY PATH
+ // ---------------------------------------------------------------------
+
+ public sealed class ScenarioTheTextScanForConstruction
+ {
+ // Given: every .cs under src/ and tests/ (bin/obj excluded) scanned for the literal below —
+ // split, mirroring Story442_FakeClockPins.cs's own hand-rolled-clock-declaration split, so
+ // this file's own source text never spells the literal out contiguously and self-matches
+ // the scan (and never spells THAT pin's own literal out contiguously either).
+ const string ConstructorCallLiteral = "new " + "Orchestrator(";
+
+ static readonly string[] hits = new[] { "src", "tests" }
+ .SelectMany(dir => Directory.EnumerateFiles(
+ Path.Combine(SolutionLocator.Root(), dir), "*.cs", SearchOption.AllDirectories))
+ .Where(path => !path.Split('/', '\\').Any(segment => segment is "bin" or "obj"))
+ .Where(path => File.ReadAllText(path).Contains(ConstructorCallLiteral, StringComparison.Ordinal))
+ .ToArray();
+
+ /// AC4 — the builder is one site
+ [Fact]
+ public void HitsOrchestratorBuilder() =>
+ Assert.Contains(hits, path => path.EndsWith("OrchestratorBuilder.cs", StringComparison.Ordinal));
+
+ /// AC4 — AddOrchestration is the other site
+ [Fact]
+ public void HitsTheServiceCollectionExtensions() =>
+ Assert.Contains(hits, path => path.EndsWith("OrchestrationServiceCollectionExtensions.cs", StringComparison.Ordinal));
+
+ /// AC4 — exactly two files hit
+ [Fact]
+ public void HitsNothingElse() => Assert.Equal(2, hits.Length);
+ }
+
+ public sealed class ScenarioHostTestsAfterTheMove
+ {
+ // Given: GenWave.Host.Tests.csproj and its file tree
+
+ static readonly string HostTestsDir =
+ Path.Combine(SolutionLocator.Root(), "tests", "GenWave.Host.Tests");
+
+ /// AC5 — the ProjectReference is present
+ [Fact]
+ public void ReferencesTestSupport()
+ {
+ var csprojPath = Path.Combine(HostTestsDir, "GenWave.Host.Tests.csproj");
+ var csproj = XDocument.Load(csprojPath);
+
+ var referencesTestSupport = csproj.Descendants("ProjectReference")
+ .Select(reference => reference.Attribute("Include")?.Value)
+ .Any(include => include is not null
+ && include.EndsWith("GenWave.TestSupport.csproj", StringComparison.Ordinal));
+
+ Assert.True(referencesTestSupport, $"{csprojPath} carries no ProjectReference to GenWave.TestSupport");
+ }
+
+ /// AC5 — the duplicate fake is deleted
+ [Fact]
+ public void NoLongerCarriesFakeRenderBudgetProvider() =>
+ Assert.Empty(Directory.EnumerateFiles(HostTestsDir, "FakeRenderBudgetProvider.cs", SearchOption.AllDirectories));
+
+ /// AC5 — the duplicate fake is deleted
+ [Fact]
+ public void NoLongerCarriesFakeBoundaryBiasProvider() =>
+ Assert.Empty(Directory.EnumerateFiles(HostTestsDir, "FakeBoundaryBiasProvider.cs", SearchOption.AllDirectories));
+ }
+
+ public sealed class ScenarioTheSupportAssembly
+ {
+ // Given: GenWave.TestSupport reflected
+
+ /// AC8 — the project carries fakes, not facts
+ [Fact]
+ public void HasNoFactMethods()
+ {
+ var assembly = typeof(GenWave.TestSupport.AssemblyMarker).Assembly;
+
+ var factMethods = assembly.GetTypes()
+ .SelectMany(type => type.GetMethods(
+ BindingFlags.Public | BindingFlags.NonPublic
+ | BindingFlags.Instance | BindingFlags.Static | BindingFlags.DeclaredOnly))
+ .Where(method => method.GetCustomAttributes(inherit: true)
+ .Any(attribute => attribute is Xunit.FactAttribute))
+ .ToList();
+
+ Assert.Empty(factMethods);
+ }
+ }
+}
diff --git a/tests/GenWave.Architecture.Tests/Specs/Story456_SeamIndex.cs b/tests/GenWave.Architecture.Tests/Specs/Story456_SeamIndex.cs
new file mode 100644
index 00000000..d33edb4a
--- /dev/null
+++ b/tests/GenWave.Architecture.Tests/Specs/Story456_SeamIndex.cs
@@ -0,0 +1,34 @@
+// STORY-456 — The speaker travels with the plan — the seam index (gh-#772 · SPEC F189.7 · PLAN T528)
+//
+// BDD specification — xUnit. AC11 runs tools/SeamIndexGenerator in check mode and reads the committed SEAMS.md.
+//
+// RED at plan time: every fact is [Fact(Skip = Pending)] with a loud body — remove the Skip only in the
+// task that makes it green. The builder comments name the arrange each scenario needs.
+
+namespace GenWave.Architecture.Tests.Specs;
+
+public static class FeatureSeamIndexListsTheSpeakerSource
+{
+ const string Pending = "pending: T528 — SEAMS.md regenerated with ISpeakerSnapshotSource and IPersonaCardByIdSource (STORY-456)";
+
+ // ---------------------------------------------------------------------
+ // HAPPY PATH
+ // ---------------------------------------------------------------------
+
+ public sealed class ScenarioTheCommittedSeamIndex
+ {
+ // Given: SEAMS.md at the repo root
+
+ /// AC11 —
+ [Fact(Skip = Pending)]
+ public void ListsISpeakerSnapshotSource() => Assert.Fail(Pending);
+
+ /// AC11 —
+ [Fact(Skip = Pending)]
+ public void ListsIPersonaCardByIdSource() => Assert.Fail(Pending);
+
+ /// AC11 —
+ [Fact(Skip = Pending)]
+ public void IsByteIdenticalToTheGenerator() => Assert.Fail(Pending);
+ }
+}
diff --git a/tests/GenWave.Architecture.Tests/Specs/Story460_TheOrchestratorAfter.cs b/tests/GenWave.Architecture.Tests/Specs/Story460_TheOrchestratorAfter.cs
new file mode 100644
index 00000000..17d3b201
--- /dev/null
+++ b/tests/GenWave.Architecture.Tests/Specs/Story460_TheOrchestratorAfter.cs
@@ -0,0 +1,79 @@
+// STORY-460 — The Orchestrator after (gh-#401 · SPEC F193 · PLAN T537)
+//
+// BDD specification — xUnit. AC1 reflects every public constructor in GenWave.Orchestration; AC2 is a text scan; AC3 reflects the Orchestrator;
+// AC4 reads the replay's ceremony-only trace; AC5 is the existing laws.
+//
+// RED at plan time: every fact is [Fact(Skip = Pending)] with a loud body — remove the Skip only in the
+// task that makes it green. The builder comments name the arrange each scenario needs.
+
+namespace GenWave.Architecture.Tests.Specs;
+
+public static class FeatureTheOrchestratorAfter
+{
+ const string Pending = "pending: T537 — fitness pins: no optional seam params, two construction sites, the public surface (STORY-460)";
+
+ // ---------------------------------------------------------------------
+ // HAPPY PATH
+ // ---------------------------------------------------------------------
+
+ public sealed class ScenarioEveryPublicConstructorInOrchestration
+ {
+ // Given: parameters reflected
+
+ /// AC1 —
+ [Fact(Skip = Pending)]
+ public void NoInterfaceParameterHasADefault() => Assert.Fail(Pending);
+ }
+
+ public sealed class ScenarioTheTextScanAfterTheSplit
+ {
+ // Given: the Orchestrator's own construction call, scanned over src/ and tests/
+
+ /// AC2 —
+ [Fact(Skip = Pending)]
+ public void HitsExactlyTheBuilderAndTheRoot() => Assert.Fail(Pending);
+ }
+
+ public sealed class ScenarioTheOrchestratorReflected
+ {
+ // Given: typeof(Orchestrator)
+
+ /// AC3 —
+ [Fact(Skip = Pending)]
+ public void ImplementsINextItemProvider() => Assert.Fail(Pending);
+
+ /// AC3 —
+ [Fact(Skip = Pending)]
+ public void ImplementsIBoundaryFitLog() => Assert.Fail(Pending);
+
+ /// AC3 —
+ [Fact(Skip = Pending)]
+ public void ExposesSignOffLeadTime() => Assert.Fail(Pending);
+
+ /// AC3 —
+ [Fact(Skip = Pending)]
+ public void ExposesTimeDateHonestyThreshold() => Assert.Fail(Pending);
+ }
+
+ public sealed class ScenarioTheCeremonyOnlyUnitsTrace
+ {
+ // Given: the replay's ceremony-only unit
+
+ /// AC4 —
+ [Fact(Skip = Pending)]
+ public void ListsTheSignOff() => Assert.Fail(Pending);
+
+ /// AC4 —
+ [Fact(Skip = Pending)]
+ public void ListsNoLeadIn() => Assert.Fail(Pending);
+ }
+
+ public sealed class ScenarioTheLaws
+ {
+ // Given: L1, L5, L10, L11
+
+ /// AC5 —
+ [Fact(Skip = Pending)]
+ public void AllGreen() => Assert.Fail(Pending);
+ }
+}
diff --git a/tests/GenWave.Host.Tests/FakeRenderBudgetProvider.cs b/tests/GenWave.Host.Tests/FakeRenderBudgetProvider.cs
deleted file mode 100644
index 4fb0a92b..00000000
--- a/tests/GenWave.Host.Tests/FakeRenderBudgetProvider.cs
+++ /dev/null
@@ -1,16 +0,0 @@
-using GenWave.Core.Abstractions;
-
-namespace GenWave.Host.Tests;
-
-///
-/// Mutable double (SPEC F44.2, mirrors
-/// one seam over). Set between calls to simulate a live
-/// IOptionsMonitor<TtsOptions> reload without standing up a real options stack in a
-/// unit test.
-///
-sealed class FakeRenderBudgetProvider(TimeSpan budget) : IRenderBudgetProvider
-{
- public TimeSpan Budget { get; set; } = budget;
-
- public TimeSpan Current => Budget;
-}
diff --git a/tests/GenWave.Host.Tests/GenWave.Host.Tests.csproj b/tests/GenWave.Host.Tests/GenWave.Host.Tests.csproj
index 85b11bbe..db9836ce 100644
--- a/tests/GenWave.Host.Tests/GenWave.Host.Tests.csproj
+++ b/tests/GenWave.Host.Tests/GenWave.Host.Tests.csproj
@@ -50,4 +50,8 @@
+
+
+
+
diff --git a/tests/GenWave.Host.Tests/Specs/Story012_AcceptanceGate01_RenderAheadGracefulSkip.cs b/tests/GenWave.Host.Tests/Specs/Story012_AcceptanceGate01_RenderAheadGracefulSkip.cs
index c4d2c210..99cb3b38 100644
--- a/tests/GenWave.Host.Tests/Specs/Story012_AcceptanceGate01_RenderAheadGracefulSkip.cs
+++ b/tests/GenWave.Host.Tests/Specs/Story012_AcceptanceGate01_RenderAheadGracefulSkip.cs
@@ -15,6 +15,7 @@
using GenWave.Host.Tests.Fakes;
using GenWave.Host.Tests.Support;
using GenWave.Orchestration;
+using GenWave.TestSupport;
using GenWave.Tts;
namespace GenWave.Host.Tests.Specs;
@@ -184,12 +185,21 @@ static Orchestrator BuildOrchestrator(
var rotationProvider = new FakeRotationSettingsProvider(new RotationSettings());
var musicSelectionPolicy = new MusicSelectionPolicy(catalog, NullLogger.Instance);
- return new Orchestrator(
- identityProvider, scopeProvider, cadenceProvider, rotationProvider, musicSelectionPolicy, segmentSource,
- new NoOpActivePersonaAccessor(), NullLogger.Instance,
- new FakeRenderBudgetProvider(renderBudget),
- new SpeechDeferralQueue(TimeProvider.System),
- TimeProvider.System, new FakeBoundaryBiasProvider(TimeSpan.Zero));
+ return new OrchestratorBuilder()
+ .WithIdentity(identityProvider)
+ .WithScope(scopeProvider)
+ .WithCadence(cadenceProvider)
+ .WithRotation(rotationProvider)
+ .WithMusicSelectionPolicy(musicSelectionPolicy)
+ .WithTts(segmentSource)
+ .WithPersonaAccessor(new NoOpActivePersonaAccessor())
+ .WithLogger(NullLogger.Instance)
+ .WithRenderBudget(renderBudget)
+ .WithDeferralQueue(new SpeechDeferralQueue(TimeProvider.System))
+ .WithTime(TimeProvider.System)
+ .WithLookahead(TimeSpan.Zero)
+ .Build()
+ .Orchestrator;
}
// -------------------------------------------------------------------------
diff --git a/tests/GenWave.Host.Tests/Specs/Story102_MainScopeLiveness.cs b/tests/GenWave.Host.Tests/Specs/Story102_MainScopeLiveness.cs
index cf5e63da..4edabd50 100644
--- a/tests/GenWave.Host.Tests/Specs/Story102_MainScopeLiveness.cs
+++ b/tests/GenWave.Host.Tests/Specs/Story102_MainScopeLiveness.cs
@@ -19,6 +19,7 @@
using GenWave.Core.Domain;
using GenWave.Host.Api;
using GenWave.Orchestration;
+using GenWave.TestSupport;
namespace GenWave.Host.Tests.Specs;
@@ -210,13 +211,21 @@ public async Task SelectionHonorsAWidenedScopeWithoutRebuild()
var cadenceProvider = new FakeCadenceProvider(SilentCadence);
var rotationProvider = new FakeRotationSettingsProvider(new RotationSettings());
var musicSelectionPolicy = new MusicSelectionPolicy(catalog, NullLogger.Instance);
- var orchestrator = new Orchestrator(
- identityProvider, scopeProvider, cadenceProvider, rotationProvider, musicSelectionPolicy,
- new NoOpTtsSegmentSource(),
- new NoOpActivePersonaAccessor(), NullLogger.Instance,
- new FakeRenderBudgetProvider(TimeSpan.FromSeconds(5)),
- new SpeechDeferralQueue(TimeProvider.System),
- TimeProvider.System, new FakeBoundaryBiasProvider(TimeSpan.Zero));
+ var orchestrator = new OrchestratorBuilder()
+ .WithIdentity(identityProvider)
+ .WithScope(scopeProvider)
+ .WithCadence(cadenceProvider)
+ .WithRotation(rotationProvider)
+ .WithMusicSelectionPolicy(musicSelectionPolicy)
+ .WithTts(new NoOpTtsSegmentSource())
+ .WithPersonaAccessor(new NoOpActivePersonaAccessor())
+ .WithLogger(NullLogger.Instance)
+ .WithRenderBudget(TimeSpan.FromSeconds(5))
+ .WithDeferralQueue(new SpeechDeferralQueue(TimeProvider.System))
+ .WithTime(TimeProvider.System)
+ .WithLookahead(TimeSpan.Zero)
+ .Build()
+ .Orchestrator;
await orchestrator.GetNextAsync(new PlayoutContext([]), CancellationToken.None);
Assert.Equal(new long[] { 1L }, catalog.Scopes[0].LibraryIds);
@@ -290,13 +299,21 @@ public async Task EmptyEffectiveScopeStillReturnsNothing()
var cadenceProvider = new FakeCadenceProvider(SilentCadence);
var rotationProvider = new FakeRotationSettingsProvider(new RotationSettings());
var musicSelectionPolicy = new MusicSelectionPolicy(catalog, NullLogger.Instance);
- var orchestrator = new Orchestrator(
- identityProvider, scopeProvider, cadenceProvider, rotationProvider, musicSelectionPolicy,
- new NoOpTtsSegmentSource(),
- new NoOpActivePersonaAccessor(), NullLogger.Instance,
- new FakeRenderBudgetProvider(TimeSpan.FromSeconds(5)),
- new SpeechDeferralQueue(TimeProvider.System),
- TimeProvider.System, new FakeBoundaryBiasProvider(TimeSpan.Zero));
+ var orchestrator = new OrchestratorBuilder()
+ .WithIdentity(identityProvider)
+ .WithScope(scopeProvider)
+ .WithCadence(cadenceProvider)
+ .WithRotation(rotationProvider)
+ .WithMusicSelectionPolicy(musicSelectionPolicy)
+ .WithTts(new NoOpTtsSegmentSource())
+ .WithPersonaAccessor(new NoOpActivePersonaAccessor())
+ .WithLogger(NullLogger.Instance)
+ .WithRenderBudget(TimeSpan.FromSeconds(5))
+ .WithDeferralQueue(new SpeechDeferralQueue(TimeProvider.System))
+ .WithTime(TimeProvider.System)
+ .WithLookahead(TimeSpan.Zero)
+ .Build()
+ .Orchestrator;
var item = await orchestrator.GetNextAsync(new PlayoutContext([]), CancellationToken.None);
diff --git a/tests/GenWave.Host.Tests/Specs/Story139_SettingsSurfaceCompletion.cs b/tests/GenWave.Host.Tests/Specs/Story139_SettingsSurfaceCompletion.cs
index c35aa777..71803aa6 100644
--- a/tests/GenWave.Host.Tests/Specs/Story139_SettingsSurfaceCompletion.cs
+++ b/tests/GenWave.Host.Tests/Specs/Story139_SettingsSurfaceCompletion.cs
@@ -20,6 +20,8 @@
using GenWave.Host.Playout;
using GenWave.Host.Tests.Fakes;
using GenWave.Orchestration;
+using GenWave.TestSupport;
+using GenWave.TestSupport.Fakes;
// Alias to disambiguate GenWave.Loudness (the analyzer project) from the Loudness domain type.
using CoreLoudness = GenWave.Core.Domain.Loudness;
@@ -178,11 +180,21 @@ public async Task TheOrchestratorRenderBudgetReadsTheMonitorNotAFrozenTimeSpan()
var budgetProvider = new FakeRenderBudgetProvider(TimeSpan.FromSeconds(1));
var musicSelectionPolicy = new MusicSelectionPolicy(catalog, NullLogger.Instance);
- var orchestrator = new Orchestrator(
- identityProvider, scopeProvider, cadenceProvider, rotationProvider, musicSelectionPolicy, tts,
- new NoOpActivePersonaAccessor(), NullLogger.Instance, budgetProvider,
- new SpeechDeferralQueue(time),
- time, new FakeBoundaryBiasProvider(TimeSpan.Zero));
+ var orchestrator = new OrchestratorBuilder()
+ .WithIdentity(identityProvider)
+ .WithScope(scopeProvider)
+ .WithCadence(cadenceProvider)
+ .WithRotation(rotationProvider)
+ .WithMusicSelectionPolicy(musicSelectionPolicy)
+ .WithTts(tts)
+ .WithPersonaAccessor(new NoOpActivePersonaAccessor())
+ .WithLogger(NullLogger.Instance)
+ .WithRenderBudget(budgetProvider)
+ .WithDeferralQueue(new SpeechDeferralQueue(time))
+ .WithTime(time)
+ .WithLookahead(TimeSpan.Zero)
+ .Build()
+ .Orchestrator;
var ctx = new PlayoutContext([]);
// Unit 1 — budget (1s) is far shorter than the render delay (10s): the budget timer is
diff --git a/tests/GenWave.Host.Tests/Specs/Story456_PersonaCardById.cs b/tests/GenWave.Host.Tests/Specs/Story456_PersonaCardById.cs
new file mode 100644
index 00000000..d3884219
--- /dev/null
+++ b/tests/GenWave.Host.Tests/Specs/Story456_PersonaCardById.cs
@@ -0,0 +1,31 @@
+// STORY-456 — The speaker travels with the plan — a card by id (gh-#772 · SPEC F189.2 · PLAN T525)
+//
+// BDD specification — xUnit. AC10 drives the Host IPersonaCardByIdSource implementation against the Postgres fixture: a persona row whose
+// card carries one pronunciation rule.
+//
+// RED at plan time: every fact is [Fact(Skip = Pending)] with a loud body — remove the Skip only in the
+// task that makes it green. The builder comments name the arrange each scenario needs.
+
+namespace GenWave.Host.Tests.Specs;
+
+public static class FeaturePersonaCardById
+{
+ const string Pending = "pending: T525 — the Host card-by-id store reads a persona card by id (STORY-456)";
+
+ // ---------------------------------------------------------------------
+ // HAPPY PATH
+ // ---------------------------------------------------------------------
+
+ public sealed class ScenarioAPersonaRowWithACard
+ {
+ // Given: the card carries one pronunciation rule
+
+ /// AC10 —
+ [Fact(Skip = Pending)]
+ public void ResolvesTheCardById() => Assert.Fail(Pending);
+
+ /// AC10 —
+ [Fact(Skip = Pending)]
+ public void TheSnapshotCarriesTheRule() => Assert.Fail(Pending);
+ }
+}
diff --git a/tests/GenWave.Orchestration.Tests/Fakes/FakeBoundaryBiasProvider.cs b/tests/GenWave.Orchestration.Tests/Fakes/FakeBoundaryBiasProvider.cs
deleted file mode 100644
index 55a71ec8..00000000
--- a/tests/GenWave.Orchestration.Tests/Fakes/FakeBoundaryBiasProvider.cs
+++ /dev/null
@@ -1,16 +0,0 @@
-using GenWave.Core.Abstractions;
-
-namespace GenWave.Orchestration.Tests.Fakes;
-
-///
-/// Mutable double (SPEC F74.3, mirrors
-/// one seam over). Set between
-/// calls to simulate a config-provider reload without standing up a real options stack in a unit
-/// test.
-///
-sealed class FakeBoundaryBiasProvider(TimeSpan lookahead) : IBoundaryBiasProvider
-{
- public TimeSpan Lookahead { get; set; } = lookahead;
-
- public TimeSpan Current => Lookahead;
-}
diff --git a/tests/GenWave.Orchestration.Tests/Fakes/ProductionChainHarness.cs b/tests/GenWave.Orchestration.Tests/Fakes/ProductionChainHarness.cs
index 15f0b08b..fa637a1d 100644
--- a/tests/GenWave.Orchestration.Tests/Fakes/ProductionChainHarness.cs
+++ b/tests/GenWave.Orchestration.Tests/Fakes/ProductionChainHarness.cs
@@ -83,46 +83,38 @@ public static ProductionChain BuildProductionChain(
var mediaCatalog = catalog ?? new FakeMediaCatalog(MakeTrackRef("t1"));
var musicSelectionPolicy = new MusicSelectionPolicy(mediaCatalog, NullLogger.Instance);
- var orchestrator = new Orchestrator(
- identityProvider, scopeProvider, cadenceProvider, rotationProvider, musicSelectionPolicy,
- tts, personaAccessor, logger,
- new FakeRenderBudgetProvider(renderBudget ?? TimeSpan.FromSeconds(5)),
- queue,
- time, new FakeBoundaryBiasProvider(lookahead),
- scheduleResolver: caching,
- personaStore: personaStore,
- events: events,
- catalog: mediaCatalog,
- patterEstimator: patterEstimator,
- crosstalkPlanner: crosstalkPlanner);
+ var orchestrator = new OrchestratorBuilder()
+ .WithIdentity(identityProvider)
+ .WithScope(scopeProvider)
+ .WithCadence(cadenceProvider)
+ .WithRotation(rotationProvider)
+ .WithMusicSelectionPolicy(musicSelectionPolicy)
+ .WithTts(tts)
+ .WithPersonaAccessor(personaAccessor)
+ .WithLogger(logger)
+ .WithRenderBudget(renderBudget ?? TimeSpan.FromSeconds(5))
+ .WithDeferralQueue(queue)
+ .WithTime(time)
+ .WithBoundaryBias(new FakeBoundaryBiasProvider(lookahead))
+ .WithScheduleResolver(caching)
+ .WithPersonaStore(personaStore)
+ .WithEvents(events)
+ .WithCatalog(mediaCatalog)
+ .WithPatterEstimator(patterEstimator)
+ .WithCrosstalkPlanner(crosstalkPlanner)
+ .Build()
+ .Orchestrator;
return new ProductionChain(orchestrator, queue, time, scheduleStore, tts, events, logger, mediaCatalog);
}
- public static Persona MakePersona(long id, string name, string voice)
- {
- var now = DateTime.UnixEpoch;
- return new Persona(id, name, "", "", voice, now, now);
- }
+ // Moved to GenWave.TestSupport.TestData at PLAN T511 (OrchestratorBuilder's own defaults need
+ // these too); forwarding keeps every existing call site in this project unchanged until T512 folds this harness into the builder.
+ public static Persona MakePersona(long id, string name, string voice) =>
+ TestData.MakePersona(id, name, voice);
- public static MediaReference MakeTrackRef(string id) => new(
- MediaId: id,
- Locator: $"/media/{id}.mp3",
- Title: $"Track {id}",
- Loudness: new Loudness(-23.0, -1.0, true),
- DurationMs: null,
- SampleRate: null,
- Channels: null,
- BitrateKbps: null,
- Artist: null,
- Album: null,
- Genre: null,
- Year: null);
+ public static MediaReference MakeTrackRef(string id) => TestData.MakeTrackRef(id);
- public static FakePersonaStore OneDjStore(long id, string name, string voice)
- {
- var store = new FakePersonaStore();
- store.Add(MakePersona(id, name, voice));
- return store;
- }
+ public static FakePersonaStore OneDjStore(long id, string name, string voice) =>
+ TestData.OneDjStore(id, name, voice);
}
diff --git a/tests/GenWave.Orchestration.Tests/GenWave.Orchestration.Tests.csproj b/tests/GenWave.Orchestration.Tests/GenWave.Orchestration.Tests.csproj
index b2803b4b..185875ef 100644
--- a/tests/GenWave.Orchestration.Tests/GenWave.Orchestration.Tests.csproj
+++ b/tests/GenWave.Orchestration.Tests/GenWave.Orchestration.Tests.csproj
@@ -10,6 +10,7 @@
+
@@ -18,10 +19,13 @@
+
+
+
diff --git a/tests/GenWave.Orchestration.Tests/Specs/Gh117_StationLocalSegmentClock.cs b/tests/GenWave.Orchestration.Tests/Specs/Gh117_StationLocalSegmentClock.cs
index a3814764..4ded80d7 100644
--- a/tests/GenWave.Orchestration.Tests/Specs/Gh117_StationLocalSegmentClock.cs
+++ b/tests/GenWave.Orchestration.Tests/Specs/Gh117_StationLocalSegmentClock.cs
@@ -51,20 +51,22 @@ public static class FeatureStationLocalSegmentClock
var tts = new FakeTtsSegmentSource();
var musicSelectionPolicy = new MusicSelectionPolicy(
new FakeMediaCatalog(MakeRef("track1")), NullLogger.Instance);
- var orchestrator = new Orchestrator(
- new FakeStationIdentityProvider(new StationIdentity("s1", "GenWave", "af_heart")),
- new FakeStationScopeProvider(new LibraryScope([1L])),
- new FakeCadenceProvider(cadence),
- new FakeRotationSettingsProvider(new RotationSettings()),
- musicSelectionPolicy,
- tts,
- new FakeActivePersonaAccessor(),
- NullLogger.Instance,
- new FakeRenderBudgetProvider(TimeSpan.FromSeconds(30)),
- new SpeechDeferralQueue(new FakeTimeProvider(FixedUtc)),
- new FakeTimeProvider(FixedUtc),
- new FakeBoundaryBiasProvider(TimeSpan.Zero),
- stationClock: stationClock);
+ var orchestrator = new OrchestratorBuilder()
+ .WithIdentity(new FakeStationIdentityProvider(new StationIdentity("s1", "GenWave", "af_heart")))
+ .WithScope(new FakeStationScopeProvider(new LibraryScope([1L])))
+ .WithCadence(cadence)
+ .WithRotation(new FakeRotationSettingsProvider(new RotationSettings()))
+ .WithMusicSelectionPolicy(musicSelectionPolicy)
+ .WithTts(tts)
+ .WithPersonaAccessor(new FakeActivePersonaAccessor())
+ .WithLogger(NullLogger.Instance)
+ .WithRenderBudget(TimeSpan.FromSeconds(30))
+ .WithDeferralQueue(new SpeechDeferralQueue(new FakeTimeProvider(FixedUtc)))
+ .WithTime(new FakeTimeProvider(FixedUtc))
+ .WithBoundaryBias(new FakeBoundaryBiasProvider(TimeSpan.Zero))
+ .WithStationClock(stationClock)
+ .Build()
+ .Orchestrator;
return (orchestrator, tts);
}
diff --git a/tests/GenWave.Orchestration.Tests/Specs/Gh253_PatterDurationEstimator.cs b/tests/GenWave.Orchestration.Tests/Specs/Gh253_PatterDurationEstimator.cs
index 91fe1556..68a4eeab 100644
--- a/tests/GenWave.Orchestration.Tests/Specs/Gh253_PatterDurationEstimator.cs
+++ b/tests/GenWave.Orchestration.Tests/Specs/Gh253_PatterDurationEstimator.cs
@@ -257,25 +257,27 @@ public static async Task Rendered_segment_durations_flow_into_the_estimator()
var clock = new FakeTimeProvider(DateTimeOffset.Parse("2030-01-01T00:00:00Z"));
var estimator = new RollingPatterDurationEstimator();
var tts = new FakeTtsSegmentSource { DurationMs = 12_000 };
- var orchestrator = new Orchestrator(
- new FakeStationIdentityProvider(new StationIdentity("s1", "GenWave", "default")),
- new FakeStationScopeProvider(new LibraryScope([1L])),
- new FakeCadenceProvider(new CadenceConfig
+ var orchestrator = new OrchestratorBuilder()
+ .WithIdentity(new FakeStationIdentityProvider(new StationIdentity("s1", "GenWave", "default")))
+ .WithScope(new FakeStationScopeProvider(new LibraryScope([1L])))
+ .WithCadence(new CadenceConfig
{
LeadInBeforeEachTrack = true,
BackAnnounceAfterEachTrack = false,
StationIdEveryNUnits = 0,
- }),
- new FakeRotationSettingsProvider(new RotationSettings()),
- new MusicSelectionPolicy(new FakeMediaCatalog(MakeTrack("m1")), NullLogger.Instance),
- tts,
- new FakeActivePersonaAccessor(),
- NullLogger.Instance,
- new FakeRenderBudgetProvider(TimeSpan.FromSeconds(30)),
- new SpeechDeferralQueue(clock),
- clock,
- new FakeBoundaryBiasProvider(TimeSpan.FromMinutes(10)),
- patterEstimator: estimator);
+ })
+ .WithRotation(new FakeRotationSettingsProvider(new RotationSettings()))
+ .WithMusicSelectionPolicy(new MusicSelectionPolicy(new FakeMediaCatalog(MakeTrack("m1")), NullLogger.Instance))
+ .WithTts(tts)
+ .WithPersonaAccessor(new FakeActivePersonaAccessor())
+ .WithLogger(NullLogger.Instance)
+ .WithRenderBudget(TimeSpan.FromSeconds(30))
+ .WithDeferralQueue(new SpeechDeferralQueue(clock))
+ .WithTime(clock)
+ .WithBoundaryBias(new FakeBoundaryBiasProvider(TimeSpan.FromMinutes(10)))
+ .WithPatterEstimator(estimator)
+ .Build()
+ .Orchestrator;
// When three units are planned (each unit = one lead-in + one music item)
var ctx = new PlayoutContext([]);
@@ -294,25 +296,27 @@ public static async Task A_render_without_a_measured_duration_is_never_observed(
var clock = new FakeTimeProvider(DateTimeOffset.Parse("2030-01-01T00:00:00Z"));
var estimator = new RollingPatterDurationEstimator();
var tts = new FakeTtsSegmentSource { DurationMs = null };
- var orchestrator = new Orchestrator(
- new FakeStationIdentityProvider(new StationIdentity("s1", "GenWave", "default")),
- new FakeStationScopeProvider(new LibraryScope([1L])),
- new FakeCadenceProvider(new CadenceConfig
+ var orchestrator = new OrchestratorBuilder()
+ .WithIdentity(new FakeStationIdentityProvider(new StationIdentity("s1", "GenWave", "default")))
+ .WithScope(new FakeStationScopeProvider(new LibraryScope([1L])))
+ .WithCadence(new CadenceConfig
{
LeadInBeforeEachTrack = true,
BackAnnounceAfterEachTrack = false,
StationIdEveryNUnits = 0,
- }),
- new FakeRotationSettingsProvider(new RotationSettings()),
- new MusicSelectionPolicy(new FakeMediaCatalog(MakeTrack("m1")), NullLogger.Instance),
- tts,
- new FakeActivePersonaAccessor(),
- NullLogger.Instance,
- new FakeRenderBudgetProvider(TimeSpan.FromSeconds(30)),
- new SpeechDeferralQueue(clock),
- clock,
- new FakeBoundaryBiasProvider(TimeSpan.FromMinutes(10)),
- patterEstimator: estimator);
+ })
+ .WithRotation(new FakeRotationSettingsProvider(new RotationSettings()))
+ .WithMusicSelectionPolicy(new MusicSelectionPolicy(new FakeMediaCatalog(MakeTrack("m1")), NullLogger.Instance))
+ .WithTts(tts)
+ .WithPersonaAccessor(new FakeActivePersonaAccessor())
+ .WithLogger(NullLogger.Instance)
+ .WithRenderBudget(TimeSpan.FromSeconds(30))
+ .WithDeferralQueue(new SpeechDeferralQueue(clock))
+ .WithTime(clock)
+ .WithBoundaryBias(new FakeBoundaryBiasProvider(TimeSpan.FromMinutes(10)))
+ .WithPatterEstimator(estimator)
+ .Build()
+ .Orchestrator;
// When units are planned
var ctx = new PlayoutContext([]);
diff --git a/tests/GenWave.Orchestration.Tests/Specs/Gh259_AttributionFromTheAiringItem.cs b/tests/GenWave.Orchestration.Tests/Specs/Gh259_AttributionFromTheAiringItem.cs
index feacc6e6..afc8e4f7 100644
--- a/tests/GenWave.Orchestration.Tests/Specs/Gh259_AttributionFromTheAiringItem.cs
+++ b/tests/GenWave.Orchestration.Tests/Specs/Gh259_AttributionFromTheAiringItem.cs
@@ -32,18 +32,23 @@ static Persona BuildPersona(string name) =>
BackAnnounceAfterEachTrack = false,
StationIdEveryNUnits = stationIdEveryNUnits,
};
- var identityProvider = new FakeStationIdentityProvider(new StationIdentity("s1", "GenWave", "af_heart"));
- var scopeProvider = new FakeStationScopeProvider(new LibraryScope([1L]));
- var cadenceProvider = new FakeCadenceProvider(cadence);
- var rotationProvider = new FakeRotationSettingsProvider(new RotationSettings());
var catalog = new FakeMediaCatalog(MakeRef("track1"));
var tts = new FakeTtsSegmentSource();
- var musicSelectionPolicy = new MusicSelectionPolicy(catalog, NullLogger.Instance);
- var orchestrator = new Orchestrator(
- identityProvider, scopeProvider, cadenceProvider, rotationProvider, musicSelectionPolicy, tts, accessor,
- NullLogger.Instance, new FakeRenderBudgetProvider(TimeSpan.FromSeconds(30)),
- new SpeechDeferralQueue(TimeProvider.System),
- TimeProvider.System, new FakeBoundaryBiasProvider(TimeSpan.Zero));
+ var orchestrator = new OrchestratorBuilder()
+ .WithIdentity(new FakeStationIdentityProvider(new StationIdentity("s1", "GenWave", "af_heart")))
+ .WithScope(new FakeStationScopeProvider(new LibraryScope([1L])))
+ .WithCadence(cadence)
+ .WithRotation(new FakeRotationSettingsProvider(new RotationSettings()))
+ .WithMusicSelectionPolicy(new MusicSelectionPolicy(catalog, NullLogger.Instance))
+ .WithTts(tts)
+ .WithPersonaAccessor(accessor)
+ .WithLogger(NullLogger.Instance)
+ .WithRenderBudget(TimeSpan.FromSeconds(30))
+ .WithDeferralQueue(new SpeechDeferralQueue(TimeProvider.System))
+ .WithTime(TimeProvider.System)
+ .WithBoundaryBias(new FakeBoundaryBiasProvider(TimeSpan.Zero))
+ .Build()
+ .Orchestrator;
return (orchestrator, tts);
}
diff --git a/tests/GenWave.Orchestration.Tests/Specs/Story007_OrchestratorPlanner.cs b/tests/GenWave.Orchestration.Tests/Specs/Story007_OrchestratorPlanner.cs
index 9b371c8c..cd150359 100644
--- a/tests/GenWave.Orchestration.Tests/Specs/Story007_OrchestratorPlanner.cs
+++ b/tests/GenWave.Orchestration.Tests/Specs/Story007_OrchestratorPlanner.cs
@@ -25,20 +25,22 @@ static Orchestrator BuildOrchestrator(
ITtsSegmentSource ttsSource,
CadenceConfig? cadence = null,
LibraryScope? scope = null,
- TimeSpan? renderBudget = null)
- {
- var identityProvider = new FakeStationIdentityProvider(new StationIdentity("s1", "GenWave", "default"));
- var scopeProvider = new FakeStationScopeProvider(scope ?? new LibraryScope([1L]));
- var cadenceProvider = new FakeCadenceProvider(cadence ?? new CadenceConfig());
- var rotationProvider = new FakeRotationSettingsProvider(new RotationSettings());
- var musicSelectionPolicy = new MusicSelectionPolicy(catalog, NullLogger.Instance);
- return new Orchestrator(
- identityProvider, scopeProvider, cadenceProvider, rotationProvider, musicSelectionPolicy, ttsSource,
- new FakeActivePersonaAccessor(), NullLogger.Instance,
- new FakeRenderBudgetProvider(renderBudget ?? TimeSpan.FromSeconds(30)),
- new SpeechDeferralQueue(TimeProvider.System),
- TimeProvider.System, new FakeBoundaryBiasProvider(TimeSpan.Zero));
- }
+ TimeSpan? renderBudget = null) =>
+ new OrchestratorBuilder()
+ .WithIdentity(new FakeStationIdentityProvider(new StationIdentity("s1", "GenWave", "default")))
+ .WithScope(new FakeStationScopeProvider(scope ?? new LibraryScope([1L])))
+ .WithCadence(cadence ?? new CadenceConfig())
+ .WithRotation(new FakeRotationSettingsProvider(new RotationSettings()))
+ .WithMusicSelectionPolicy(new MusicSelectionPolicy(catalog, NullLogger.Instance))
+ .WithTts(ttsSource)
+ .WithPersonaAccessor(new FakeActivePersonaAccessor())
+ .WithLogger(NullLogger.Instance)
+ .WithRenderBudget(renderBudget ?? TimeSpan.FromSeconds(30))
+ .WithDeferralQueue(new SpeechDeferralQueue(TimeProvider.System))
+ .WithTime(TimeProvider.System)
+ .WithBoundaryBias(new FakeBoundaryBiasProvider(TimeSpan.Zero))
+ .Build()
+ .Orchestrator;
static List ProduceN(Orchestrator orchestrator, int n)
{
@@ -262,16 +264,21 @@ public async Task SecondUnitHonorsACadenceChangeMadeAfterTheFirst()
BackAnnounceAfterEachTrack = false,
StationIdEveryNUnits = 0,
});
- var identityProvider = new FakeStationIdentityProvider(new StationIdentity("s1", "GenWave", "default"));
- var scopeProvider = new FakeStationScopeProvider(new LibraryScope([1L]));
- var rotationProvider = new FakeRotationSettingsProvider(new RotationSettings());
- var musicSelectionPolicy = new MusicSelectionPolicy(catalog, NullLogger.Instance);
- var orchestrator = new Orchestrator(
- identityProvider, scopeProvider, cadenceProvider, rotationProvider, musicSelectionPolicy, tts,
- new FakeActivePersonaAccessor(), NullLogger.Instance,
- new FakeRenderBudgetProvider(TimeSpan.FromSeconds(30)),
- new SpeechDeferralQueue(TimeProvider.System),
- TimeProvider.System, new FakeBoundaryBiasProvider(TimeSpan.Zero));
+ var orchestrator = new OrchestratorBuilder()
+ .WithIdentity(new FakeStationIdentityProvider(new StationIdentity("s1", "GenWave", "default")))
+ .WithScope(new FakeStationScopeProvider(new LibraryScope([1L])))
+ .WithCadence(cadenceProvider)
+ .WithRotation(new FakeRotationSettingsProvider(new RotationSettings()))
+ .WithMusicSelectionPolicy(new MusicSelectionPolicy(catalog, NullLogger.Instance))
+ .WithTts(tts)
+ .WithPersonaAccessor(new FakeActivePersonaAccessor())
+ .WithLogger(NullLogger.Instance)
+ .WithRenderBudget(TimeSpan.FromSeconds(30))
+ .WithDeferralQueue(new SpeechDeferralQueue(TimeProvider.System))
+ .WithTime(TimeProvider.System)
+ .WithBoundaryBias(new FakeBoundaryBiasProvider(TimeSpan.Zero))
+ .Build()
+ .Orchestrator;
var ctx = new PlayoutContext([]);
// Unit 1 — cadence has LeadIn off: the pulled item is music, no lead-in precedes it.
diff --git a/tests/GenWave.Orchestration.Tests/Specs/Story008_RenderAheadGracefulSkipToMusic.cs b/tests/GenWave.Orchestration.Tests/Specs/Story008_RenderAheadGracefulSkipToMusic.cs
index d872de48..f15d54d9 100644
--- a/tests/GenWave.Orchestration.Tests/Specs/Story008_RenderAheadGracefulSkipToMusic.cs
+++ b/tests/GenWave.Orchestration.Tests/Specs/Story008_RenderAheadGracefulSkipToMusic.cs
@@ -24,20 +24,22 @@ static Orchestrator BuildOrchestrator(
FakeMediaCatalog catalog,
FakeTtsSegmentSource ttsSource,
CadenceConfig? cadence = null,
- TimeSpan? renderBudget = null)
- {
- var identityProvider = new FakeStationIdentityProvider(new StationIdentity("s1", "GenWave", "default"));
- var scopeProvider = new FakeStationScopeProvider(new LibraryScope([1L]));
- var cadenceProvider = new FakeCadenceProvider(cadence ?? new CadenceConfig());
- var rotationProvider = new FakeRotationSettingsProvider(new RotationSettings());
- var musicSelectionPolicy = new MusicSelectionPolicy(catalog, NullLogger.Instance);
- return new Orchestrator(
- identityProvider, scopeProvider, cadenceProvider, rotationProvider, musicSelectionPolicy, ttsSource,
- new FakeActivePersonaAccessor(), NullLogger.Instance,
- new FakeRenderBudgetProvider(renderBudget ?? TimeSpan.FromSeconds(30)),
- new SpeechDeferralQueue(TimeProvider.System),
- TimeProvider.System, new FakeBoundaryBiasProvider(TimeSpan.Zero));
- }
+ TimeSpan? renderBudget = null) =>
+ new OrchestratorBuilder()
+ .WithIdentity(new FakeStationIdentityProvider(new StationIdentity("s1", "GenWave", "default")))
+ .WithScope(new FakeStationScopeProvider(new LibraryScope([1L])))
+ .WithCadence(cadence ?? new CadenceConfig())
+ .WithRotation(new FakeRotationSettingsProvider(new RotationSettings()))
+ .WithMusicSelectionPolicy(new MusicSelectionPolicy(catalog, NullLogger.Instance))
+ .WithTts(ttsSource)
+ .WithPersonaAccessor(new FakeActivePersonaAccessor())
+ .WithLogger(NullLogger.Instance)
+ .WithRenderBudget(renderBudget ?? TimeSpan.FromSeconds(30))
+ .WithDeferralQueue(new SpeechDeferralQueue(TimeProvider.System))
+ .WithTime(TimeProvider.System)
+ .WithBoundaryBias(new FakeBoundaryBiasProvider(TimeSpan.Zero))
+ .Build()
+ .Orchestrator;
// ---------------------------------------------------------------------
// HAPPY PATH — config / default value assertions
@@ -218,17 +220,21 @@ public async Task SubsequentSlotsContinueInOrder()
StationIdEveryNUnits = 0,
};
// Use the controllable source directly for this test
- var identityProvider = new FakeStationIdentityProvider(new StationIdentity("s1", "GenWave", "default"));
- var scopeProvider = new FakeStationScopeProvider(new LibraryScope([1L]));
- var cadenceProvider = new FakeCadenceProvider(cadence);
- var rotationProvider = new FakeRotationSettingsProvider(new RotationSettings());
- var musicSelectionPolicy = new MusicSelectionPolicy(catalog, NullLogger.Instance);
- var o2 = new Orchestrator(
- identityProvider, scopeProvider, cadenceProvider, rotationProvider, musicSelectionPolicy, tts,
- new FakeActivePersonaAccessor(), NullLogger.Instance,
- new FakeRenderBudgetProvider(TimeSpan.FromSeconds(5)),
- new SpeechDeferralQueue(TimeProvider.System),
- TimeProvider.System, new FakeBoundaryBiasProvider(TimeSpan.Zero));
+ var o2 = new OrchestratorBuilder()
+ .WithIdentity(new FakeStationIdentityProvider(new StationIdentity("s1", "GenWave", "default")))
+ .WithScope(new FakeStationScopeProvider(new LibraryScope([1L])))
+ .WithCadence(cadence)
+ .WithRotation(new FakeRotationSettingsProvider(new RotationSettings()))
+ .WithMusicSelectionPolicy(new MusicSelectionPolicy(catalog, NullLogger.Instance))
+ .WithTts(tts)
+ .WithPersonaAccessor(new FakeActivePersonaAccessor())
+ .WithLogger(NullLogger.Instance)
+ .WithRenderBudget(TimeSpan.FromSeconds(5))
+ .WithDeferralQueue(new SpeechDeferralQueue(TimeProvider.System))
+ .WithTime(TimeProvider.System)
+ .WithBoundaryBias(new FakeBoundaryBiasProvider(TimeSpan.Zero))
+ .Build()
+ .Orchestrator;
var ctx = new PlayoutContext([]);
// Unit 1: LeadIn dropped (null), so first item is Music
diff --git a/tests/GenWave.Orchestration.Tests/Specs/Story121_PersonaVoiceResolution.cs b/tests/GenWave.Orchestration.Tests/Specs/Story121_PersonaVoiceResolution.cs
index 27b1f762..18f8aa55 100644
--- a/tests/GenWave.Orchestration.Tests/Specs/Story121_PersonaVoiceResolution.cs
+++ b/tests/GenWave.Orchestration.Tests/Specs/Story121_PersonaVoiceResolution.cs
@@ -44,18 +44,23 @@ static Persona BuildPersona(string voice) =>
BackAnnounceAfterEachTrack = false,
StationIdEveryNUnits = 0,
};
- var identityProvider = new FakeStationIdentityProvider(new StationIdentity("s1", "GenWave", stationVoice));
- var scopeProvider = new FakeStationScopeProvider(new LibraryScope([1L]));
- var cadenceProvider = new FakeCadenceProvider(cadence);
- var rotationProvider = new FakeRotationSettingsProvider(new RotationSettings());
var catalog = new FakeMediaCatalog(MakeRef("track1"));
var tts = new FakeTtsSegmentSource();
- var musicSelectionPolicy = new MusicSelectionPolicy(catalog, NullLogger.Instance);
- var orchestrator = new Orchestrator(
- identityProvider, scopeProvider, cadenceProvider, rotationProvider, musicSelectionPolicy, tts, accessor,
- NullLogger.Instance, new FakeRenderBudgetProvider(TimeSpan.FromSeconds(30)),
- new SpeechDeferralQueue(TimeProvider.System),
- TimeProvider.System, new FakeBoundaryBiasProvider(TimeSpan.Zero));
+ var orchestrator = new OrchestratorBuilder()
+ .WithIdentity(new FakeStationIdentityProvider(new StationIdentity("s1", "GenWave", stationVoice)))
+ .WithScope(new FakeStationScopeProvider(new LibraryScope([1L])))
+ .WithCadence(cadence)
+ .WithRotation(new FakeRotationSettingsProvider(new RotationSettings()))
+ .WithMusicSelectionPolicy(new MusicSelectionPolicy(catalog, NullLogger.Instance))
+ .WithTts(tts)
+ .WithPersonaAccessor(accessor)
+ .WithLogger(NullLogger.Instance)
+ .WithRenderBudget(TimeSpan.FromSeconds(30))
+ .WithDeferralQueue(new SpeechDeferralQueue(TimeProvider.System))
+ .WithTime(TimeProvider.System)
+ .WithBoundaryBias(new FakeBoundaryBiasProvider(TimeSpan.Zero))
+ .Build()
+ .Orchestrator;
return (orchestrator, tts);
}
@@ -124,18 +129,23 @@ public sealed class ScenarioStationIdsAreStationImaging
BackAnnounceAfterEachTrack = false,
StationIdEveryNUnits = 1,
};
- var identityProvider = new FakeStationIdentityProvider(new StationIdentity("s1", "GenWave", stationVoice));
- var scopeProvider = new FakeStationScopeProvider(new LibraryScope([1L]));
- var cadenceProvider = new FakeCadenceProvider(cadence);
- var rotationProvider = new FakeRotationSettingsProvider(new RotationSettings());
var catalog = new FakeMediaCatalog(MakeRef("track1"));
var tts = new FakeTtsSegmentSource();
- var musicSelectionPolicy = new MusicSelectionPolicy(catalog, NullLogger.Instance);
- var orchestrator = new Orchestrator(
- identityProvider, scopeProvider, cadenceProvider, rotationProvider, musicSelectionPolicy, tts, accessor,
- NullLogger.Instance, new FakeRenderBudgetProvider(TimeSpan.FromSeconds(30)),
- new SpeechDeferralQueue(TimeProvider.System),
- TimeProvider.System, new FakeBoundaryBiasProvider(TimeSpan.Zero));
+ var orchestrator = new OrchestratorBuilder()
+ .WithIdentity(new FakeStationIdentityProvider(new StationIdentity("s1", "GenWave", stationVoice)))
+ .WithScope(new FakeStationScopeProvider(new LibraryScope([1L])))
+ .WithCadence(cadence)
+ .WithRotation(new FakeRotationSettingsProvider(new RotationSettings()))
+ .WithMusicSelectionPolicy(new MusicSelectionPolicy(catalog, NullLogger.Instance))
+ .WithTts(tts)
+ .WithPersonaAccessor(accessor)
+ .WithLogger(NullLogger.Instance)
+ .WithRenderBudget(TimeSpan.FromSeconds(30))
+ .WithDeferralQueue(new SpeechDeferralQueue(TimeProvider.System))
+ .WithTime(TimeProvider.System)
+ .WithBoundaryBias(new FakeBoundaryBiasProvider(TimeSpan.Zero))
+ .Build()
+ .Orchestrator;
return (orchestrator, tts);
}
diff --git a/tests/GenWave.Orchestration.Tests/Specs/Story131_PersonaAttributionRequestShape.cs b/tests/GenWave.Orchestration.Tests/Specs/Story131_PersonaAttributionRequestShape.cs
index 05e14186..043bf40b 100644
--- a/tests/GenWave.Orchestration.Tests/Specs/Story131_PersonaAttributionRequestShape.cs
+++ b/tests/GenWave.Orchestration.Tests/Specs/Story131_PersonaAttributionRequestShape.cs
@@ -40,18 +40,23 @@ static Persona BuildPersona(string name, string voice) =>
BackAnnounceAfterEachTrack = false,
StationIdEveryNUnits = 0,
};
- var identityProvider = new FakeStationIdentityProvider(new StationIdentity("s1", "GenWave", stationVoice));
- var scopeProvider = new FakeStationScopeProvider(new LibraryScope([1L]));
- var cadenceProvider = new FakeCadenceProvider(cadence);
- var rotationProvider = new FakeRotationSettingsProvider(new RotationSettings());
var catalog = new FakeMediaCatalog(MakeRef("track1"));
var tts = new FakeTtsSegmentSource();
- var musicSelectionPolicy = new MusicSelectionPolicy(catalog, NullLogger.Instance);
- var orchestrator = new Orchestrator(
- identityProvider, scopeProvider, cadenceProvider, rotationProvider, musicSelectionPolicy, tts, accessor,
- NullLogger.Instance, new FakeRenderBudgetProvider(TimeSpan.FromSeconds(30)),
- new SpeechDeferralQueue(TimeProvider.System),
- TimeProvider.System, new FakeBoundaryBiasProvider(TimeSpan.Zero));
+ var orchestrator = new OrchestratorBuilder()
+ .WithIdentity(new FakeStationIdentityProvider(new StationIdentity("s1", "GenWave", stationVoice)))
+ .WithScope(new FakeStationScopeProvider(new LibraryScope([1L])))
+ .WithCadence(cadence)
+ .WithRotation(new FakeRotationSettingsProvider(new RotationSettings()))
+ .WithMusicSelectionPolicy(new MusicSelectionPolicy(catalog, NullLogger.Instance))
+ .WithTts(tts)
+ .WithPersonaAccessor(accessor)
+ .WithLogger(NullLogger.Instance)
+ .WithRenderBudget(TimeSpan.FromSeconds(30))
+ .WithDeferralQueue(new SpeechDeferralQueue(TimeProvider.System))
+ .WithTime(TimeProvider.System)
+ .WithBoundaryBias(new FakeBoundaryBiasProvider(TimeSpan.Zero))
+ .Build()
+ .Orchestrator;
return (orchestrator, tts);
}
diff --git a/tests/GenWave.Orchestration.Tests/Specs/Story134_RotationRelaxationDiagnostics.cs b/tests/GenWave.Orchestration.Tests/Specs/Story134_RotationRelaxationDiagnostics.cs
index 94877c9c..268c1141 100644
--- a/tests/GenWave.Orchestration.Tests/Specs/Story134_RotationRelaxationDiagnostics.cs
+++ b/tests/GenWave.Orchestration.Tests/Specs/Story134_RotationRelaxationDiagnostics.cs
@@ -39,18 +39,22 @@ public static class FeatureRotationRelaxationDiagnostics
BuildOrchestrator(MediaReference? ready)
{
var catalog = new FakeMediaCatalog(ready);
- var identityProvider = new FakeStationIdentityProvider(new StationIdentity("s1", "GenWave", "default"));
- var scopeProvider = new FakeStationScopeProvider(new LibraryScope([1L]));
- var cadenceProvider = new FakeCadenceProvider(SilentCadence);
- var rotationProvider = new FakeRotationSettingsProvider(new RotationSettings());
var logger = new CapturingLogger();
- var musicSelectionPolicy = new MusicSelectionPolicy(catalog, NullLogger.Instance);
- var orchestrator = new Orchestrator(
- identityProvider, scopeProvider, cadenceProvider, rotationProvider, musicSelectionPolicy,
- new FakeTtsSegmentSource(), new FakeActivePersonaAccessor(), logger,
- new FakeRenderBudgetProvider(TimeSpan.FromSeconds(5)),
- new SpeechDeferralQueue(TimeProvider.System),
- TimeProvider.System, new FakeBoundaryBiasProvider(TimeSpan.Zero));
+ var orchestrator = new OrchestratorBuilder()
+ .WithIdentity(new FakeStationIdentityProvider(new StationIdentity("s1", "GenWave", "default")))
+ .WithScope(new FakeStationScopeProvider(new LibraryScope([1L])))
+ .WithCadence(SilentCadence)
+ .WithRotation(new FakeRotationSettingsProvider(new RotationSettings()))
+ .WithMusicSelectionPolicy(new MusicSelectionPolicy(catalog, NullLogger.Instance))
+ .WithTts(new FakeTtsSegmentSource())
+ .WithPersonaAccessor(new FakeActivePersonaAccessor())
+ .WithLogger(logger)
+ .WithRenderBudget(TimeSpan.FromSeconds(5))
+ .WithDeferralQueue(new SpeechDeferralQueue(TimeProvider.System))
+ .WithTime(TimeProvider.System)
+ .WithBoundaryBias(new FakeBoundaryBiasProvider(TimeSpan.Zero))
+ .Build()
+ .Orchestrator;
return (orchestrator, catalog, logger);
}
diff --git a/tests/GenWave.Orchestration.Tests/Specs/Story135_ArtistSeparationLive.cs b/tests/GenWave.Orchestration.Tests/Specs/Story135_ArtistSeparationLive.cs
index 3cd7248a..d6a9c04b 100644
--- a/tests/GenWave.Orchestration.Tests/Specs/Story135_ArtistSeparationLive.cs
+++ b/tests/GenWave.Orchestration.Tests/Specs/Story135_ArtistSeparationLive.cs
@@ -31,18 +31,23 @@ public static class FeatureArtistSeparationLive
BuildOrchestrator(MediaReference? ready, int artistSeparation)
{
var catalog = new FakeMediaCatalog(ready);
- var identityProvider = new FakeStationIdentityProvider(new StationIdentity("s1", "GenWave", "default"));
- var scopeProvider = new FakeStationScopeProvider(new LibraryScope([1L]));
- var cadenceProvider = new FakeCadenceProvider(SilentCadence);
var rotationProvider = new FakeRotationSettingsProvider(new RotationSettings { ArtistSeparation = artistSeparation });
var logger = new CapturingLogger();
- var musicSelectionPolicy = new MusicSelectionPolicy(catalog, NullLogger.Instance);
- var orchestrator = new Orchestrator(
- identityProvider, scopeProvider, cadenceProvider, rotationProvider, musicSelectionPolicy,
- new FakeTtsSegmentSource(), new FakeActivePersonaAccessor(), logger,
- new FakeRenderBudgetProvider(TimeSpan.FromSeconds(5)),
- new SpeechDeferralQueue(TimeProvider.System),
- TimeProvider.System, new FakeBoundaryBiasProvider(TimeSpan.Zero));
+ var orchestrator = new OrchestratorBuilder()
+ .WithIdentity(new FakeStationIdentityProvider(new StationIdentity("s1", "GenWave", "default")))
+ .WithScope(new FakeStationScopeProvider(new LibraryScope([1L])))
+ .WithCadence(SilentCadence)
+ .WithRotation(rotationProvider)
+ .WithMusicSelectionPolicy(new MusicSelectionPolicy(catalog, NullLogger.Instance))
+ .WithTts(new FakeTtsSegmentSource())
+ .WithPersonaAccessor(new FakeActivePersonaAccessor())
+ .WithLogger(logger)
+ .WithRenderBudget(TimeSpan.FromSeconds(5))
+ .WithDeferralQueue(new SpeechDeferralQueue(TimeProvider.System))
+ .WithTime(TimeProvider.System)
+ .WithBoundaryBias(new FakeBoundaryBiasProvider(TimeSpan.Zero))
+ .Build()
+ .Orchestrator;
return (orchestrator, catalog, rotationProvider, logger);
}
diff --git a/tests/GenWave.Orchestration.Tests/Specs/Story136_StationIdCadence.cs b/tests/GenWave.Orchestration.Tests/Specs/Story136_StationIdCadence.cs
index aad32415..17f898ad 100644
--- a/tests/GenWave.Orchestration.Tests/Specs/Story136_StationIdCadence.cs
+++ b/tests/GenWave.Orchestration.Tests/Specs/Story136_StationIdCadence.cs
@@ -28,24 +28,27 @@ public static class FeatureStationIdCadence
static Orchestrator BuildOrchestrator(int stationIdEveryNUnits)
{
- var identityProvider = new FakeStationIdentityProvider(new StationIdentity("s1", "GenWave", "default"));
- var scopeProvider = new FakeStationScopeProvider(new LibraryScope([1L]));
- var cadenceProvider = new FakeCadenceProvider(new CadenceConfig
- {
- LeadInBeforeEachTrack = false,
- BackAnnounceAfterEachTrack = false,
- StationIdEveryNUnits = stationIdEveryNUnits,
- });
- var rotationProvider = new FakeRotationSettingsProvider(new RotationSettings());
var catalog = new FakeMediaCatalog(MakeRef("track1"));
- var tts = new FakeTtsSegmentSource();
- var musicSelectionPolicy = new MusicSelectionPolicy(catalog, NullLogger.Instance);
- return new Orchestrator(
- identityProvider, scopeProvider, cadenceProvider, rotationProvider, musicSelectionPolicy, tts,
- new FakeActivePersonaAccessor(), NullLogger.Instance,
- new FakeRenderBudgetProvider(TimeSpan.FromSeconds(30)),
- new SpeechDeferralQueue(TimeProvider.System),
- TimeProvider.System, new FakeBoundaryBiasProvider(TimeSpan.Zero));
+ return new OrchestratorBuilder()
+ .WithIdentity(new FakeStationIdentityProvider(new StationIdentity("s1", "GenWave", "default")))
+ .WithScope(new FakeStationScopeProvider(new LibraryScope([1L])))
+ .WithCadence(new CadenceConfig
+ {
+ LeadInBeforeEachTrack = false,
+ BackAnnounceAfterEachTrack = false,
+ StationIdEveryNUnits = stationIdEveryNUnits,
+ })
+ .WithRotation(new FakeRotationSettingsProvider(new RotationSettings()))
+ .WithMusicSelectionPolicy(new MusicSelectionPolicy(catalog, NullLogger.Instance))
+ .WithTts(new FakeTtsSegmentSource())
+ .WithPersonaAccessor(new FakeActivePersonaAccessor())
+ .WithLogger(NullLogger.Instance)
+ .WithRenderBudget(TimeSpan.FromSeconds(30))
+ .WithDeferralQueue(new SpeechDeferralQueue(TimeProvider.System))
+ .WithTime(TimeProvider.System)
+ .WithBoundaryBias(new FakeBoundaryBiasProvider(TimeSpan.Zero))
+ .Build()
+ .Orchestrator;
}
static List ProduceN(Orchestrator orchestrator, int n)
diff --git a/tests/GenWave.Orchestration.Tests/Specs/Story138_StationIdentityLive.cs b/tests/GenWave.Orchestration.Tests/Specs/Story138_StationIdentityLive.cs
index 81cfe64a..4a5c46a2 100644
--- a/tests/GenWave.Orchestration.Tests/Specs/Story138_StationIdentityLive.cs
+++ b/tests/GenWave.Orchestration.Tests/Specs/Story138_StationIdentityLive.cs
@@ -38,18 +38,23 @@ public static class FeatureStationIdentityLive
BuildOrchestrator(string name)
{
var identityProvider = new FakeStationIdentityProvider(new StationIdentity("s1", name, "af_heart"));
- var scopeProvider = new FakeStationScopeProvider(new LibraryScope([1L]));
- var cadenceProvider = new FakeCadenceProvider(LeadInOnlyCadence);
- var rotationProvider = new FakeRotationSettingsProvider(new RotationSettings());
var catalog = new FakeMediaCatalog(MakeRef("track1"));
var tts = new FakeTtsSegmentSource();
- var musicSelectionPolicy = new MusicSelectionPolicy(catalog, NullLogger.Instance);
- var orchestrator = new Orchestrator(
- identityProvider, scopeProvider, cadenceProvider, rotationProvider, musicSelectionPolicy, tts,
- new FakeActivePersonaAccessor(), NullLogger.Instance,
- new FakeRenderBudgetProvider(TimeSpan.FromSeconds(30)),
- new SpeechDeferralQueue(TimeProvider.System),
- TimeProvider.System, new FakeBoundaryBiasProvider(TimeSpan.Zero));
+ var orchestrator = new OrchestratorBuilder()
+ .WithIdentity(identityProvider)
+ .WithScope(new FakeStationScopeProvider(new LibraryScope([1L])))
+ .WithCadence(LeadInOnlyCadence)
+ .WithRotation(new FakeRotationSettingsProvider(new RotationSettings()))
+ .WithMusicSelectionPolicy(new MusicSelectionPolicy(catalog, NullLogger.Instance))
+ .WithTts(tts)
+ .WithPersonaAccessor(new FakeActivePersonaAccessor())
+ .WithLogger(NullLogger.Instance)
+ .WithRenderBudget(TimeSpan.FromSeconds(30))
+ .WithDeferralQueue(new SpeechDeferralQueue(TimeProvider.System))
+ .WithTime(TimeProvider.System)
+ .WithBoundaryBias(new FakeBoundaryBiasProvider(TimeSpan.Zero))
+ .Build()
+ .Orchestrator;
return (orchestrator, tts, identityProvider);
}
diff --git a/tests/GenWave.Orchestration.Tests/Specs/Story212_EnvelopeProviderAndLadder.cs b/tests/GenWave.Orchestration.Tests/Specs/Story212_EnvelopeProviderAndLadder.cs
index 990996bd..a7f9d745 100644
--- a/tests/GenWave.Orchestration.Tests/Specs/Story212_EnvelopeProviderAndLadder.cs
+++ b/tests/GenWave.Orchestration.Tests/Specs/Story212_EnvelopeProviderAndLadder.cs
@@ -52,19 +52,23 @@ public static class FeatureEnvelopeProviderAndLadder
IPersonaPickProvider? personaPickProvider = null,
int artistSeparation = 0)
{
- var identityProvider = new FakeStationIdentityProvider(new StationIdentity("s1", "GenWave", "default"));
- var scopeProvider = new FakeStationScopeProvider(new LibraryScope([1L]));
- var cadenceProvider = new FakeCadenceProvider(SilentCadence);
- var rotationProvider = new FakeRotationSettingsProvider(new RotationSettings { ArtistSeparation = artistSeparation });
var policyLogger = new CapturingLogger();
- var musicSelectionPolicy = new MusicSelectionPolicy(
- catalog, policyLogger, new FakeEnvelopeProvider(envelope), personaPickProvider);
- var orchestrator = new Orchestrator(
- identityProvider, scopeProvider, cadenceProvider, rotationProvider, musicSelectionPolicy,
- new FakeTtsSegmentSource(), new FakeActivePersonaAccessor(), NullLogger.Instance,
- new FakeRenderBudgetProvider(TimeSpan.FromSeconds(5)),
- new SpeechDeferralQueue(TimeProvider.System),
- TimeProvider.System, new FakeBoundaryBiasProvider(TimeSpan.Zero));
+ var orchestrator = new OrchestratorBuilder()
+ .WithIdentity(new FakeStationIdentityProvider(new StationIdentity("s1", "GenWave", "default")))
+ .WithScope(new FakeStationScopeProvider(new LibraryScope([1L])))
+ .WithCadence(SilentCadence)
+ .WithRotation(new FakeRotationSettingsProvider(new RotationSettings { ArtistSeparation = artistSeparation }))
+ .WithMusicSelectionPolicy(new MusicSelectionPolicy(
+ catalog, policyLogger, new FakeEnvelopeProvider(envelope), personaPickProvider))
+ .WithTts(new FakeTtsSegmentSource())
+ .WithPersonaAccessor(new FakeActivePersonaAccessor())
+ .WithLogger(NullLogger.Instance)
+ .WithRenderBudget(TimeSpan.FromSeconds(5))
+ .WithDeferralQueue(new SpeechDeferralQueue(TimeProvider.System))
+ .WithTime(TimeProvider.System)
+ .WithBoundaryBias(new FakeBoundaryBiasProvider(TimeSpan.Zero))
+ .Build()
+ .Orchestrator;
return (orchestrator, policyLogger);
}
diff --git a/tests/GenWave.Orchestration.Tests/Specs/Story213_PersonaRanker.cs b/tests/GenWave.Orchestration.Tests/Specs/Story213_PersonaRanker.cs
index 183e780b..07428ed4 100644
--- a/tests/GenWave.Orchestration.Tests/Specs/Story213_PersonaRanker.cs
+++ b/tests/GenWave.Orchestration.Tests/Specs/Story213_PersonaRanker.cs
@@ -275,12 +275,21 @@ public static async Task OneLineCarriesAllSixAnswerFields()
var logger = new CapturingLogger();
var musicSelectionPolicy = new MusicSelectionPolicy(
catalog, logger, new FakeEnvelopeProvider(SegmentEnvelope.StationDefault), provider);
- var orchestrator = new Orchestrator(
- identityProvider, scopeProvider, cadenceProvider, rotationProvider, musicSelectionPolicy,
- new FakeTtsSegmentSource(), personaAccessor, NullLogger.Instance,
- new FakeRenderBudgetProvider(TimeSpan.FromSeconds(5)),
- new SpeechDeferralQueue(TimeProvider.System),
- TimeProvider.System, new FakeBoundaryBiasProvider(TimeSpan.Zero));
+ var orchestrator = new OrchestratorBuilder()
+ .WithIdentity(identityProvider)
+ .WithScope(scopeProvider)
+ .WithCadence(cadenceProvider)
+ .WithRotation(rotationProvider)
+ .WithMusicSelectionPolicy(musicSelectionPolicy)
+ .WithTts(new FakeTtsSegmentSource())
+ .WithPersonaAccessor(personaAccessor)
+ .WithLogger(NullLogger.Instance)
+ .WithRenderBudget(TimeSpan.FromSeconds(5))
+ .WithDeferralQueue(new SpeechDeferralQueue(TimeProvider.System))
+ .WithTime(TimeProvider.System)
+ .WithBoundaryBias(new FakeBoundaryBiasProvider(TimeSpan.Zero))
+ .Build()
+ .Orchestrator;
var item = await orchestrator.GetNextAsync(new PlayoutContext([]), CancellationToken.None);
diff --git a/tests/GenWave.Orchestration.Tests/Specs/Story227_RequestFulfillment.cs b/tests/GenWave.Orchestration.Tests/Specs/Story227_RequestFulfillment.cs
index 3c6471c7..216e5a4a 100644
--- a/tests/GenWave.Orchestration.Tests/Specs/Story227_RequestFulfillment.cs
+++ b/tests/GenWave.Orchestration.Tests/Specs/Story227_RequestFulfillment.cs
@@ -61,12 +61,21 @@ static Orchestrator BuildOrchestrator(
catalog, new CapturingLogger(),
new FakeEnvelopeProvider(envelope), personaPickProvider, requestFulfillmentSource);
- return new Orchestrator(
- identityProvider, scopeProvider, cadenceProvider, rotationProvider, musicSelectionPolicy,
- new FakeTtsSegmentSource(), new FakeActivePersonaAccessor(), new CapturingLogger(),
- new FakeRenderBudgetProvider(TimeSpan.FromSeconds(5)),
- new SpeechDeferralQueue(TimeProvider.System),
- TimeProvider.System, new FakeBoundaryBiasProvider(TimeSpan.Zero));
+ return new OrchestratorBuilder()
+ .WithIdentity(identityProvider)
+ .WithScope(scopeProvider)
+ .WithCadence(cadenceProvider)
+ .WithRotation(rotationProvider)
+ .WithMusicSelectionPolicy(musicSelectionPolicy)
+ .WithTts(new FakeTtsSegmentSource())
+ .WithPersonaAccessor(new FakeActivePersonaAccessor())
+ .WithLogger(new CapturingLogger())
+ .WithRenderBudget(TimeSpan.FromSeconds(5))
+ .WithDeferralQueue(new SpeechDeferralQueue(TimeProvider.System))
+ .WithTime(TimeProvider.System)
+ .WithBoundaryBias(new FakeBoundaryBiasProvider(TimeSpan.Zero))
+ .Build()
+ .Orchestrator;
}
public static class ScenarioALiveRequestShortCircuitsTheChain
@@ -190,19 +199,21 @@ public static async Task ExactlyOneRequestIsFulfilledAndItsTrackWinsThePick()
new FakeEnvelopeProvider(envelope),
personaPickProvider: null,
requestFulfillmentSource: fulfillmentSource);
- var orchestrator = new Orchestrator(
- new FakeStationIdentityProvider(new StationIdentity("s1", "GenWave", "default")),
- new FakeStationScopeProvider(new LibraryScope([1L])),
- new FakeCadenceProvider(SilentCadence),
- new FakeRotationSettingsProvider(new RotationSettings()),
- musicSelectionPolicy,
- new FakeTtsSegmentSource(),
- new FakeActivePersonaAccessor(),
- new CapturingLogger(),
- new FakeRenderBudgetProvider(TimeSpan.FromSeconds(5)),
- deferralQueue,
- clock,
- new FakeBoundaryBiasProvider(TimeSpan.FromMinutes(10)));
+ var orchestrator = new OrchestratorBuilder()
+ .WithIdentity(new FakeStationIdentityProvider(new StationIdentity("s1", "GenWave", "default")))
+ .WithScope(new FakeStationScopeProvider(new LibraryScope([1L])))
+ .WithCadence(SilentCadence)
+ .WithRotation(new FakeRotationSettingsProvider(new RotationSettings()))
+ .WithMusicSelectionPolicy(musicSelectionPolicy)
+ .WithTts(new FakeTtsSegmentSource())
+ .WithPersonaAccessor(new FakeActivePersonaAccessor())
+ .WithLogger(new CapturingLogger())
+ .WithRenderBudget(TimeSpan.FromSeconds(5))
+ .WithDeferralQueue(deferralQueue)
+ .WithTime(clock)
+ .WithBoundaryBias(new FakeBoundaryBiasProvider(TimeSpan.FromMinutes(10)))
+ .Build()
+ .Orchestrator;
// Act: pull the next item — the bias window is active, so pre-fix this drove the
// resample loop straight through the fulfillment rung on every attempt.
diff --git a/tests/GenWave.Orchestration.Tests/Specs/Story228_RequestShoutOut.cs b/tests/GenWave.Orchestration.Tests/Specs/Story228_RequestShoutOut.cs
index 74b117f2..ade65ca1 100644
--- a/tests/GenWave.Orchestration.Tests/Specs/Story228_RequestShoutOut.cs
+++ b/tests/GenWave.Orchestration.Tests/Specs/Story228_RequestShoutOut.cs
@@ -55,19 +55,21 @@ static Orchestrator BuildOrchestrator(CadenceConfig cadence, ITtsSegmentSource t
personaPickProvider: null,
requestFulfillmentSource: fulfillmentSource);
- return new Orchestrator(
- new FakeStationIdentityProvider(new StationIdentity("s1", "GenWave", "default")),
- new FakeStationScopeProvider(new LibraryScope([1L])),
- new FakeCadenceProvider(cadence),
- new FakeRotationSettingsProvider(new RotationSettings()),
- musicSelectionPolicy,
- ttsSource,
- new FakeActivePersonaAccessor(),
- new CapturingLogger(),
- new FakeRenderBudgetProvider(TimeSpan.FromSeconds(5)),
- new SpeechDeferralQueue(TimeProvider.System),
- TimeProvider.System,
- new FakeBoundaryBiasProvider(TimeSpan.Zero));
+ return new OrchestratorBuilder()
+ .WithIdentity(new FakeStationIdentityProvider(new StationIdentity("s1", "GenWave", "default")))
+ .WithScope(new FakeStationScopeProvider(new LibraryScope([1L])))
+ .WithCadence(cadence)
+ .WithRotation(new FakeRotationSettingsProvider(new RotationSettings()))
+ .WithMusicSelectionPolicy(musicSelectionPolicy)
+ .WithTts(ttsSource)
+ .WithPersonaAccessor(new FakeActivePersonaAccessor())
+ .WithLogger(new CapturingLogger())
+ .WithRenderBudget(TimeSpan.FromSeconds(5))
+ .WithDeferralQueue(new SpeechDeferralQueue(TimeProvider.System))
+ .WithTime(TimeProvider.System)
+ .WithBoundaryBias(new FakeBoundaryBiasProvider(TimeSpan.Zero))
+ .Build()
+ .Orchestrator;
}
static CadenceConfig CadenceWithLeadIn(bool leadInBeforeEachTrack) => new()
diff --git a/tests/GenWave.Orchestration.Tests/Specs/Story241_StationFollowsTheClock.cs b/tests/GenWave.Orchestration.Tests/Specs/Story241_StationFollowsTheClock.cs
index d1c20113..ba57732c 100644
--- a/tests/GenWave.Orchestration.Tests/Specs/Story241_StationFollowsTheClock.cs
+++ b/tests/GenWave.Orchestration.Tests/Specs/Story241_StationFollowsTheClock.cs
@@ -52,24 +52,38 @@ static ProductionChain BuildProductionChain(
var personaAccessor = new OnAirPersonaAccessor(caching, personaStore, NullLogger.Instance);
var envelopeProvider = new ScheduleEnvelopeProvider(caching, stationDefault);
- var identityProvider = new FakeStationIdentityProvider(new StationIdentity("s1", "GenWave", "default"));
- var scopeProvider = new FakeStationScopeProvider(new LibraryScope([1L]));
- var cadenceProvider = new FakeCadenceProvider(cadence ?? new CadenceConfig
- {
- LeadInBeforeEachTrack = false,
- BackAnnounceAfterEachTrack = false,
- StationIdEveryNUnits = 0,
- });
- var rotationProvider = new FakeRotationSettingsProvider(rotationSettings ?? new RotationSettings());
var logger = new CapturingLogger();
var musicSelectionPolicy = new MusicSelectionPolicy(catalog, logger, envelopeProvider, personaPickProvider);
var tts = new FakeTtsSegmentSource();
- var orchestrator = new Orchestrator(
- identityProvider, scopeProvider, cadenceProvider, rotationProvider, musicSelectionPolicy,
- tts, personaAccessor, NullLogger.Instance,
- new FakeRenderBudgetProvider(TimeSpan.FromSeconds(5)),
- new SpeechDeferralQueue(time),
- time, new FakeBoundaryBiasProvider(TimeSpan.Zero));
+ // Deliberately never calls WithScheduleResolver/WithSchedule below: the ORIGINAL wiring never
+ // gave the Orchestrator its own scheduleResolver seam either — `caching` is threaded only
+ // through personaAccessor and envelopeProvider above, exactly as the production DI shape does.
+ // The builder's own default (an empty-snapshot CachingScheduleResolver) resolves no boundary
+ // and no current show for every one of Orchestrator's own scheduleResolver reads, which is
+ // externally indistinguishable from Orchestrator's true null default here (both leave
+ // EnqueueHandoffCeremonyAsync a permanent no-op and every Show-name read null) — the only
+ // difference is one unasserted "No CachingScheduleResolver wired" WARN line the null path logs
+ // once, which this file's facts never assert on.
+ var orchestrator = new OrchestratorBuilder()
+ .WithIdentity(new FakeStationIdentityProvider(new StationIdentity("s1", "GenWave", "default")))
+ .WithScope(new FakeStationScopeProvider(new LibraryScope([1L])))
+ .WithCadence(cadence ?? new CadenceConfig
+ {
+ LeadInBeforeEachTrack = false,
+ BackAnnounceAfterEachTrack = false,
+ StationIdEveryNUnits = 0,
+ })
+ .WithRotation(new FakeRotationSettingsProvider(rotationSettings ?? new RotationSettings()))
+ .WithMusicSelectionPolicy(musicSelectionPolicy)
+ .WithTts(tts)
+ .WithPersonaAccessor(personaAccessor)
+ .WithLogger(NullLogger.Instance)
+ .WithRenderBudget(TimeSpan.FromSeconds(5))
+ .WithDeferralQueue(new SpeechDeferralQueue(time))
+ .WithTime(time)
+ .WithBoundaryBias(new FakeBoundaryBiasProvider(TimeSpan.Zero))
+ .Build()
+ .Orchestrator;
return new ProductionChain(orchestrator, personaAccessor, envelopeProvider, time, logger, tts);
}
diff --git a/tests/GenWave.Orchestration.Tests/Specs/Story243_DjsHandOffAudibly.cs b/tests/GenWave.Orchestration.Tests/Specs/Story243_DjsHandOffAudibly.cs
index 660fa266..f8847f32 100644
--- a/tests/GenWave.Orchestration.Tests/Specs/Story243_DjsHandOffAudibly.cs
+++ b/tests/GenWave.Orchestration.Tests/Specs/Story243_DjsHandOffAudibly.cs
@@ -56,15 +56,6 @@ internal static ProductionChain BuildProductionChain(
var caching = new CachingScheduleResolver(scheduleStore, resolver, new FakeScheduleSpecialStore());
var personaAccessor = new OnAirPersonaAccessor(caching, personaStore, NullLogger.Instance);
- var identityProvider = new FakeStationIdentityProvider(new StationIdentity("s1", "GenWave", "default"));
- var scopeProvider = new FakeStationScopeProvider(new LibraryScope([1L]));
- var cadenceProvider = new FakeCadenceProvider(cadence ?? new CadenceConfig
- {
- LeadInBeforeEachTrack = false,
- BackAnnounceAfterEachTrack = false,
- StationIdEveryNUnits = 0,
- });
- var rotationProvider = new FakeRotationSettingsProvider(new RotationSettings());
var logger = new CapturingLogger();
// The render double's own RenderDelay rides the SAME fake clock as the render budget below
// (STORY-442, PLAN T483) — a spec drives both off due order on chain.Time, never wall-clock
@@ -75,15 +66,29 @@ internal static ProductionChain BuildProductionChain(
var catalog = new FakeMediaCatalog(MakeTrackRef("t1"));
var musicSelectionPolicy = new MusicSelectionPolicy(catalog, NullLogger.Instance);
- var orchestrator = new Orchestrator(
- identityProvider, scopeProvider, cadenceProvider, rotationProvider, musicSelectionPolicy,
- tts, personaAccessor, logger,
- new FakeRenderBudgetProvider(renderBudget ?? TimeSpan.FromSeconds(5)),
- queue,
- time, new FakeBoundaryBiasProvider(lookahead),
- scheduleResolver: caching,
- personaStore: personaStore,
- events: events);
+ var orchestrator = new OrchestratorBuilder()
+ .WithIdentity(new FakeStationIdentityProvider(new StationIdentity("s1", "GenWave", "default")))
+ .WithScope(new FakeStationScopeProvider(new LibraryScope([1L])))
+ .WithCadence(cadence ?? new CadenceConfig
+ {
+ LeadInBeforeEachTrack = false,
+ BackAnnounceAfterEachTrack = false,
+ StationIdEveryNUnits = 0,
+ })
+ .WithRotation(new FakeRotationSettingsProvider(new RotationSettings()))
+ .WithMusicSelectionPolicy(musicSelectionPolicy)
+ .WithTts(tts)
+ .WithPersonaAccessor(personaAccessor)
+ .WithLogger(logger)
+ .WithRenderBudget(renderBudget ?? TimeSpan.FromSeconds(5))
+ .WithDeferralQueue(queue)
+ .WithTime(time)
+ .WithBoundaryBias(new FakeBoundaryBiasProvider(lookahead))
+ .WithScheduleResolver(caching)
+ .WithPersonaStore(personaStore)
+ .WithEvents(events)
+ .Build()
+ .Orchestrator;
return new ProductionChain(orchestrator, queue, time, scheduleStore, tts, events, logger);
}
diff --git a/tests/GenWave.Orchestration.Tests/Specs/Story297_ContextSegmentsAir.cs b/tests/GenWave.Orchestration.Tests/Specs/Story297_ContextSegmentsAir.cs
index db6461c0..6127e5d3 100644
--- a/tests/GenWave.Orchestration.Tests/Specs/Story297_ContextSegmentsAir.cs
+++ b/tests/GenWave.Orchestration.Tests/Specs/Story297_ContextSegmentsAir.cs
@@ -60,16 +60,23 @@ static Orchestrator BuildOrchestrator(
var catalog = new FakeMediaCatalog(MakeTrackRef("t1"));
var musicSelectionPolicy = new MusicSelectionPolicy(catalog, NullLogger.Instance);
- return new Orchestrator(
- identityProvider, scopeProvider, cadenceProvider, rotationProvider, musicSelectionPolicy, tts,
- personaAccessor ?? new FakeActivePersonaAccessor(),
- logger ?? NullLogger.Instance,
- new FakeRenderBudgetProvider(TimeSpan.FromSeconds(30)),
- queue,
- clock,
- new FakeBoundaryBiasProvider(TimeSpan.Zero),
- personaStore: personaStore,
- contextSettings: contextSettings ?? new FakeContextSettingsProvider());
+ var builder = new OrchestratorBuilder()
+ .WithIdentity(identityProvider)
+ .WithScope(scopeProvider)
+ .WithCadence(cadenceProvider)
+ .WithRotation(rotationProvider)
+ .WithMusicSelectionPolicy(musicSelectionPolicy)
+ .WithTts(tts)
+ .WithPersonaAccessor(personaAccessor ?? new FakeActivePersonaAccessor())
+ .WithLogger(logger ?? NullLogger.Instance)
+ .WithRenderBudget(TimeSpan.FromSeconds(30))
+ .WithDeferralQueue(queue)
+ .WithTime(clock)
+ .WithBoundaryBias(new FakeBoundaryBiasProvider(TimeSpan.Zero))
+ .WithContextSettings(contextSettings ?? new FakeContextSettingsProvider());
+ if (personaStore is not null)
+ builder = builder.WithPersonaStore(personaStore);
+ return builder.Build().Orchestrator;
}
// ---------------------------------------------------------------------
diff --git a/tests/GenWave.Orchestration.Tests/Specs/Story301_ClockAnchoredIdents.cs b/tests/GenWave.Orchestration.Tests/Specs/Story301_ClockAnchoredIdents.cs
index 6e8fa779..f67aaeb5 100644
--- a/tests/GenWave.Orchestration.Tests/Specs/Story301_ClockAnchoredIdents.cs
+++ b/tests/GenWave.Orchestration.Tests/Specs/Story301_ClockAnchoredIdents.cs
@@ -32,12 +32,22 @@ static Orchestrator BuildOrchestrator(
var rotationProvider = new FakeRotationSettingsProvider(new RotationSettings());
var musicSelectionPolicy = new MusicSelectionPolicy(catalog, NullLogger.Instance);
- return new Orchestrator(
- identityProvider, scopeProvider, cadenceProvider, rotationProvider, musicSelectionPolicy, tts,
- new FakeActivePersonaAccessor(), NullLogger.Instance,
- new FakeRenderBudgetProvider(TimeSpan.FromSeconds(30)),
- queue, clock, new FakeBoundaryBiasProvider(TimeSpan.Zero),
- catalog: catalog);
+ return new OrchestratorBuilder()
+ .WithIdentity(identityProvider)
+ .WithScope(scopeProvider)
+ .WithCadence(cadenceProvider)
+ .WithRotation(rotationProvider)
+ .WithMusicSelectionPolicy(musicSelectionPolicy)
+ .WithTts(tts)
+ .WithPersonaAccessor(new FakeActivePersonaAccessor())
+ .WithLogger(NullLogger.Instance)
+ .WithRenderBudget(TimeSpan.FromSeconds(30))
+ .WithDeferralQueue(queue)
+ .WithTime(clock)
+ .WithBoundaryBias(new FakeBoundaryBiasProvider(TimeSpan.Zero))
+ .WithCatalog(catalog)
+ .Build()
+ .Orchestrator;
}
// ---------------------------------------------------------------------
diff --git a/tests/GenWave.Orchestration.Tests/Specs/Story302_TimeAnnouncements.cs b/tests/GenWave.Orchestration.Tests/Specs/Story302_TimeAnnouncements.cs
index 02336336..745302dd 100644
--- a/tests/GenWave.Orchestration.Tests/Specs/Story302_TimeAnnouncements.cs
+++ b/tests/GenWave.Orchestration.Tests/Specs/Story302_TimeAnnouncements.cs
@@ -39,12 +39,21 @@ static Orchestrator BuildOrchestrator(
var catalog = new FakeMediaCatalog(MakeTrackRef("t1"));
var musicSelectionPolicy = new MusicSelectionPolicy(catalog, NullLogger.Instance);
- return new Orchestrator(
- identityProvider, scopeProvider, cadenceProvider, rotationProvider, musicSelectionPolicy, tts,
- new FakeActivePersonaAccessor(),
- logger ?? NullLogger.Instance,
- new FakeRenderBudgetProvider(TimeSpan.FromSeconds(30)),
- queue, clock, new FakeBoundaryBiasProvider(TimeSpan.Zero));
+ return new OrchestratorBuilder()
+ .WithIdentity(identityProvider)
+ .WithScope(scopeProvider)
+ .WithCadence(cadenceProvider)
+ .WithRotation(rotationProvider)
+ .WithMusicSelectionPolicy(musicSelectionPolicy)
+ .WithTts(tts)
+ .WithPersonaAccessor(new FakeActivePersonaAccessor())
+ .WithLogger(logger ?? NullLogger.Instance)
+ .WithRenderBudget(TimeSpan.FromSeconds(30))
+ .WithDeferralQueue(queue)
+ .WithTime(clock)
+ .WithBoundaryBias(new FakeBoundaryBiasProvider(TimeSpan.Zero))
+ .Build()
+ .Orchestrator;
}
// ---------------------------------------------------------------------
diff --git a/tests/GenWave.Orchestration.Tests/Specs/Story303_StraddleHandoff.cs b/tests/GenWave.Orchestration.Tests/Specs/Story303_StraddleHandoff.cs
index f7524711..3bae9fad 100644
--- a/tests/GenWave.Orchestration.Tests/Specs/Story303_StraddleHandoff.cs
+++ b/tests/GenWave.Orchestration.Tests/Specs/Story303_StraddleHandoff.cs
@@ -658,21 +658,23 @@ static FakePersonaStore ThreeDjStore()
var resolver = new ScheduleResolver(time, new FakeStationDefaultEnvelopeSource(SegmentEnvelope.StationDefault));
var caching = new CachingScheduleResolver(scheduleStore, resolver, new FakeScheduleSpecialStore());
var queue = new SpeechDeferralQueue(time);
- var orchestrator = new Orchestrator(
- new FakeStationIdentityProvider(new StationIdentity("s1", "GenWave", "default")),
- new FakeStationScopeProvider(new LibraryScope([1L])),
- new FakeCadenceProvider(CadenceOff),
- new FakeRotationSettingsProvider(new RotationSettings()),
- new MusicSelectionPolicy(catalog, NullLogger.Instance),
- new FakeTtsSegmentSource(),
- new FakeActivePersonaAccessor(),
- NullLogger.Instance,
- new FakeRenderBudgetProvider(TimeSpan.FromSeconds(30)),
- queue,
- time,
- new FakeBoundaryBiasProvider(TimeSpan.FromMinutes(10)),
- scheduleResolver: caching,
- personaStore: personaStore ?? TwoDjStore());
+ var orchestrator = new OrchestratorBuilder()
+ .WithIdentity(new FakeStationIdentityProvider(new StationIdentity("s1", "GenWave", "default")))
+ .WithScope(new FakeStationScopeProvider(new LibraryScope([1L])))
+ .WithCadence(CadenceOff)
+ .WithRotation(new FakeRotationSettingsProvider(new RotationSettings()))
+ .WithMusicSelectionPolicy(new MusicSelectionPolicy(catalog, NullLogger.Instance))
+ .WithTts(new FakeTtsSegmentSource())
+ .WithPersonaAccessor(new FakeActivePersonaAccessor())
+ .WithLogger(NullLogger.Instance)
+ .WithRenderBudget(TimeSpan.FromSeconds(30))
+ .WithDeferralQueue(queue)
+ .WithTime(time)
+ .WithBoundaryBias(new FakeBoundaryBiasProvider(TimeSpan.FromMinutes(10)))
+ .WithScheduleResolver(caching)
+ .WithPersonaStore(personaStore ?? TwoDjStore())
+ .Build()
+ .Orchestrator;
return (orchestrator, scheduleStore, time, queue);
}
diff --git a/tests/GenWave.Orchestration.Tests/Specs/Story307_CeremonyNamesTheShow.cs b/tests/GenWave.Orchestration.Tests/Specs/Story307_CeremonyNamesTheShow.cs
index 130a201f..6d067f7a 100644
--- a/tests/GenWave.Orchestration.Tests/Specs/Story307_CeremonyNamesTheShow.cs
+++ b/tests/GenWave.Orchestration.Tests/Specs/Story307_CeremonyNamesTheShow.cs
@@ -43,15 +43,6 @@ static ProductionChain BuildProductionChain(
var caching = new CachingScheduleResolver(scheduleStore, resolver, new FakeScheduleSpecialStore());
var personaAccessor = new OnAirPersonaAccessor(caching, personaStore, NullLogger.Instance);
- var identityProvider = new FakeStationIdentityProvider(new StationIdentity("s1", "GenWave", "default"));
- var scopeProvider = new FakeStationScopeProvider(new LibraryScope([1L]));
- var cadenceProvider = new FakeCadenceProvider(new CadenceConfig
- {
- LeadInBeforeEachTrack = false,
- BackAnnounceAfterEachTrack = false,
- StationIdEveryNUnits = 0,
- });
- var rotationProvider = new FakeRotationSettingsProvider(new RotationSettings());
var logger = new CapturingLogger();
var tts = new FakeTtsSegmentSource();
var events = new CapturingStationEventSink();
@@ -59,15 +50,29 @@ static ProductionChain BuildProductionChain(
var catalog = new FakeMediaCatalog(MakeTrackRef("t1"));
var musicSelectionPolicy = new MusicSelectionPolicy(catalog, NullLogger.Instance);
- var orchestrator = new Orchestrator(
- identityProvider, scopeProvider, cadenceProvider, rotationProvider, musicSelectionPolicy,
- tts, personaAccessor, logger,
- new FakeRenderBudgetProvider(TimeSpan.FromSeconds(5)),
- queue,
- time, new FakeBoundaryBiasProvider(lookahead),
- scheduleResolver: caching,
- personaStore: personaStore,
- events: events);
+ var orchestrator = new OrchestratorBuilder()
+ .WithIdentity(new FakeStationIdentityProvider(new StationIdentity("s1", "GenWave", "default")))
+ .WithScope(new FakeStationScopeProvider(new LibraryScope([1L])))
+ .WithCadence(new CadenceConfig
+ {
+ LeadInBeforeEachTrack = false,
+ BackAnnounceAfterEachTrack = false,
+ StationIdEveryNUnits = 0,
+ })
+ .WithRotation(new FakeRotationSettingsProvider(new RotationSettings()))
+ .WithMusicSelectionPolicy(musicSelectionPolicy)
+ .WithTts(tts)
+ .WithPersonaAccessor(personaAccessor)
+ .WithLogger(logger)
+ .WithRenderBudget(TimeSpan.FromSeconds(5))
+ .WithDeferralQueue(queue)
+ .WithTime(time)
+ .WithBoundaryBias(new FakeBoundaryBiasProvider(lookahead))
+ .WithScheduleResolver(caching)
+ .WithPersonaStore(personaStore)
+ .WithEvents(events)
+ .Build()
+ .Orchestrator;
return new ProductionChain(orchestrator, queue, time, scheduleStore, tts);
}
diff --git a/tests/GenWave.Orchestration.Tests/Specs/Story321_LateTimeCheckDies.cs b/tests/GenWave.Orchestration.Tests/Specs/Story321_LateTimeCheckDies.cs
index e0c16fa0..9fe4ba72 100644
--- a/tests/GenWave.Orchestration.Tests/Specs/Story321_LateTimeCheckDies.cs
+++ b/tests/GenWave.Orchestration.Tests/Specs/Story321_LateTimeCheckDies.cs
@@ -59,13 +59,22 @@ static Orchestrator BuildOrchestrator(
var catalog = new FakeMediaCatalog(MakeTrackRef("t1"));
var musicSelectionPolicy = new MusicSelectionPolicy(catalog, NullLogger.Instance);
- return new Orchestrator(
- identityProvider, scopeProvider, cadenceProvider, rotationProvider, musicSelectionPolicy, tts,
- new FakeActivePersonaAccessor(),
- logger ?? NullLogger.Instance,
- new FakeRenderBudgetProvider(TimeSpan.FromSeconds(30)),
- queue, clock, new FakeBoundaryBiasProvider(TimeSpan.Zero),
- imagingSettings: imagingSettings);
+ return new OrchestratorBuilder()
+ .WithIdentity(identityProvider)
+ .WithScope(scopeProvider)
+ .WithCadence(cadenceProvider)
+ .WithRotation(rotationProvider)
+ .WithMusicSelectionPolicy(musicSelectionPolicy)
+ .WithTts(tts)
+ .WithPersonaAccessor(new FakeActivePersonaAccessor())
+ .WithLogger(logger ?? NullLogger.Instance)
+ .WithRenderBudget(TimeSpan.FromSeconds(30))
+ .WithDeferralQueue(queue)
+ .WithTime(clock)
+ .WithBoundaryBias(new FakeBoundaryBiasProvider(TimeSpan.Zero))
+ .WithImagingSettings(imagingSettings)
+ .Build()
+ .Orchestrator;
}
// ── HAPPY PATH ──────────────────────────────────────────────────────────
diff --git a/tests/GenWave.Orchestration.Tests/Specs/Story358_AnnouncementVendAndVerbatim.cs b/tests/GenWave.Orchestration.Tests/Specs/Story358_AnnouncementVendAndVerbatim.cs
index cefc53d1..b1840c4a 100644
--- a/tests/GenWave.Orchestration.Tests/Specs/Story358_AnnouncementVendAndVerbatim.cs
+++ b/tests/GenWave.Orchestration.Tests/Specs/Story358_AnnouncementVendAndVerbatim.cs
@@ -35,16 +35,25 @@ static Orchestrator BuildOrchestrator(
var catalog = new FakeMediaCatalog(MakeRef("t1"));
var musicSelectionPolicy = new MusicSelectionPolicy(catalog, NullLogger.Instance);
- return new Orchestrator(
- identityProvider, scopeProvider, cadenceProvider, rotationProvider, musicSelectionPolicy, tts,
- new FakeActivePersonaAccessor(), logger ?? NullLogger.Instance,
- new FakeRenderBudgetProvider(TimeSpan.FromSeconds(5)),
- new SpeechDeferralQueue(TimeProvider.System),
- TimeProvider.System, new FakeBoundaryBiasProvider(TimeSpan.Zero),
- announcementSource: announcementSource,
- announcementRenderer: announcementRenderer,
- voiceLister: voiceLister,
- announcementCopyWriter: announcementCopyWriter);
+ return new OrchestratorBuilder()
+ .WithIdentity(identityProvider)
+ .WithScope(scopeProvider)
+ .WithCadence(cadenceProvider)
+ .WithRotation(rotationProvider)
+ .WithMusicSelectionPolicy(musicSelectionPolicy)
+ .WithTts(tts)
+ .WithPersonaAccessor(new FakeActivePersonaAccessor())
+ .WithLogger(logger ?? NullLogger.Instance)
+ .WithRenderBudget(TimeSpan.FromSeconds(5))
+ .WithDeferralQueue(new SpeechDeferralQueue(TimeProvider.System))
+ .WithTime(TimeProvider.System)
+ .WithBoundaryBias(new FakeBoundaryBiasProvider(TimeSpan.Zero))
+ .WithAnnouncementSource(announcementSource)
+ .WithAnnouncementRenderer(announcementRenderer)
+ .WithVoiceLister(voiceLister)
+ .WithAnnouncementCopyWriter(announcementCopyWriter)
+ .Build()
+ .Orchestrator;
}
static MediaReference MakeRef(string id) => new(
diff --git a/tests/GenWave.Orchestration.Tests/Specs/Story371_TheNudgeInTheRanker.cs b/tests/GenWave.Orchestration.Tests/Specs/Story371_TheNudgeInTheRanker.cs
index 110a97c8..ff4829c7 100644
--- a/tests/GenWave.Orchestration.Tests/Specs/Story371_TheNudgeInTheRanker.cs
+++ b/tests/GenWave.Orchestration.Tests/Specs/Story371_TheNudgeInTheRanker.cs
@@ -418,12 +418,21 @@ public sealed class ScenarioObservability
var logger = new CapturingLogger();
var musicSelectionPolicy = new MusicSelectionPolicy(
catalog, logger, new FakeEnvelopeProvider(SegmentEnvelope.StationDefault), provider);
- var orchestrator = new Orchestrator(
- identityProvider, scopeProvider, cadenceProvider, rotationProvider, musicSelectionPolicy,
- new FakeTtsSegmentSource(), personaAccessor, NullLogger.Instance,
- new FakeRenderBudgetProvider(TimeSpan.FromSeconds(5)),
- new SpeechDeferralQueue(TimeProvider.System),
- TimeProvider.System, new FakeBoundaryBiasProvider(TimeSpan.Zero));
+ var orchestrator = new OrchestratorBuilder()
+ .WithIdentity(identityProvider)
+ .WithScope(scopeProvider)
+ .WithCadence(cadenceProvider)
+ .WithRotation(rotationProvider)
+ .WithMusicSelectionPolicy(musicSelectionPolicy)
+ .WithTts(new FakeTtsSegmentSource())
+ .WithPersonaAccessor(personaAccessor)
+ .WithLogger(NullLogger.Instance)
+ .WithRenderBudget(TimeSpan.FromSeconds(5))
+ .WithDeferralQueue(new SpeechDeferralQueue(TimeProvider.System))
+ .WithTime(TimeProvider.System)
+ .WithBoundaryBias(new FakeBoundaryBiasProvider(TimeSpan.Zero))
+ .Build()
+ .Orchestrator;
var item = await orchestrator.GetNextAsync(new PlayoutContext([]), CancellationToken.None);
return (item, logger);
diff --git a/tests/GenWave.Orchestration.Tests/Specs/Story372_DeepCutsAndTheRelaxLadder.cs b/tests/GenWave.Orchestration.Tests/Specs/Story372_DeepCutsAndTheRelaxLadder.cs
index b57dbf26..1ec04ab7 100644
--- a/tests/GenWave.Orchestration.Tests/Specs/Story372_DeepCutsAndTheRelaxLadder.cs
+++ b/tests/GenWave.Orchestration.Tests/Specs/Story372_DeepCutsAndTheRelaxLadder.cs
@@ -105,12 +105,21 @@ public static class FeatureDeepCutsAndTheRelaxLadder
var policyLogger = new CapturingLogger();
var musicSelectionPolicy = new MusicSelectionPolicy(
catalog, policyLogger, new FakeEnvelopeProvider(envelope), provider);
- var orchestrator = new Orchestrator(
- identityProvider, scopeProvider, cadenceProvider, rotationProvider, musicSelectionPolicy,
- new FakeTtsSegmentSource(), personaAccessor, NullLogger.Instance,
- new FakeRenderBudgetProvider(TimeSpan.FromSeconds(5)),
- new SpeechDeferralQueue(TimeProvider.System),
- TimeProvider.System, new FakeBoundaryBiasProvider(TimeSpan.Zero));
+ var orchestrator = new OrchestratorBuilder()
+ .WithIdentity(identityProvider)
+ .WithScope(scopeProvider)
+ .WithCadence(cadenceProvider)
+ .WithRotation(rotationProvider)
+ .WithMusicSelectionPolicy(musicSelectionPolicy)
+ .WithTts(new FakeTtsSegmentSource())
+ .WithPersonaAccessor(personaAccessor)
+ .WithLogger(NullLogger.Instance)
+ .WithRenderBudget(TimeSpan.FromSeconds(5))
+ .WithDeferralQueue(new SpeechDeferralQueue(TimeProvider.System))
+ .WithTime(TimeProvider.System)
+ .WithBoundaryBias(new FakeBoundaryBiasProvider(TimeSpan.Zero))
+ .Build()
+ .Orchestrator;
return (orchestrator, policyLogger);
}
diff --git a/tests/GenWave.Orchestration.Tests/Specs/Story451_OneConstructionPath.cs b/tests/GenWave.Orchestration.Tests/Specs/Story451_OneConstructionPath.cs
new file mode 100644
index 00000000..c44581a9
--- /dev/null
+++ b/tests/GenWave.Orchestration.Tests/Specs/Story451_OneConstructionPath.cs
@@ -0,0 +1,296 @@
+// STORY-451 — One construction path (gh-#401 · SPEC F184 · PLAN T510–T514)
+//
+// BDD specification — xUnit. AC1–AC3 drive OrchestratorBuilder (tests/GenWave.TestSupport); AC6/AC7 drive AddOrchestration through a
+// ServiceCollection; AC9 is the recount. AC4/AC5/AC8 are pins in Architecture.Tests (Story451_ConstructionPins).
+//
+// AC1/AC2 went green at T511, AC3/AC9 at T512. AC6/AC7 land at T514, below: they drive
+// AddGenWaveOrchestration through a real ServiceCollection rather than OrchestratorBuilder, so the
+// optional-seam resolution the extension method itself owns (not the builder's own With* defaults)
+// is what each fact actually exercises.
+
+using System.Reflection;
+using System.Text.RegularExpressions;
+using GenWave.Abstractions.Playout;
+using GenWave.Core.Abstractions;
+using GenWave.Core.Domain;
+using GenWave.Orchestration.Tests.Fakes;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Logging;
+using Microsoft.Extensions.Logging.Abstractions;
+using Microsoft.Extensions.Time.Testing;
+
+namespace GenWave.Orchestration.Tests.Specs;
+
+public static class FeatureOneConstructionPath
+{
+ // ---------------------------------------------------------------------
+ // Helpers
+ // ---------------------------------------------------------------------
+
+ ///
+ /// Every seam AddGenWaveOrchestration itself does not supply (AC6/AC7's own arrange) — the 12
+ /// required Orchestrator constructor params it resolves via GetRequiredService, plus
+ /// IMediaCatalog/ILogger<MusicSelectionPolicy> so its own TryAddSingleton<MusicSelectionPolicy>
+ /// can activate. TimeProvider/SpeechDeferralQueue/MusicSelectionPolicy stay unregistered here on
+ /// purpose — AddGenWaveOrchestration TryAdds all three itself.
+ ///
+ static ServiceCollection RequiredSeamServices()
+ {
+ var services = new ServiceCollection();
+
+ services.AddSingleton(new FakeStationIdentityProvider(new StationIdentity("s1", "GenWave", "default")));
+ services.AddSingleton(new FakeStationScopeProvider(new LibraryScope([1L])));
+ services.AddSingleton(new FakeCadenceProvider(new CadenceConfig
+ {
+ LeadInBeforeEachTrack = false,
+ BackAnnounceAfterEachTrack = false,
+ StationIdEveryNUnits = 0,
+ }));
+ services.AddSingleton(new FakeRotationSettingsProvider(new RotationSettings()));
+ services.AddSingleton(new FakeTtsSegmentSource());
+ services.AddSingleton(new FakeActivePersonaAccessor());
+ services.AddSingleton(new FakeRenderBudgetProvider(TimeSpan.FromSeconds(30)));
+ services.AddSingleton(new FakeBoundaryBiasProvider(TimeSpan.Zero));
+ services.AddSingleton>(NullLogger.Instance);
+ services.AddSingleton>(NullLogger.Instance);
+ services.AddSingleton(new FakeMediaCatalog(TestData.MakeTrackRef("t1")));
+
+ return services;
+ }
+
+ // ---------------------------------------------------------------------
+ // HAPPY PATH
+ // ---------------------------------------------------------------------
+
+ public sealed class ScenarioTheBuilderWithDefaults
+ {
+ readonly OrchestratorChain chain;
+
+ // Given: new OrchestratorBuilder().Build()
+ public ScenarioTheBuilderWithDefaults()
+ {
+ chain = new OrchestratorBuilder().Build();
+ }
+
+ /// AC1 — the chain record carries the Orchestrator
+ [Fact]
+ public void ExposesTheOrchestrator() => Assert.NotNull(chain.Orchestrator);
+
+ /// AC1 — the deferral queue is the one the Orchestrator was built with
+ [Fact]
+ public void ExposesTheDeferralQueue() => Assert.NotNull(chain.Queue);
+
+ /// AC1 — a FakeTimeProvider drives the chain by default
+ [Fact]
+ public void ExposesTheFakeClock() => Assert.IsType(chain.Time);
+
+ /// AC1 — the tts fake is reachable for assertions by default
+ [Fact]
+ public void ExposesTheTtsFake() => Assert.IsType(chain.Tts);
+
+ /// AC1 — the capturing event sink is reachable by default
+ [Fact]
+ public void ExposesTheEventSink() => Assert.IsType(chain.Events);
+
+ /// AC1 — the fake catalog is reachable by default
+ [Fact]
+ public void ExposesTheCatalog() => Assert.IsType(chain.Catalog);
+
+ ///
+ /// AC1 — the estimator slot is reachable. Its default is deliberately null, not a fake — see
+ /// 's own remarks: the one estimator double this project owns
+ /// () is not behaviorally equivalent to
+ /// Orchestrator's own internal fallback, so defaulting to it here would silently change
+ /// boundary-fit behavior for every spec that never overrides this seam.
+ ///
+ [Fact]
+ public void ExposesThePatterEstimator() => Assert.Null(chain.PatterEstimator);
+ }
+
+ public sealed class ScenarioAnOverriddenAdSpotVend : IAsyncLifetime
+ {
+ readonly FakeAdSpotVend vend = new()
+ {
+ Answer = new MediaItem(
+ "spot-1", "/authored/ads/spot-1.wav", "Spot spot-1",
+ new Loudness(-14.0, -1.0, true),
+ SegmentKind: SegmentKind.Ad),
+ };
+
+ // Given: builder.WithAdSpotVend(fake) and ad cadence every unit; two units served
+ public async Task InitializeAsync()
+ {
+ var chain = new OrchestratorBuilder()
+ .WithAdCadence(new FakeAdCadenceProvider(1))
+ .WithAdSpotVend(vend)
+ .Build();
+
+ var ctx = new PlayoutContext([]);
+ await chain.Orchestrator.GetNextAsync(ctx, CancellationToken.None); // unit 0 — no trigger
+ await chain.Orchestrator.GetNextAsync(ctx, CancellationToken.None); // unit 1 — fires
+ }
+
+ public Task DisposeAsync() => Task.CompletedTask;
+
+ /// AC2 — the seam override reaches the Orchestrator
+ [Fact]
+ public void TheFakeReceivedOneVend() => Assert.Equal(1, vend.CallCount);
+ }
+
+ public sealed class ScenarioTheHarnessRidesTheBuilder
+ {
+ // Given: ProductionChainHarness rewritten as a builder call — reruns the exact two-DJ
+ // noon-boundary shape Story243_DjsHandOffAudibly.cs/Story307_CeremonyNamesTheShow.cs each
+ // exercise through ProductionChainHarness.BuildProductionChain directly (rather than their own
+ // inline copy), pinning the resulting unit sequence so a future change to the builder's
+ // wiring order trips this fact first.
+
+ static readonly DayOfWeek Monday = new DateTimeOffset(2026, 3, 2, 0, 0, 0, TimeSpan.Zero).DayOfWeek;
+ static readonly DateTimeOffset JustBeforeNoon = new(2026, 3, 2, 11, 55, 0, TimeSpan.Zero);
+
+ static ScheduleWeekSnapshot TwoDjSchedule() => new(
+ [
+ new ScheduleSegment(Id: 1, Day: Monday, StartMinute: 0, EndMinute: 720, PersonaId: 10, Genres: null, EnergyMin: null, EnergyMax: null),
+ new ScheduleSegment(Id: 2, Day: Monday, StartMinute: 720, EndMinute: 1440, PersonaId: 20, Genres: null, EnergyMin: null, EnergyMax: null),
+ ]);
+
+ static FakePersonaStore TwoDjStore()
+ {
+ var store = new FakePersonaStore();
+ store.Add(TestData.MakePersona(10, "DJ Alpha", "af_alpha"));
+ store.Add(TestData.MakePersona(20, "DJ Beta", "af_beta"));
+ return store;
+ }
+
+ static bool IsSignOff(MediaItem item) =>
+ item.MediaId.StartsWith("tts:signoff", StringComparison.OrdinalIgnoreCase);
+
+ static bool IsSignOn(MediaItem item) =>
+ item.MediaId.StartsWith("tts:signon", StringComparison.OrdinalIgnoreCase);
+
+ static string Classify(MediaItem item) =>
+ IsSignOff(item) ? "SignOff" : IsSignOn(item) ? "SignOn" : "Music";
+
+ /// AC3 — the four harness specs' unit order is unchanged
+ [Fact]
+ public async Task ProducesTheSameUnitOrderAsBefore()
+ {
+ var chain = ProductionChainHarness.BuildProductionChain(
+ TwoDjStore(), TwoDjSchedule(), JustBeforeNoon, TimeSpan.FromMinutes(10));
+
+ var kinds = new List();
+ for (var i = 0; i < 12; i++)
+ {
+ var item = await chain.Orchestrator.GetNextAsync(new PlayoutContext([]), CancellationToken.None)
+ ?? throw new InvalidOperationException("Expected GetNextAsync to produce a media item.");
+ kinds.Add(Classify(item));
+ chain.Time.Advance(TimeSpan.FromSeconds(30));
+ }
+
+ Assert.Equal(
+ [
+ "Music", "Music", "Music", "Music", "Music", "Music",
+ "Music", "SignOff", "SignOn", "Music", "Music", "Music",
+ ],
+ kinds);
+ }
+ }
+
+ public sealed class ScenarioAddOrchestrationWithNoOptionalSeams : IAsyncLifetime
+ {
+ readonly List served = [];
+ INextItemProvider? orchestrator;
+
+ // Given: ServiceCollection + AddGenWaveOrchestration, no IAdSpotVend registered — the ad
+ // cadence still fires every unit, so a real vend WOULD be reachable if one were wired.
+ public async Task InitializeAsync()
+ {
+ var services = RequiredSeamServices();
+ services.AddSingleton(new FakeAdCadenceProvider(1));
+ services.AddGenWaveOrchestration();
+
+ var provider = services.BuildServiceProvider().GetRequiredService();
+ orchestrator = provider;
+
+ var ctx = new PlayoutContext([]);
+ served.Add(await provider.GetNextAsync(ctx, CancellationToken.None)); // unit 0 — no trigger
+ served.Add(await provider.GetNextAsync(ctx, CancellationToken.None)); // unit 1 — fires
+ }
+
+ public Task DisposeAsync() => Task.CompletedTask;
+
+ /// AC6 — resolution succeeds without the optional seam
+ [Fact]
+ public void ResolvesTheNextItemProvider() => Assert.NotNull(orchestrator);
+
+ /// AC6 — the Orchestrator's vend is NoOpAdSpotVend: no ad ever airs
+ [Fact]
+ public void BuiltWithTheNoOpVend() => Assert.DoesNotContain(served, item => item?.SegmentKind == SegmentKind.Ad);
+ }
+
+ public sealed class ScenarioAddOrchestrationWithARegisteredSeam : IAsyncLifetime
+ {
+ readonly FakeAdSpotVend vend = new()
+ {
+ Answer = new MediaItem(
+ "spot-1", "/authored/ads/spot-1.wav", "Spot spot-1",
+ new Loudness(-14.0, -1.0, true),
+ SegmentKind: SegmentKind.Ad),
+ };
+
+ // Given: a fake IAdSpotVend registered before AddGenWaveOrchestration — AddGenWaveOrchestration's
+ // own TryAddSingleton(NoOpAdSpotVend.Instance) must lose to it.
+ public async Task InitializeAsync()
+ {
+ var services = RequiredSeamServices();
+ services.AddSingleton(new FakeAdCadenceProvider(1));
+ services.AddSingleton(vend);
+ services.AddGenWaveOrchestration();
+
+ var orchestrator = services.BuildServiceProvider().GetRequiredService();
+
+ var ctx = new PlayoutContext([]);
+ await orchestrator.GetNextAsync(ctx, CancellationToken.None); // unit 0 — no trigger
+ await orchestrator.GetNextAsync(ctx, CancellationToken.None); // unit 1 — fires
+ }
+
+ public Task DisposeAsync() => Task.CompletedTask;
+
+ /// AC7 — the registered fake wins over the NoOp
+ [Fact]
+ public void BuiltWithTheRegisteredVend() => Assert.Equal(1, vend.CallCount);
+ }
+
+ public sealed class ScenarioTheRecountAfterTheMove
+ {
+ // Given: the Orchestration.Tests assembly reflected — every [Fact]/[Theory]-decorated METHOD
+ // (not each Theory row) counted once, mirroring how the pre-move baseline was measured.
+
+ const int FactMethodCount = 556; // reflected [Fact]/[Theory] methods, measured before T512 moved any site (the move adds and removes no attribute, so pre = post); the runner reports 562 cases = 556 + theory rows
+
+ static IReadOnlyList<(MethodInfo Method, FactAttribute Attribute)> ReflectFactMethods() =>
+ typeof(FeatureOneConstructionPath).Assembly
+ .GetTypes()
+ .SelectMany(t => t.GetMethods(
+ BindingFlags.Public | BindingFlags.Instance | BindingFlags.Static | BindingFlags.DeclaredOnly))
+ .SelectMany(m => m.GetCustomAttributes().OfType()
+ .Select(a => (Method: m, Attribute: a)))
+ .ToList();
+
+ /// AC9 — no fact was lost in the migration
+ [Fact]
+ public void KeepsThePreMoveFactCount() =>
+ Assert.Equal(FactMethodCount, ReflectFactMethods().Count);
+
+ /// AC9 — no fact is skipped outside the STORY-451…459 pendings
+ [Fact]
+ public void SkipsNothingOutsideTheEpicPendings()
+ {
+ var skipsOutsideTheEpic = ReflectFactMethods()
+ .Select(x => x.Attribute.Skip)
+ .Where(skip => skip is not null && !Regex.IsMatch(skip, "STORY-45[1-9]"));
+
+ Assert.Empty(skipsOutsideTheEpic);
+ }
+ }
+}
diff --git a/tests/GenWave.Orchestration.Tests/Specs/Story452_BreakCharacterisationReplay.cs b/tests/GenWave.Orchestration.Tests/Specs/Story452_BreakCharacterisationReplay.cs
new file mode 100644
index 00000000..67372238
--- /dev/null
+++ b/tests/GenWave.Orchestration.Tests/Specs/Story452_BreakCharacterisationReplay.cs
@@ -0,0 +1,299 @@
+// STORY-452 — The break characterisation replay (gh-#401 · SPEC F185 · PLAN T515, T522)
+//
+// BDD specification — xUnit. FROZEN for gh-#401 (PLAN T515): AC1–AC4 pin four tables — buffer
+// order by media id, DjName per item, event kinds in order, and WARN+ messages — each OBSERVED
+// from ONE deterministic run of the F185.1 script through OrchestratorBuilder against the real,
+// unsplit Orchestrator (RunScriptAsync below). Once T515 lands, no PR inside gh-#401 may change an
+// assertion in THIS file or in Story452_PinnedTables.cs. The ONLY exception is PLAN T522 (PR-3),
+// which may un-skip AC6 (MatchThePinnedTraceTable) and add BreakPlan.ToTrace() once BreakPlan
+// exists. Any other change to this pair must name the behaviour change in its own PR title and in
+// SPEC F186.
+//
+// One script, served unit by unit on a shared FakeTimeProvider: back-announce + lead-in on,
+// station-id cadence hit, ad cadence hit, two pending owner announcements, a vended crosstalk
+// exchange, a due context segment, time/date at on-time/late/expired, a show boundary with a
+// straddling track (sign-off drains, sign-on holds), a ceremony-only finale, one over-budget TTS
+// render (dropped), one null TTS render (dropped).
+//
+// Disclosed deviations from the SPEC F185.1 "six music units" headline:
+// - Nine units, not six: the over-budget render and the null render each get their own isolated
+// unit (every other cadence off) so neither drop is masked by an unrelated element sharing the
+// pull; the ceremony-only finale is its own unit by construction (SPEC F124/gh-#300).
+// - The crosstalk asset lives at a FIXED path this file writes before the run, not STORY-329's
+// Path.GetTempFileName() idiom — a random name would leak into the crosstalk item's MediaId
+// (tts:crosstalk:{asset filename}), breaking AC1's determinism across runs.
+// - The "over-budget render" beat is a sign-off, not an owner announcement: the announcement
+// renderer double completes synchronously with no delay mechanism to race a budget against,
+// while FakeTtsSegmentSource.RenderDelay does.
+
+using System.Runtime.CompilerServices;
+using GenWave.Core.Abstractions;
+using GenWave.Core.Domain;
+using GenWave.Orchestration.Tests.Fakes;
+using Microsoft.Extensions.Logging.Abstractions;
+using Microsoft.Extensions.Time.Testing;
+
+namespace GenWave.Orchestration.Tests.Specs;
+
+public static class FeatureBreakCharacterisationReplay
+{
+ const long HostPersonaId = 10;
+ const long NeighborPersonaId = 20;
+
+ /// The one handoff pairing every ceremony beat (O5, O6, O9) hands off across.
+ static readonly HandoffContext Handoff = new("af_flip", "Nova", "Milo");
+
+ /// Everything AC1–AC4 assert on, captured from one deterministic run of the script.
+ sealed record ReplayResult(
+ IReadOnlyList BufferedMediaIds,
+ IReadOnlyList DjNames,
+ IReadOnlyList EventKinds,
+ IReadOnlyList Warnings);
+
+ ///
+ /// Builds the F185.1 script through against the real, unsplit
+ /// and serves it unit by unit — see this file's header for the
+ /// nine-vs-six-units disclosure.
+ ///
+ static async Task RunScriptAsync()
+ {
+ var clock = new FakeTimeProvider(new DateTimeOffset(2026, 3, 2, 10, 0, 0, TimeSpan.Zero));
+ var cadence = new FakeCadenceProvider(new CadenceConfig
+ {
+ LeadInBeforeEachTrack = true,
+ BackAnnounceAfterEachTrack = true,
+ StationIdEveryNUnits = 3,
+ });
+ var adCadence = new FakeAdCadenceProvider(2);
+ var adSpotVend = new FakeAdSpotVend();
+ var announcementSource = new FakeAnnouncementSource();
+ var announcementRenderer = new FakeVerbatimSegmentRenderer();
+ var fakeTts = new FakeTtsSegmentSource { TimeProvider = clock };
+ var events = new CapturingStationEventSink();
+ var logger = new CapturingLogger();
+
+ var personaStore = new FakePersonaStore();
+ personaStore.Add(TestData.MakePersona(HostPersonaId, "Nova", "af_nova"));
+ personaStore.Add(TestData.MakePersona(NeighborPersonaId, "Milo", "af_milo"));
+
+ var week = new ScheduleWeekSnapshot([
+ new ScheduleSegment(1, DayOfWeek.Monday, 0, 720, HostPersonaId, null, null, null,
+ Show: new ShowSummary(5, "Morning Mix", null, null) { Slug = "morning-mix" }, ShowId: 5),
+ new ScheduleSegment(2, DayOfWeek.Monday, 720, 1440, NeighborPersonaId, null, null, null),
+ ]);
+
+ var crosstalkScope = new FakeCrosstalkScopeProvider(["morning-mix"], everyNthAiring: 1);
+ var crosstalkPlanner = new CrosstalkPlanner(personaStore, crosstalkScope, NullLogger.Instance);
+ var crosstalkAssetPath = Path.Combine(Path.GetTempPath(), "genwave-story452-crosstalk.wav");
+ File.WriteAllBytes(crosstalkAssetPath, [0]);
+
+ var contextSettings = new FakeContextSettingsProvider();
+ var imaging = new FakeStationImagingSettingsProvider { Current = new StationImagingSettings(false, false, 180) };
+
+ var catalog = FakeMediaCatalog.WithPool([
+ TestData.MakeTrackRef("t1"), TestData.MakeTrackRef("t2"), TestData.MakeTrackRef("t3"),
+ TestData.MakeTrackRef("t4"), TestData.MakeTrackRef("t5"),
+ TestData.MakeTrackRef("t6-straddle") with { DurationMs = 540_000 },
+ ]);
+
+ var chain = new OrchestratorBuilder()
+ .WithTime(clock)
+ .WithSchedule(week)
+ .WithPersonaStore(personaStore)
+ .WithCadence(cadence)
+ .WithAdCadence(adCadence)
+ .WithAdSpotVend(adSpotVend)
+ .WithAnnouncementSource(announcementSource)
+ .WithAnnouncementRenderer(announcementRenderer)
+ .WithCrosstalkPlanner(crosstalkPlanner)
+ .WithContextSettings(contextSettings)
+ .WithImagingSettings(imaging)
+ .WithCatalog(catalog)
+ .WithTts(fakeTts)
+ .WithEvents(events)
+ .WithLogger(logger)
+ .WithLookahead(TimeSpan.FromMinutes(10))
+ .Build();
+
+ var orchestrator = chain.Orchestrator;
+ var queue = chain.Queue;
+
+ async Task PullOneAsync(PlayoutContext? ctx = null)
+ {
+ var item = await orchestrator.GetNextAsync(ctx ?? new PlayoutContext([]), CancellationToken.None);
+ Assert.NotNull(item);
+ return item;
+ }
+
+ async Task> PullManyAsync(int count, PlayoutContext? ctx = null)
+ {
+ var items = new List();
+ for (var i = 0; i < count; i++) items.Add(await PullOneAsync(ctx));
+ return items;
+ }
+
+ // Races the sign-off render (RenderDelay=10s) against the default 5s render budget by
+ // advancing the SAME clock the budget's own timer and the render's own Task.Delay share
+ // (mirrors STORY-243/STORY-442's own racing idiom).
+ async Task PullRacingBudgetAsync()
+ {
+ var task = orchestrator.GetNextAsync(new PlayoutContext([]), CancellationToken.None);
+ for (var i = 0; i < 8 && !task.IsCompleted; i++)
+ {
+ clock.Advance(TimeSpan.FromSeconds(1));
+ await Task.Yield();
+ }
+
+ var item = await task;
+ Assert.NotNull(item);
+ return item;
+ }
+
+ // O1 — two pending owner announcements drain (SPEC F144.1) alongside the opening lead-in;
+ // no back-announce yet (no previous track).
+ announcementSource.Pending.Enqueue(new AnnouncementItem(9001, "Happy birthday to Sam!", Verbatim: true, RequestedVoice: null));
+ announcementSource.Pending.Enqueue(new AnnouncementItem(9002, "Pledge drive ends Friday.", Verbatim: true, RequestedVoice: null));
+ var o1 = await PullManyAsync(4);
+
+ // O2 — a stocked crosstalk exchange vends for the enabled "morning-mix" show (SPEC F127.1).
+ crosstalkPlanner.Stock(new StockedCrosstalkExchange("morning-mix",
+ new CrosstalkCast(HostPersonaId, NeighborPersonaId), crosstalkAssetPath, new Loudness(-16.0, -1.0, true), Cue: null, DurationMs: 6_000));
+ var o2 = await PullManyAsync(4);
+
+ // O3 — the ad cadence (every 2 units) hits (SPEC F158.1).
+ adSpotVend.Answer = new MediaItem("ad-spot-1", "/ads/spot1.mp3", "Ad Spot", new Loudness(-16.0, -1.0, true));
+ var o3 = await PullManyAsync(4);
+
+ // O4 — the station-id cadence (every 3 units) hits alongside a due, fresh context segment
+ // (SPEC F107.3).
+ queue.Enqueue(SpeechDeferralKind.Context, "test: weather due", due: clock.GetUtcNow(), discriminator: "weather",
+ context: new ContextSegmentFacts("Sunny and mild, 70F.", clock.GetUtcNow().AddMinutes(10)));
+ var o4 = await PullManyAsync(5);
+
+ // O5 — an isolated sign-off whose render exceeds the render budget and is dropped (SPEC
+ // F92.4). Every other cadence is off for this one unit so the sign-off is the ONLY pending
+ // render; the ad cadence is also permanently zeroed here — its one hit at O3 already proved
+ // the element, and leaving it on would refire later and collide with O7/O8's own design.
+ cadence.Cadence = new CadenceConfig { LeadInBeforeEachTrack = false, BackAnnounceAfterEachTrack = false, StationIdEveryNUnits = 0 };
+ adCadence.EveryNUnits = 0;
+ fakeTts.RenderDelay = TimeSpan.FromSeconds(10);
+ queue.Enqueue(SpeechDeferralKind.SignOff, "test: over-budget signoff", due: clock.GetUtcNow(), handoff: Handoff);
+ var o5 = new List { await PullRacingBudgetAsync() };
+ fakeTts.RenderDelay = null;
+
+ // O6 — a show-boundary straddle: the sign-off drains now (due in 6 minutes, inside the
+ // 10-minute lookahead), the paired sign-on holds, and the boundary-crossing 9-minute track
+ // straddles it (SPEC F111/F124). Lead-in/back-announce return; station-id stays off for the
+ // rest of the script — its one hit at O4 already proved the element.
+ cadence.Cadence = new CadenceConfig { LeadInBeforeEachTrack = true, BackAnnounceAfterEachTrack = true, StationIdEveryNUnits = 0 };
+ var signOffDue = clock.GetUtcNow() + TimeSpan.FromMinutes(6);
+ queue.Enqueue(SpeechDeferralKind.SignOff, "test: straddle signoff", due: signOffDue, handoff: Handoff);
+ queue.Enqueue(SpeechDeferralKind.SignOn, "test: straddle signon", due: signOffDue + TimeSpan.FromSeconds(15), handoff: Handoff);
+ var o6 = await PullManyAsync(4);
+
+ // O7 — the held sign-on drains once its due time passes, alongside a context segment whose
+ // render returns null and is dropped (SPEC F107.6).
+ clock.Advance(TimeSpan.FromMinutes(7));
+ fakeTts.ShouldReturnNull = request => request.Kind == SegmentKind.ContextSegment;
+ queue.Enqueue(SpeechDeferralKind.Context, "test: null-render context", due: clock.GetUtcNow(), discriminator: "traffic",
+ context: new ContextSegmentFacts("Heavy traffic downtown.", clock.GetUtcNow().AddMinutes(10)));
+ var o7 = await PullManyAsync(4);
+ fakeTts.ShouldReturnNull = null;
+
+ // O8 — three time/date deferrals with distinct discriminators (so none supersedes another —
+ // SpeechDeferralQueue's own (Kind, Discriminator) key): on-time, late (past the 90s honesty
+ // threshold but inside the 180s budget), and expired (past the budget — dropped undrained,
+ // SPEC F141.2).
+ var timeDateNow = clock.GetUtcNow();
+ queue.Enqueue(SpeechDeferralKind.TimeDate, "test: on-time", due: timeDateNow, discriminator: "ontime");
+ queue.Enqueue(SpeechDeferralKind.TimeDate, "test: late", due: timeDateNow - TimeSpan.FromSeconds(120), discriminator: "late");
+ queue.Enqueue(SpeechDeferralKind.TimeDate, "test: expired", due: timeDateNow - TimeSpan.FromSeconds(300), discriminator: "expired");
+ var o8 = await PullManyAsync(5);
+
+ // O9 — a ceremony-only finale (SPEC F124/gh-#300): with lead-in/back-announce off (matching
+ // gh-#300's own arrange — a pending back-announce would otherwise air INSTEAD of the
+ // ceremony), the queued-ahead tail already crosses the 45s-out boundary, so the decline
+ // check fires before any music is planned: a single spoken segment (sign-off; the paired
+ // sign-on holds), drawing nothing from the catalog.
+ cadence.Cadence = new CadenceConfig { LeadInBeforeEachTrack = false, BackAnnounceAfterEachTrack = false, StationIdEveryNUnits = 0 };
+ queue.Enqueue(SpeechDeferralKind.SignOff, "test: decline signoff", due: clock.GetUtcNow() + TimeSpan.FromSeconds(30), handoff: Handoff);
+ queue.Enqueue(SpeechDeferralKind.SignOn, "test: decline signon", due: clock.GetUtcNow() + TimeSpan.FromSeconds(45), handoff: Handoff);
+ var o9 = await PullManyAsync(1, new PlayoutContext([], QueuedAheadMs: 200_000));
+
+ List[] units = [o1, o2, o3, o4, o5, o6, o7, o8, o9];
+ return new ReplayResult(
+ [.. units.SelectMany(unit => unit.Select(item => item.MediaId))],
+ [.. units.SelectMany(unit => unit.Select(item => item.DjName ?? "(none)"))],
+ [.. events.Events.Select(evt => evt.GetType().Name)],
+ [.. logger.Warnings]);
+ }
+
+ public sealed class ScenarioTheScriptServedEndToEnd
+ {
+ // Given: the F185.1 script through the builder, every unit served (RunScriptAsync above)
+
+ /// AC1 — media ids per unit equal the pinned table.
+ [Fact]
+ public async Task BuffersTheUnitsInThePinnedOrder()
+ {
+ var result = await RunScriptAsync();
+
+ Assert.Equal(Story452PinnedTables.BufferedMediaIds, result.BufferedMediaIds);
+ }
+
+ /// AC2 — DjName per buffered item equals the pinned table.
+ [Fact]
+ public async Task StampsThePinnedDjNames()
+ {
+ var result = await RunScriptAsync();
+
+ Assert.Equal(Story452PinnedTables.DjNames, result.DjNames);
+ }
+
+ /// AC3 — event kinds in order equal the pinned list.
+ [Fact]
+ public async Task PublishesThePinnedEventKinds()
+ {
+ var result = await RunScriptAsync();
+
+ Assert.Equal(Story452PinnedTables.EventKinds, result.EventKinds);
+ }
+
+ /// AC4 — WARN+ messages equal the pinned list.
+ [Fact]
+ public async Task LogsThePinnedWarnings()
+ {
+ var result = await RunScriptAsync();
+
+ Assert.Equal(Story452PinnedTables.Warnings, result.Warnings);
+ }
+ }
+
+ public sealed class ScenarioTheFileHeader
+ {
+ // Given: this file's own leading comment
+
+ /// AC5 — the header states the freeze and names gh-#401 and the one PR (T522) that may add an assertion.
+ [Fact]
+ public void DeclaresTheFreeze()
+ {
+ var header = ReadOwnSource();
+
+ Assert.True(header.Contains("FROZEN", StringComparison.Ordinal)
+ && header.Contains("gh-#401", StringComparison.Ordinal) && header.Contains("T522", StringComparison.Ordinal));
+ }
+
+ static string ReadOwnSource([CallerFilePath] string path = "") => File.ReadAllText(path);
+ }
+
+ public sealed class ScenarioThePlanTraces
+ {
+ const string Pending = "pending: T522 — BreakPlan.ToTrace() per unit (STORY-452)";
+
+ // Given: the same run once BreakPlan exists (T522)
+
+ /// AC6 — ToTrace() per unit equals the pinned table.
+ [Fact(Skip = Pending)]
+ public void MatchThePinnedTraceTable() => throw new NotImplementedException(Pending);
+ }
+}
diff --git a/tests/GenWave.Orchestration.Tests/Specs/Story452_PinnedTables.cs b/tests/GenWave.Orchestration.Tests/Specs/Story452_PinnedTables.cs
new file mode 100644
index 00000000..48400fa0
--- /dev/null
+++ b/tests/GenWave.Orchestration.Tests/Specs/Story452_PinnedTables.cs
@@ -0,0 +1,80 @@
+// STORY-452 — pinned trace tables for the break characterisation replay (gh-#401 · PLAN T515, T522)
+//
+// FROZEN alongside Story452_BreakCharacterisationReplay.cs — see that file's header for the exact
+// freeze rule (only PLAN T522 may touch either file, and only to add the AC6 trace table). These
+// four tables are OBSERVED output from one deterministic run of that file's RunScriptAsync(), never
+// hand-designed values.
+
+namespace GenWave.Orchestration.Tests.Specs;
+
+static class Story452PinnedTables
+{
+ public static readonly string[] BufferedMediaIds =
+ [
+ // O1 — two pending owner announcements + the opening lead-in (no back-announce yet)
+ "tts:announcement:9001:tts:1",
+ "tts:announcement:9002:tts:2",
+ "tts:leadin-1",
+ "t1",
+ // O2 — the vended crosstalk exchange
+ "tts:backannounce-2",
+ "tts:crosstalk:genwave-story452-crosstalk",
+ "tts:leadin-3",
+ "t2",
+ // O3 — the ad cadence hit
+ "tts:backannounce-4",
+ "ad-spot-1",
+ "tts:leadin-5",
+ "t3",
+ // O4 — the station-id cadence hit + a due context segment
+ "tts:backannounce-6",
+ "tts:stationid-7",
+ "tts:contextsegment-8",
+ "tts:leadin-9",
+ "t4",
+ // O5 — the sign-off render exceeds the render budget and is dropped; the fallback track airs
+ "t5",
+ // O6 — the show-boundary straddle: sign-off drains, sign-on holds, the track crosses the boundary
+ "tts:backannounce-11",
+ "tts:signoff-12",
+ "tts:leadin-13",
+ "t6-straddle",
+ // O7 — the held sign-on drains; the context segment's render returns null and is dropped
+ "tts:backannounce-14",
+ "tts:signon-15",
+ "tts:leadin-17",
+ "t5",
+ // O8 — on-time and late time/date deferrals render; the expired one is dropped undrained
+ // (on-time and late are indistinguishable here: only their count and the expired drop are pinned,
+ // so row order never proves the honesty threshold)
+ "tts:backannounce-18",
+ "tts:timedate-19",
+ "tts:timedate-20",
+ "tts:leadin-21",
+ "t6-straddle",
+ // O9 — the ceremony-only finale: the decline fires before any music is planned
+ "tts:signoff-22",
+ ];
+
+ public static readonly string[] DjNames =
+ [
+ "Nova", "Nova", "(none)", "Nova", // O1
+ "(none)", "Nova", "(none)", "Nova", // O2
+ "(none)", "Nova", "(none)", "Nova", // O3
+ "(none)", "Nova", "(none)", "(none)", "Nova", // O4
+ "Nova", // O5
+ "(none)", "(none)", "(none)", "Nova", // O6
+ "(none)", "(none)", "(none)", "Nova", // O7
+ "(none)", "(none)", "(none)", "(none)", "Nova", // O8
+ "(none)", // O9
+ ];
+
+ public static readonly string[] EventKinds = ["HandoffPieceDropped"];
+
+ public static readonly string[] Warnings =
+ [
+ "Handoff piece SignOff dropped (render budget exceeded) — that half of the ceremony airs nothing; the other piece still airs if it rendered, and the next boundary retries the full ceremony (SPEC F92.4).",
+ "Context segment for provider traffic dropped (render returned null) — no context item reaches air this boundary; music continues, and the next drain retries (SPEC F107.6).",
+ "TimeDate deferral armed for 10:02 dropped undrained — 300s past its armed hour (budget 180s); a late time check would invent the hour.",
+ ];
+}
diff --git a/tests/GenWave.Orchestration.Tests/Specs/Story453_BreakContract.cs b/tests/GenWave.Orchestration.Tests/Specs/Story453_BreakContract.cs
new file mode 100644
index 00000000..b6f4c387
--- /dev/null
+++ b/tests/GenWave.Orchestration.Tests/Specs/Story453_BreakContract.cs
@@ -0,0 +1,260 @@
+// STORY-453 — The break contract has one fact per cell (gh-#401 · SPEC F186 · PLAN T517–T519)
+//
+// BDD specification — xUnit. One fact per cell of the SPEC F186 table, all against the unsplit code. Rows AC1–AC17 (T517), drops AC18–AC23
+// (T518). AC24 (the two follow-up issues) is a manual check on PR-2's body. F186.4: back-announce on a
+// ceremony-only unit STAYS (Dean, 2026-09-17); the other two cells are scripted as-built and filed as follow-ups.
+//
+// RED at plan time: every fact is [Fact(Skip = Pending)] with a loud body — remove the Skip only in the
+// task that makes it green. The builder comments name the arrange each scenario needs.
+
+namespace GenWave.Orchestration.Tests.Specs;
+
+public static class FeatureBreakContract
+{
+ const string Pending = "pending: T517 — every F186 row and collision pinned through the builder (STORY-453)";
+ const string Manual = "manual: two follow-up issues named in SPEC F186.4 — review evidence on PR-2 (STORY-453)";
+
+ // ---------------------------------------------------------------------
+ // HAPPY PATH
+ // ---------------------------------------------------------------------
+
+ public sealed class ScenarioBackAnnounceWithAPreviousTrack
+ {
+ // Given: cadence back-announce on, a previous track
+
+ /// AC1 — the first spoken item names the previous track
+ [Fact(Skip = Pending)]
+ public void BuffersABackAnnounceFirst() => Assert.Fail(Pending);
+ }
+
+ public sealed class ScenarioTheFirstUnit
+ {
+ // Given: no previous track
+
+ /// AC2 — nothing to announce
+ [Fact(Skip = Pending)]
+ public void BuffersNoBackAnnounce() => Assert.Fail(Pending);
+ }
+
+ public sealed class ScenarioAVendableCrosstalk
+ {
+ // Given: a crosstalk vend, a next track, no drainAsOf, no ceremony due
+
+ /// AC3 — the crosstalk file sits after the back-announce
+ [Fact(Skip = Pending)]
+ public void FollowsTheBackAnnounce() => Assert.Fail(Pending);
+ }
+
+ public sealed class ScenarioACrosstalkAndADueSignOff
+ {
+ // Given: a vendable crosstalk with a SignOff due now
+
+ /// AC4 — a drained ceremony excludes crosstalk
+ [Fact(Skip = Pending)]
+ public void BuffersNoCrosstalk() => Assert.Fail(Pending);
+ }
+
+ public sealed class ScenarioThreePendingAnnouncements
+ {
+ // Given: three claimable announcements
+
+ /// AC5 — the cap is two per break
+ [Fact(Skip = Pending)]
+ public void BuffersExactlyTwo() => Assert.Fail(Pending);
+ }
+
+ public sealed class ScenarioStationIdCadenceHit
+ {
+ // Given: StationIdEveryNUnits = 2, unit 2
+
+ /// AC6 — the cadence enqueues a StationId
+ [Fact(Skip = Pending)]
+ public void BuffersAStationIdAfterTheAnnouncements() => Assert.Fail(Pending);
+ }
+
+ public sealed class ScenarioAdCadenceHit
+ {
+ // Given: ad cadence every 2 units and a vendable spot, unit 2
+
+ /// AC7 — the vended spot is buffered
+ [Fact(Skip = Pending)]
+ public void BuffersTheSpot() => Assert.Fail(Pending);
+ }
+
+ public sealed class ScenarioStationIdAndAdDueTogether
+ {
+ // Given: both cadences hit the same unit
+
+ /// AC8 — the drain tiebreak
+ [Fact(Skip = Pending)]
+ public void StationIdPrecedesTheAd() => Assert.Fail(Pending);
+ }
+
+ public sealed class ScenarioContextAndTimeDateDueTogether
+ {
+ // Given: both deferrals due the same unit
+
+ /// AC9 — the drain tiebreak
+ [Fact(Skip = Pending)]
+ public void ContextPrecedesTimeDate() => Assert.Fail(Pending);
+ }
+
+ public sealed class ScenarioAPooledStationId
+ {
+ // Given: catalog with a ready station-id asset
+
+ /// AC10 — the pool hit is used
+ [Fact(Skip = Pending)]
+ public void BuffersThePooledItem() => Assert.Fail(Pending);
+
+ /// AC10 — no render when the pool hits
+ [Fact(Skip = Pending)]
+ public void SendsNoShowIdentRequest() => Assert.Fail(Pending);
+ }
+
+ public sealed class ScenarioAnEmptyStationIdPool
+ {
+ // Given: catalog without a station-id asset
+
+ /// AC11 — the tts fake received the render
+ [Fact(Skip = Pending)]
+ public void SendsAShowIdentRequest() => Assert.Fail(Pending);
+ }
+
+ public sealed class ScenarioALateTimeDate
+ {
+ // Given: TimeDate due 10 s ago with a 90 s budget
+
+ /// AC12 — the request says Late
+ [Fact(Skip = Pending)]
+ public void CarriesFreshnessLate() => Assert.Fail(Pending);
+ }
+
+ public sealed class ScenarioADueContextSegment
+ {
+ // Given: a Context deferral with provider key weather
+
+ /// AC13 — the buffered item names the provider
+ [Fact(Skip = Pending)]
+ public void CarriesTheProviderKey() => Assert.Fail(Pending);
+ }
+
+ public sealed class ScenarioLeadInOn
+ {
+ // Given: cadence lead-in on
+
+ /// AC14 — lead-in sits last
+ [Fact(Skip = Pending)]
+ public void IsTheLastSpokenItemBeforeTheTrack() => Assert.Fail(Pending);
+ }
+
+ public sealed class ScenarioACeremonyOnlyUnit
+ {
+ // Given: a SignOff below the music floor and a vendable crosstalk
+
+ /// AC15 —
+ [Fact(Skip = Pending)]
+ public void BuffersNoCrosstalk() => Assert.Fail(Pending);
+
+ /// AC15 —
+ [Fact(Skip = Pending)]
+ public void BuffersNoLeadIn() => Assert.Fail(Pending);
+
+ /// AC15 —
+ [Fact(Skip = Pending)]
+ public void BuffersNoTrack() => Assert.Fail(Pending);
+ }
+
+ public sealed class ScenarioACeremonyOnlyUnitWithAPreviousTrack
+ {
+ // Given: the same unit with back-announce on (Dean: stays)
+
+ /// AC16 — the outgoing DJ signs off the last track
+ [Fact(Skip = Pending)]
+ public void BuffersABackAnnounceBeforeTheSignOff() => Assert.Fail(Pending);
+ }
+
+ public sealed class ScenarioAStraddlingTrackWithASignOffPending
+ {
+ // Given: the track crosses the boundary
+
+ /// AC17 — NotBefore at or after now plus the tail
+ [Fact(Skip = Pending)]
+ public void HoldsTheSignOnPastTheQueuedTail() => Assert.Fail(Pending);
+ }
+
+ // ---------------------------------------------------------------------
+ // SAD PATH — segregated
+ // ---------------------------------------------------------------------
+
+ public sealed class ScenarioAnExpiredTimeDate
+ {
+ // Given: TimeDate due 120 s ago with a 90 s budget
+
+ /// AC18 —
+ [Fact(Skip = Pending)]
+ public void BuffersNoTimeDate() => Assert.Fail(Pending);
+
+ /// AC18 —
+ [Fact(Skip = Pending)]
+ public void LogsOneExpiryLine() => Assert.Fail(Pending);
+ }
+
+ public sealed class ScenarioAnAdVendThatThrows
+ {
+ // Given: the vend throws
+
+ /// AC19 —
+ [Fact(Skip = Pending)]
+ public void AssemblesTheBreakWithoutAnAd() => Assert.Fail(Pending);
+
+ /// AC19 —
+ [Fact(Skip = Pending)]
+ public void LogsOneWarnNamingTheVend() => Assert.Fail(Pending);
+ }
+
+ public sealed class ScenarioANullLeadInRender
+ {
+ // Given: the lead-in render returns null
+
+ /// AC20 — the slot drops alone
+ [Fact(Skip = Pending)]
+ public void KeepsEveryOtherItemInPosition() => Assert.Fail(Pending);
+ }
+
+ public sealed class ScenarioAnOverBudgetBackAnnounce
+ {
+ // Given: the back-announce render exceeds the budget
+
+ /// AC21 — the slot drops alone
+ [Fact(Skip = Pending)]
+ public void KeepsEveryOtherItemInPosition() => Assert.Fail(Pending);
+ }
+
+ public sealed class ScenarioANullSignOffRender
+ {
+ // Given: the SignOff render returns null
+
+ /// AC22 —
+ [Fact(Skip = Pending)]
+ public void PublishesOneHandoffPieceDropped() => Assert.Fail(Pending);
+ }
+
+ public sealed class ScenarioANullAnnouncementRender
+ {
+ // Given: an announcement render returns null
+
+ /// AC23 — the claim is not released
+ [Fact(Skip = Pending)]
+ public void KeepsTheAnnouncementClaimed() => Assert.Fail(Pending);
+ }
+
+ public sealed class ScenarioTheFollowUpsOnPrTwo
+ {
+ // Given: PR-2's body (manual)
+
+ /// AC24 — announcements on a ceremony-only unit; ad in a straddle break
+ [Fact(Skip = Manual)]
+ public void TwoIssuesExistInProjectThree() => Assert.Fail(Manual);
+ }
+}
diff --git a/tests/GenWave.Orchestration.Tests/Specs/Story454_ThePlanIsAValue.cs b/tests/GenWave.Orchestration.Tests/Specs/Story454_ThePlanIsAValue.cs
new file mode 100644
index 00000000..14265217
--- /dev/null
+++ b/tests/GenWave.Orchestration.Tests/Specs/Story454_ThePlanIsAValue.cs
@@ -0,0 +1,108 @@
+// STORY-454 — The plan is a value (gh-#401 · SPEC F187 · PLAN T520)
+//
+// BDD specification — xUnit. AC1/AC2 reflect the types; AC3–AC8 build plans by hand and read ordinals, traces, reservations and speakers.
+//
+// RED at plan time: every fact is [Fact(Skip = Pending)] with a loud body — remove the Skip only in the
+// task that makes it green. The builder comments name the arrange each scenario needs.
+
+namespace GenWave.Orchestration.Tests.Specs;
+
+public static class FeatureThePlanIsAValue
+{
+ const string Pending = "pending: T520 — BreakPlan, PlannedSlot, SlotSource, Reservation, BreakContext, ToTrace() (STORY-454)";
+
+ // ---------------------------------------------------------------------
+ // HAPPY PATH
+ // ---------------------------------------------------------------------
+
+ public sealed class ScenarioThePlanTypesReflected
+ {
+ // Given: typeof(BreakPlan), PlannedSlot, Reservation, BreakContext
+
+ /// AC1 —
+ [Fact(Skip = Pending)]
+ public void BreakPlanIsAPublicSealedRecord() => Assert.Fail(Pending);
+
+ /// AC1 —
+ [Fact(Skip = Pending)]
+ public void PlannedSlotIsAPublicSealedRecord() => Assert.Fail(Pending);
+
+ /// AC1 —
+ [Fact(Skip = Pending)]
+ public void ReservationIsAPublicSealedRecord() => Assert.Fail(Pending);
+
+ /// AC1 —
+ [Fact(Skip = Pending)]
+ public void BreakContextIsAPublicSealedRecord() => Assert.Fail(Pending);
+ }
+
+ public sealed class ScenarioTheClosedHierarchies
+ {
+ // Given: every subtype of SlotSource and SlotOutcome in the assembly
+
+ /// AC2 — Render, Verbatim, Ready
+ [Fact(Skip = Pending)]
+ public void EverySlotSourceIsSealed() => Assert.Fail(Pending);
+
+ /// AC2 — Rendered, TimedOut, Failed, Abandoned
+ [Fact(Skip = Pending)]
+ public void EverySlotOutcomeIsSealed() => Assert.Fail(Pending);
+ }
+
+ public sealed class ScenarioAThreeSlotPlan
+ {
+ // Given: a plan with three slots
+
+ /// AC3 — ordinals 1, 2, 3 in list order
+ [Fact(Skip = Pending)]
+ public void NumbersThemFromOne() => Assert.Fail(Pending);
+ }
+
+ public sealed class ScenarioARenderSlotTrace
+ {
+ // Given: Render slot LeadIn, speaker Ada, no reservation
+
+ /// AC4 — #1 LeadIn render speaker=Ada res=-
+ [Fact(Skip = Pending)]
+ public void PrintsTheLine() => Assert.Fail(Pending);
+ }
+
+ public sealed class ScenarioAReservedReadySlotTrace
+ {
+ // Given: Ready slot Ad with reservation AdSpot:42
+
+ /// AC5 — #1 Ad ready speaker=- res=AdSpot:42
+ [Fact(Skip = Pending)]
+ public void PrintsTheLine() => Assert.Fail(Pending);
+ }
+
+ public sealed class ScenarioTheEmptyPlan
+ {
+ // Given: a plan with no slots
+
+ /// AC6 — (empty)
+ [Fact(Skip = Pending)]
+ public void PrintsEmpty() => Assert.Fail(Pending);
+ }
+
+ public sealed class ScenarioAPlanFromTheScript
+ {
+ // Given: the F185.1 break planned
+
+ /// AC7 — BackAnnounce, LeadIn, SignOff, SignOn, TimeDate, Context
+ [Fact(Skip = Pending)]
+ public void UnclaimedKindsCarryNoReservation() => Assert.Fail(Pending);
+
+ /// AC7 — every other slot
+ [Fact(Skip = Pending)]
+ public void ClaimedKindsCarryOne() => Assert.Fail(Pending);
+
+ /// AC8 — Render and Verbatim
+ [Fact(Skip = Pending)]
+ public void SpokenSlotsCarryASpeaker() => Assert.Fail(Pending);
+
+ /// AC8 —
+ [Fact(Skip = Pending)]
+ public void ReadySlotsCarryNone() => Assert.Fail(Pending);
+ }
+}
diff --git a/tests/GenWave.Orchestration.Tests/Specs/Story455_BreakPlanner.cs b/tests/GenWave.Orchestration.Tests/Specs/Story455_BreakPlanner.cs
new file mode 100644
index 00000000..feb925f4
--- /dev/null
+++ b/tests/GenWave.Orchestration.Tests/Specs/Story455_BreakPlanner.cs
@@ -0,0 +1,84 @@
+// STORY-455 — BreakPlanner decides the break (gh-#401 · SPEC F188 · PLAN T521–T523)
+//
+// BDD specification — xUnit. AC1–AC4 drive BreakPlanner directly with recording/throwing/counting fakes; AC5 is the STORY-452 replay
+// through the Orchestrator that consumes the plan; AC6 reflects the deleted methods (T522).
+//
+// RED at plan time: every fact is [Fact(Skip = Pending)] with a loud body — remove the Skip only in the
+// task that makes it green. The builder comments name the arrange each scenario needs.
+
+namespace GenWave.Orchestration.Tests.Specs;
+
+public static class FeatureBreakPlanner
+{
+ const string Pending = "pending: T521 — BreakPlanner.PlanAsync performs today's steps and drains in today's order (STORY-455)";
+
+ // ---------------------------------------------------------------------
+ // HAPPY PATH
+ // ---------------------------------------------------------------------
+
+ public sealed class ScenarioTheScriptPlanned
+ {
+ // Given: PlanAsync per unit of the F185.1 script
+
+ /// AC1 — slot kinds in order equal the trace table
+ [Fact(Skip = Pending)]
+ public void YieldsThePinnedSlotKinds() => Assert.Fail(Pending);
+ }
+
+ public sealed class ScenarioThrowingRenderFakes
+ {
+ // Given: ITtsSegmentSource, IVerbatimSegmentRenderer and the copy writer throw when called
+
+ /// AC2 — the plan phase is render-free
+ [Fact(Skip = Pending)]
+ public void NoFakeWasCalled() => Assert.Fail(Pending);
+ }
+
+ public sealed class ScenarioRecordingSideEffectFakes
+ {
+ // Given: crosstalk, announcements, deferral queue, catalog, ad vend record their calls
+
+ /// AC3 — MarkVended, claim, StationId enqueue, Ad enqueue, drain, pool lookup, vend
+ [Fact(Skip = Pending)]
+ public void RecordsTodaysOrder() => Assert.Fail(Pending);
+ }
+
+ public sealed class ScenarioCountingBudgetProviders
+ {
+ // Given: budget providers count their reads
+
+ /// AC4 —
+ [Fact(Skip = Pending)]
+ public void ReadsEachOnce() => Assert.Fail(Pending);
+
+ /// AC4 —
+ [Fact(Skip = Pending)]
+ public void CarriesTheRenderBudgetOnThePlan() => Assert.Fail(Pending);
+ }
+
+ public sealed class ScenarioTheOrchestratorRidesThePlan
+ {
+ // Given: the STORY-452 replay after T522
+
+ /// AC5 —
+ [Fact(Skip = Pending)]
+ public void KeepsEveryFrozenAssertionGreen() => Assert.Fail(Pending);
+
+ /// AC5 —
+ [Fact(Skip = Pending)]
+ public void MatchesThePinnedTraces() => Assert.Fail(Pending);
+ }
+
+ public sealed class ScenarioTheOldPathReflected
+ {
+ // Given: typeof(Orchestrator) non-public methods
+
+ /// AC6 —
+ [Fact(Skip = Pending)]
+ public void HasNoEnqueuePatterAsync() => Assert.Fail(Pending);
+
+ /// AC6 — BuildStationIdRequest, BuildAdRequest, BuildHandoffRequest, BuildTimeDateRequest, BuildContextSegmentRequestAsync
+ [Fact(Skip = Pending)]
+ public void HasNoBuildRequestMethods() => Assert.Fail(Pending);
+ }
+}
diff --git a/tests/GenWave.Orchestration.Tests/Specs/Story456_SpeakerTravelsWithThePlan.cs b/tests/GenWave.Orchestration.Tests/Specs/Story456_SpeakerTravelsWithThePlan.cs
new file mode 100644
index 00000000..f0166db6
--- /dev/null
+++ b/tests/GenWave.Orchestration.Tests/Specs/Story456_SpeakerTravelsWithThePlan.cs
@@ -0,0 +1,83 @@
+// STORY-456 — The speaker travels with the plan (gh-#772 · SPEC F189.1, F189.4, F189.5, F189.7 · PLAN T524, T527)
+//
+// BDD specification — xUnit. AC1 pins the additive contract; AC6–AC9 drive the planner and the ceremony arm with a counting snapshot source
+// and a source that flips the active persona at render time. The render branch is Tts.Tests
+// (Story456_SnapshotDrivesTheRender); card-by-id is Host.Tests (Story456_PersonaCardById); the seam index is
+// Architecture.Tests (Story456_SeamIndex). AC13 is the dev-station wire (T529).
+//
+// RED at plan time: every fact is [Fact(Skip = Pending)] with a loud body — remove the Skip only in the
+// task that makes it green. The builder comments name the arrange each scenario needs.
+
+namespace GenWave.Orchestration.Tests.Specs;
+
+public static class FeatureSpeakerTravelsWithThePlan
+{
+ const string Pending = "pending: T527 — the planner stamps speakers; HandoffContext.Speaker captured at arm (STORY-456)";
+ const string Manual = "manual: dev-station wire, T529 — booth_log rows + stereo capture (STORY-456)";
+
+ // ---------------------------------------------------------------------
+ // HAPPY PATH
+ // ---------------------------------------------------------------------
+
+ public sealed class ScenarioASegmentRequestBuiltTheOldWay
+ {
+ // Given: SegmentRequest with today's arguments
+
+ /// AC1 — Speaker is null
+ [Fact(Skip = Pending)]
+ public void HasNoSpeaker() => Assert.Fail(Pending);
+ }
+
+ public sealed class ScenarioASignOnArmedForBWhileAIsActive
+ {
+ // Given: the active persona flips to A at render time
+
+ /// AC6 — the request's Speaker is B
+ [Fact(Skip = Pending)]
+ public void RendersWithBsSnapshot() => Assert.Fail(Pending);
+ }
+
+ public sealed class ScenarioAContextSegmentPlannedUnderA
+ {
+ // Given: the active persona flips to B before the render
+
+ /// AC7 — the request's Speaker is A
+ [Fact(Skip = Pending)]
+ public void RendersWithAsSnapshot() => Assert.Fail(Pending);
+ }
+
+ public sealed class ScenarioAnArmedCeremonyReArmed
+ {
+ // Given: re-arm with heldNotBefore
+
+ /// AC8 — HandoffContext.Speaker survives
+ [Fact(Skip = Pending)]
+ public void KeepsTheOriginalSpeaker() => Assert.Fail(Pending);
+ }
+
+ public sealed class ScenarioACountingSnapshotSource
+ {
+ // Given: a break naming the station voice and two personas
+
+ /// AC9 —
+ [Fact(Skip = Pending)]
+ public void ResolvesTheStationOnce() => Assert.Fail(Pending);
+
+ /// AC9 —
+ [Fact(Skip = Pending)]
+ public void ResolvesEachPersonaOnce() => Assert.Fail(Pending);
+ }
+
+ public sealed class ScenarioTheDevStationWire
+ {
+ // Given: two personas of different pace, flip mid-break (manual, T529)
+
+ /// AC13 —
+ [Fact(Skip = Manual)]
+ public void TheBoothLogNamesThePlannedPersona() => Assert.Fail(Manual);
+
+ /// AC13 —
+ [Fact(Skip = Manual)]
+ public void TheAudioPaceMatchesThePlan() => Assert.Fail(Manual);
+ }
+}
diff --git a/tests/GenWave.Orchestration.Tests/Specs/Story457_HandoffCeremonyProducer.cs b/tests/GenWave.Orchestration.Tests/Specs/Story457_HandoffCeremonyProducer.cs
new file mode 100644
index 00000000..d166a329
--- /dev/null
+++ b/tests/GenWave.Orchestration.Tests/Specs/Story457_HandoffCeremonyProducer.cs
@@ -0,0 +1,134 @@
+// STORY-457 — HandoffCeremonyProducer arms the ceremony (gh-#401 · SPEC F190 · PLAN T532)
+//
+// BDD specification — xUnit. AC1–AC7 drive ArmAsync per dedupe row; AC8/AC9 the capture and hold; AC10 warn-once; AC11 reflects the
+// constant; AC12 is the existing F92/F142/F112 suite.
+//
+// RED at plan time: every fact is [Fact(Skip = Pending)] with a loud body — remove the Skip only in the
+// task that makes it green. The builder comments name the arrange each scenario needs.
+
+namespace GenWave.Orchestration.Tests.Specs;
+
+public static class FeatureHandoffCeremonyProducer
+{
+ const string Pending = "pending: T532 — HandoffCeremonyProducer extracted with the dedupe matrix (STORY-457)";
+
+ // ---------------------------------------------------------------------
+ // HAPPY PATH
+ // ---------------------------------------------------------------------
+
+ public sealed class ScenarioGapToGap
+ {
+ // Given: no show either side
+
+ /// AC1 —
+ [Fact(Skip = Pending)]
+ public void ArmsNothing() => Assert.Fail(Pending);
+ }
+
+ public sealed class ScenarioSelfHandoff
+ {
+ // Given: same persona, same show
+
+ /// AC2 —
+ [Fact(Skip = Pending)]
+ public void ArmsNothing() => Assert.Fail(Pending);
+ }
+
+ public sealed class ScenarioSamePersonaDifferentShow
+ {
+ // Given: persona A, show X then Y
+
+ /// AC3 —
+ [Fact(Skip = Pending)]
+ public void ArmsASignOnOnly() => Assert.Fail(Pending);
+ }
+
+ public sealed class ScenarioShowThenGap
+ {
+ // Given: show X then nothing
+
+ /// AC4 —
+ [Fact(Skip = Pending)]
+ public void ArmsASignOffOnly() => Assert.Fail(Pending);
+ }
+
+ public sealed class ScenarioGapThenShow
+ {
+ // Given: nothing then show Y
+
+ /// AC5 —
+ [Fact(Skip = Pending)]
+ public void ArmsASignOnOnly() => Assert.Fail(Pending);
+ }
+
+ public sealed class ScenarioTwoShowsTwoPersonas
+ {
+ // Given: show X (A) then show Y (B)
+
+ /// AC6 —
+ [Fact(Skip = Pending)]
+ public void ArmsASignOff() => Assert.Fail(Pending);
+
+ /// AC6 —
+ [Fact(Skip = Pending)]
+ public void ArmsASignOn() => Assert.Fail(Pending);
+ }
+
+ public sealed class ScenarioTheSameBoundaryArmedTwice
+ {
+ // Given: ArmAsync twice
+
+ /// AC7 —
+ [Fact(Skip = Pending)]
+ public void HoldsEachKindOnce() => Assert.Fail(Pending);
+ }
+
+ public sealed class ScenarioACrossingTrackCaptured
+ {
+ // Given: CaptureCrossingTrack(track) on an armed SignOn
+
+ /// AC8 —
+ [Fact(Skip = Pending)]
+ public void StampsTheCrossingTitle() => Assert.Fail(Pending);
+ }
+
+ public sealed class ScenarioASignOnHeldPastTheTail
+ {
+ // Given: HoldSignOnPastQueuedTail(now, 40 s)
+
+ /// AC9 — clamped
+ [Fact(Skip = Pending)]
+ public void SetsNotBeforeToNowPlusTheTail() => Assert.Fail(Pending);
+ }
+
+ public sealed class ScenarioTheConstantOnTheOrchestrator
+ {
+ // Given: typeof(Orchestrator).SignOffLeadTime
+
+ /// AC11 —
+ [Fact(Skip = Pending)]
+ public void IsAPublicStaticFifteenSeconds() => Assert.Fail(Pending);
+ }
+
+ public sealed class ScenarioTheOldFacts
+ {
+ // Given: F92 / F142 / F112 specs
+
+ /// AC12 —
+ [Fact(Skip = Pending)]
+ public void StayGreenWithTheProducer() => Assert.Fail(Pending);
+ }
+
+ // ---------------------------------------------------------------------
+ // SAD PATH — segregated
+ // ---------------------------------------------------------------------
+
+ public sealed class ScenarioNoScheduleResolver
+ {
+ // Given: ArmAsync three times without a resolver
+
+ /// AC10 —
+ [Fact(Skip = Pending)]
+ public void WarnsExactlyOnce() => Assert.Fail(Pending);
+ }
+}
diff --git a/tests/GenWave.Orchestration.Tests/Specs/Story458_BreakRenderer.cs b/tests/GenWave.Orchestration.Tests/Specs/Story458_BreakRenderer.cs
new file mode 100644
index 00000000..59b9f761
--- /dev/null
+++ b/tests/GenWave.Orchestration.Tests/Specs/Story458_BreakRenderer.cs
@@ -0,0 +1,132 @@
+// STORY-458 — BreakRenderer turns slots into outcomes (gh-#401 · SPEC F191 · PLAN T534)
+//
+// BDD specification — xUnit. Every scenario drives BreakRenderer.RenderAsync with a hand-built plan, a recording tts fake and a fake clock.
+// Sad path: null, throw, budget miss.
+//
+// RED at plan time: every fact is [Fact(Skip = Pending)] with a loud body — remove the Skip only in the
+// task that makes it green. The builder comments name the arrange each scenario needs.
+
+namespace GenWave.Orchestration.Tests.Specs;
+
+public static class FeatureBreakRenderer
+{
+ const string Pending = "pending: T534 — BreakRenderer.RenderAsync on a FakeTimeProvider (STORY-458)";
+
+ // ---------------------------------------------------------------------
+ // HAPPY PATH
+ // ---------------------------------------------------------------------
+
+ public sealed class ScenarioAFourSlotPlan
+ {
+ // Given: four Render slots
+
+ /// AC1 —
+ [Fact(Skip = Pending)]
+ public void ReturnsFourOutcomes() => Assert.Fail(Pending);
+
+ /// AC1 —
+ [Fact(Skip = Pending)]
+ public void ReturnsThemInOrdinalOrder() => Assert.Fail(Pending);
+ }
+
+ public sealed class ScenarioThreeOneSecondRenders
+ {
+ // Given: a tts fake stamping the fake clock per call
+
+ /// AC2 — all three timestamps equal the start
+ [Fact(Skip = Pending)]
+ public void KicksEveryRenderAtTheStart() => Assert.Fail(Pending);
+ }
+
+ public sealed class ScenarioAReadySlot
+ {
+ // Given: Ready slot with item X
+
+ /// AC3 — Rendered(X)
+ [Fact(Skip = Pending)]
+ public void PassesTheItemThrough() => Assert.Fail(Pending);
+
+ /// AC3 —
+ [Fact(Skip = Pending)]
+ public void MakesNoTtsCall() => Assert.Fail(Pending);
+ }
+
+ public sealed class ScenarioAVerbatimSlotWithFlavoredCopy
+ {
+ // Given: the copy writer returns flavored
+
+ /// AC4 —
+ [Fact(Skip = Pending)]
+ public void RendersTheFlavoredCopy() => Assert.Fail(Pending);
+ }
+
+ public sealed class ScenarioAVerbatimSlotWithoutFlavoredCopy
+ {
+ // Given: the copy writer returns null
+
+ /// AC5 —
+ [Fact(Skip = Pending)]
+ public void RendersThePlainCopy() => Assert.Fail(Pending);
+ }
+
+ public sealed class ScenarioACapturingLoggerEventSinkAndEstimator
+ {
+ // Given: all three wired to the renderer's caller, not the renderer
+
+ /// AC6 —
+ [Fact(Skip = Pending)]
+ public void TheLoggerHeardNothing() => Assert.Fail(Pending);
+
+ /// AC6 —
+ [Fact(Skip = Pending)]
+ public void TheEventSinkHeardNothing() => Assert.Fail(Pending);
+
+ /// AC6 —
+ [Fact(Skip = Pending)]
+ public void TheEstimatorHeardNothing() => Assert.Fail(Pending);
+ }
+
+ public sealed class ScenarioARenderThatLandsOnTheBudget
+ {
+ // Given: completes at exactly the budget
+
+ /// AC10 — outcome from the task, not the race
+ [Fact(Skip = Pending)]
+ public void IsRendered() => Assert.Fail(Pending);
+ }
+
+ // ---------------------------------------------------------------------
+ // SAD PATH — segregated
+ // ---------------------------------------------------------------------
+
+ public sealed class ScenarioANullRender
+ {
+ // Given: the tts fake returns null
+
+ /// AC7 —
+ [Fact(Skip = Pending)]
+ public void IsFailed() => Assert.Fail(Pending);
+ }
+
+ public sealed class ScenarioAThrowingRender
+ {
+ // Given: the tts fake throws
+
+ /// AC8 —
+ [Fact(Skip = Pending)]
+ public void IsFailed() => Assert.Fail(Pending);
+
+ /// AC8 —
+ [Fact(Skip = Pending)]
+ public void CarriesTheException() => Assert.Fail(Pending);
+ }
+
+ public sealed class ScenarioABudgetMiss
+ {
+ // Given: 5 s budget, the render completes after 6 s of fake time
+
+ /// AC9 —
+ [Fact(Skip = Pending)]
+ public void IsTimedOut() => Assert.Fail(Pending);
+ }
+}
diff --git a/tests/GenWave.Orchestration.Tests/Specs/Story459_BreakDelivery.cs b/tests/GenWave.Orchestration.Tests/Specs/Story459_BreakDelivery.cs
new file mode 100644
index 00000000..95ddd857
--- /dev/null
+++ b/tests/GenWave.Orchestration.Tests/Specs/Story459_BreakDelivery.cs
@@ -0,0 +1,111 @@
+// STORY-459 — BreakDelivery orders the buffer and keeps the books (gh-#401 · SPEC F192 · PLAN T536)
+//
+// BDD specification — xUnit. AC1–AC6 drive Deliver with hand-built outcomes; AC7/AC8 the drop policies; AC9 goes through the Orchestrator
+// with a cancelled token.
+//
+// RED at plan time: every fact is [Fact(Skip = Pending)] with a loud body — remove the Skip only in the
+// task that makes it green. The builder comments name the arrange each scenario needs.
+
+namespace GenWave.Orchestration.Tests.Specs;
+
+public static class FeatureBreakDelivery
+{
+ const string Pending = "pending: T536 — BreakDelivery.Deliver(plan, outcomes) → BreakOutcome (STORY-459)";
+
+ // ---------------------------------------------------------------------
+ // HAPPY PATH
+ // ---------------------------------------------------------------------
+
+ public sealed class ScenarioRenderedFailedRendered
+ {
+ // Given: outcomes for slots 1–3
+
+ /// AC1 —
+ [Fact(Skip = Pending)]
+ public void ItemsHoldSlotOneThenSlotThree() => Assert.Fail(Pending);
+ }
+
+ public sealed class ScenarioRenderedStationIdAnnouncementAndAd
+ {
+ // Given: three rendered slots, unit DJ Ada
+
+ /// AC2 —
+ [Fact(Skip = Pending)]
+ public void StampsEveryItemWithTheDjName() => Assert.Fail(Pending);
+ }
+
+ public sealed class ScenarioARenderedAnnouncementWithIdSeven
+ {
+ // Given:
+
+ /// AC3 — AnnouncementMediaId.Wrap(7, …)
+ [Fact(Skip = Pending)]
+ public void WrapsTheMediaId() => Assert.Fail(Pending);
+ }
+
+ public sealed class ScenarioTwoRenderedSlotsWithDurations
+ {
+ // Given: ObserveDuration set
+
+ /// AC4 —
+ [Fact(Skip = Pending)]
+ public void ObservesBothInOrder() => Assert.Fail(Pending);
+ }
+
+ public sealed class ScenarioARenderedAdAndAFailedAnnouncement
+ {
+ // Given: reservations on both
+
+ /// AC5 —
+ [Fact(Skip = Pending)]
+ public void MarksTheAdAired() => Assert.Fail(Pending);
+
+ /// AC5 —
+ [Fact(Skip = Pending)]
+ public void MarksTheAnnouncementDropped() => Assert.Fail(Pending);
+ }
+
+ public sealed class ScenarioTheDeliveryTypeReflected
+ {
+ // Given: typeof(BreakDelivery) fields
+
+ /// AC6 — the buffer stays on the Orchestrator
+ [Fact(Skip = Pending)]
+ public void HoldsNoQueue() => Assert.Fail(Pending);
+ }
+
+ // ---------------------------------------------------------------------
+ // SAD PATH — segregated
+ // ---------------------------------------------------------------------
+
+ public sealed class ScenarioAFailedContextWithPolicyWarn
+ {
+ // Given:
+
+ /// AC7 —
+ [Fact(Skip = Pending)]
+ public void LogsOneWarnNamingTheProvider() => Assert.Fail(Pending);
+ }
+
+ public sealed class ScenarioAFailedSignOffWithPolicyWarnAndEvent
+ {
+ // Given:
+
+ /// AC8 —
+ [Fact(Skip = Pending)]
+ public void PublishesOneHandoffPieceDropped() => Assert.Fail(Pending);
+ }
+
+ public sealed class ScenarioACancelledTokenBeforeDelivery
+ {
+ // Given: served through the Orchestrator
+
+ /// AC9 —
+ [Fact(Skip = Pending)]
+ public void BuffersNothing() => Assert.Fail(Pending);
+
+ /// AC9 —
+ [Fact(Skip = Pending)]
+ public void AbandonsEveryReservation() => Assert.Fail(Pending);
+ }
+}
diff --git a/tests/GenWave.TestSupport/AssemblyMarker.cs b/tests/GenWave.TestSupport/AssemblyMarker.cs
new file mode 100644
index 00000000..54c7702d
--- /dev/null
+++ b/tests/GenWave.TestSupport/AssemblyMarker.cs
@@ -0,0 +1,6 @@
+namespace GenWave.TestSupport;
+
+/// Anchor type for reflecting this assembly (STORY-451 AC8).
+public static class AssemblyMarker
+{
+}
diff --git a/tests/GenWave.Orchestration.Tests/Fakes/CapturingLogger.cs b/tests/GenWave.TestSupport/Fakes/CapturingLogger.cs
similarity index 77%
rename from tests/GenWave.Orchestration.Tests/Fakes/CapturingLogger.cs
rename to tests/GenWave.TestSupport/Fakes/CapturingLogger.cs
index 5defb3e2..1b9dc0db 100644
--- a/tests/GenWave.Orchestration.Tests/Fakes/CapturingLogger.cs
+++ b/tests/GenWave.TestSupport/Fakes/CapturingLogger.cs
@@ -1,4 +1,4 @@
-namespace GenWave.Orchestration.Tests.Fakes;
+namespace GenWave.TestSupport.Fakes;
using Microsoft.Extensions.Logging;
@@ -11,17 +11,21 @@ namespace GenWave.Orchestration.Tests.Fakes;
/// F82.6 per-pick Debug line, which no pre-T64 spec needed to inspect. Mirrors
/// GenWave.Tts.Tests.Fakes.CapturingLogger<T>. Test-scope only.
///
-sealed class CapturingLogger : ILogger
+public sealed class CapturingLogger : ILogger
{
/// Every logged message, in call order, tagged with the level it was logged at.
public List<(LogLevel Level, string Message)> Entries { get; } = [];
+ /// Every logged message at Warning level or above, in call order.
public IEnumerable Warnings => Entries.Where(e => e.Level >= LogLevel.Warning).Select(e => e.Message);
+ /// No scope tracking — always returns null.
public IDisposable? BeginScope(TState state) where TState : notnull => null;
+ /// Always enabled — every level is captured.
public bool IsEnabled(LogLevel logLevel) => true;
+ /// Records the formatted message into .
public void Log(
LogLevel logLevel, EventId eventId, TState state, Exception? exception,
Func formatter)
diff --git a/tests/GenWave.Orchestration.Tests/Fakes/CapturingPatterDurationEstimator.cs b/tests/GenWave.TestSupport/Fakes/CapturingPatterDurationEstimator.cs
similarity index 69%
rename from tests/GenWave.Orchestration.Tests/Fakes/CapturingPatterDurationEstimator.cs
rename to tests/GenWave.TestSupport/Fakes/CapturingPatterDurationEstimator.cs
index b489525e..fe2c612e 100644
--- a/tests/GenWave.Orchestration.Tests/Fakes/CapturingPatterDurationEstimator.cs
+++ b/tests/GenWave.TestSupport/Fakes/CapturingPatterDurationEstimator.cs
@@ -1,7 +1,7 @@
using GenWave.Core.Abstractions;
using GenWave.Core.Domain;
-namespace GenWave.Orchestration.Tests.Fakes;
+namespace GenWave.TestSupport.Fakes;
///
/// gh-#463 — records every call's (Kind, PersonaName, Voice, ShowName) tuple so
@@ -9,19 +9,23 @@ namespace GenWave.Orchestration.Tests.Fakes;
/// it does with the answer. Always answers at — no
/// spec using this fake asserts on the returned duration itself, only the call shape.
///
-sealed class CapturingPatterDurationEstimator : IPatterDurationEstimator
+public sealed class CapturingPatterDurationEstimator : IPatterDurationEstimator
{
+ /// Every call, in call order.
public List<(SegmentKind Kind, string? PersonaName, string Voice, string? ShowName)> Calls { get; } = [];
+ /// Records the call (showName: null) and answers the fixed heuristic estimate.
public PatterDurationEstimate Estimate(SegmentKind kind, string? personaName, string voice) =>
Estimate(kind, personaName, voice, showName: null);
+ /// Records the call and answers the fixed heuristic estimate.
public PatterDurationEstimate Estimate(SegmentKind kind, string? personaName, string voice, string? showName)
{
Calls.Add((kind, personaName, voice, showName));
return new PatterDurationEstimate(TimeSpan.FromSeconds(5), PatterEstimateConfidence.Heuristic);
}
+ /// No-op — this fake never simulates historical-tier learning.
public void ObserveRendered(SegmentKind kind, string? personaName, string voice, TimeSpan measured)
{
}
diff --git a/tests/GenWave.Orchestration.Tests/Fakes/CapturingStationEventSink.cs b/tests/GenWave.TestSupport/Fakes/CapturingStationEventSink.cs
similarity index 73%
rename from tests/GenWave.Orchestration.Tests/Fakes/CapturingStationEventSink.cs
rename to tests/GenWave.TestSupport/Fakes/CapturingStationEventSink.cs
index de43601d..b65530c4 100644
--- a/tests/GenWave.Orchestration.Tests/Fakes/CapturingStationEventSink.cs
+++ b/tests/GenWave.TestSupport/Fakes/CapturingStationEventSink.cs
@@ -1,7 +1,7 @@
using GenWave.Core.Abstractions;
using GenWave.Core.Events;
-namespace GenWave.Orchestration.Tests.Fakes;
+namespace GenWave.TestSupport.Fakes;
///
/// Recording double (SPEC F87.6, STORY-227, PLAN T90) — collects
@@ -10,9 +10,11 @@ namespace GenWave.Orchestration.Tests.Fakes;
/// over; lives here instead because 's own specs (this
/// project) are the first in GenWave.Orchestration.Tests to need one.
///
-sealed class CapturingStationEventSink : IStationEventSink
+public sealed class CapturingStationEventSink : IStationEventSink
{
+ /// Every published event, in publish order.
public List Events { get; } = [];
+ /// Records the event into .
public void Publish(StationEvent evt) => Events.Add(evt);
}
diff --git a/tests/GenWave.Host.Tests/FakeBoundaryBiasProvider.cs b/tests/GenWave.TestSupport/Fakes/FakeBoundaryBiasProvider.cs
similarity index 66%
rename from tests/GenWave.Host.Tests/FakeBoundaryBiasProvider.cs
rename to tests/GenWave.TestSupport/Fakes/FakeBoundaryBiasProvider.cs
index 83b42f8a..4e8d5f74 100644
--- a/tests/GenWave.Host.Tests/FakeBoundaryBiasProvider.cs
+++ b/tests/GenWave.TestSupport/Fakes/FakeBoundaryBiasProvider.cs
@@ -1,6 +1,6 @@
using GenWave.Core.Abstractions;
-namespace GenWave.Host.Tests;
+namespace GenWave.TestSupport.Fakes;
///
/// Mutable double (SPEC F74.3, mirrors
@@ -8,9 +8,11 @@ namespace GenWave.Host.Tests;
/// calls to simulate a config-provider reload without standing up a real options stack in a unit
/// test.
///
-sealed class FakeBoundaryBiasProvider(TimeSpan lookahead) : IBoundaryBiasProvider
+public sealed class FakeBoundaryBiasProvider(TimeSpan lookahead) : IBoundaryBiasProvider
{
+ /// The lookahead window a spec can mutate between calls.
public TimeSpan Lookahead { get; set; } = lookahead;
+ ///
public TimeSpan Current => Lookahead;
}
diff --git a/tests/GenWave.Orchestration.Tests/Fakes/FakeCadenceProvider.cs b/tests/GenWave.TestSupport/Fakes/FakeCadenceProvider.cs
similarity index 69%
rename from tests/GenWave.Orchestration.Tests/Fakes/FakeCadenceProvider.cs
rename to tests/GenWave.TestSupport/Fakes/FakeCadenceProvider.cs
index 29277244..5f89420a 100644
--- a/tests/GenWave.Orchestration.Tests/Fakes/FakeCadenceProvider.cs
+++ b/tests/GenWave.TestSupport/Fakes/FakeCadenceProvider.cs
@@ -1,7 +1,7 @@
using GenWave.Core.Abstractions;
using GenWave.Core.Domain;
-namespace GenWave.Orchestration.Tests.Fakes;
+namespace GenWave.TestSupport.Fakes;
///
/// Mutable double (gitea-#211, mirrors
@@ -9,9 +9,11 @@ namespace GenWave.Orchestration.Tests.Fakes;
/// IOptionsMonitor<StationOptions> reload without standing up a real options stack in a
/// unit test.
///
-sealed class FakeCadenceProvider(CadenceConfig cadence) : ICadenceProvider
+public sealed class FakeCadenceProvider(CadenceConfig cadence) : ICadenceProvider
{
+ /// The cadence config a spec can mutate between calls.
public CadenceConfig Cadence { get; set; } = cadence;
+ ///
public CadenceConfig Current => Cadence;
}
diff --git a/tests/GenWave.Orchestration.Tests/Fakes/FakeMediaCatalog.cs b/tests/GenWave.TestSupport/Fakes/FakeMediaCatalog.cs
similarity index 92%
rename from tests/GenWave.Orchestration.Tests/Fakes/FakeMediaCatalog.cs
rename to tests/GenWave.TestSupport/Fakes/FakeMediaCatalog.cs
index 65bfd11f..01f9f7a5 100644
--- a/tests/GenWave.Orchestration.Tests/Fakes/FakeMediaCatalog.cs
+++ b/tests/GenWave.TestSupport/Fakes/FakeMediaCatalog.cs
@@ -1,7 +1,7 @@
using GenWave.Core.Abstractions;
using GenWave.Core.Domain;
-namespace GenWave.Orchestration.Tests.Fakes;
+namespace GenWave.TestSupport.Fakes;
///
/// Scripted catalog double for orchestrator unit tests. Captures every call to
@@ -23,11 +23,12 @@ namespace GenWave.Orchestration.Tests.Fakes;
/// literal- ones.
///
///
-sealed class FakeMediaCatalog : IMediaCatalog
+public sealed class FakeMediaCatalog : IMediaCatalog
{
readonly IReadOnlyList pool;
int nextIndex;
+ /// Seeds a single-track pool (or an empty one, passing ).
public FakeMediaCatalog(MediaReference? ready) : this(ready is null ? [] : [ready])
{
}
@@ -40,7 +41,10 @@ public FakeMediaCatalog(MediaReference? ready) : this(ready is null ? [] : [read
/// See the type-level remarks — a candidate pool for boundary-bias specs (SPEC F74.3).
public static FakeMediaCatalog WithPool(IReadOnlyList pool) => new(pool);
+ /// Every excludeIds list passed to , in call order.
public List> RandomCallExcludeLists { get; } = [];
+
+ /// Every scope passed to , in call order.
public List RandomCallScopes { get; } = [];
/// Every orderedRecentIds list passed to , in call order.
@@ -83,9 +87,11 @@ public FakeMediaCatalog(MediaReference? ready) : this(ready is null ? [] : [read
/// SQL concern the real repository's own deliberate non-delegation guards against, so one scripted
/// answer source is simpler and no less honest here.
///
+ ///
public Task GetRandomReadyByImagingKindAsync(LibraryScope scope, ImagingKind kind, CancellationToken ct) =>
GetRandomReadyByImagingKindAsync(scope, kind, showId: null, ct);
+ ///
public Task GetRandomReadyByImagingKindAsync(
LibraryScope scope, ImagingKind kind, long? showId, CancellationToken ct)
{
@@ -93,12 +99,15 @@ public FakeMediaCatalog(MediaReference? ready) : this(ready is null ? [] : [read
return Task.FromResult(ImagingPoolResult);
}
+ ///
public Task GetByIdAsync(LibraryScope scope, string mediaId, CancellationToken ct)
=> Task.FromResult(pool.FirstOrDefault(m => m.MediaId == mediaId));
+ ///
public Task GetByIdUnscopedAsync(string mediaId, CancellationToken ct)
=> Task.FromResult(pool.FirstOrDefault(m => m.MediaId == mediaId));
+ ///
public Task GetRandomReadyAsync(LibraryScope scope, IReadOnlyList excludeIds, CancellationToken ct)
{
RandomCallExcludeLists.Add(excludeIds);
@@ -106,6 +115,7 @@ public FakeMediaCatalog(MediaReference? ready) : this(ready is null ? [] : [read
return Task.FromResult(pool.Count == 0 ? null : pool[0]);
}
+ ///
public Task GetRotationCandidateAsync(
LibraryScope scope, IReadOnlyList orderedRecentIds, int artistSeparation, CancellationToken ct)
{
@@ -123,9 +133,11 @@ public FakeMediaCatalog(MediaReference? ready) : this(ready is null ? [] : [read
RepeatedArtist: ScriptedRepeatedArtist));
}
+ ///
public Task> ListAsync(LibraryScope scope, MediaQuery query, CancellationToken ct) =>
Task.FromResult(new PagedResult([], 0, 0));
+ ///
public Task GetStatusCountsAsync(LibraryScope safeScope, CancellationToken ct) =>
Task.FromResult(new CatalogStatusCounts(0, 0, 0, 0, 0));
diff --git a/tests/GenWave.Orchestration.Tests/Fakes/FakePersonaStore.cs b/tests/GenWave.TestSupport/Fakes/FakePersonaStore.cs
similarity index 79%
rename from tests/GenWave.Orchestration.Tests/Fakes/FakePersonaStore.cs
rename to tests/GenWave.TestSupport/Fakes/FakePersonaStore.cs
index 8bd8d4fd..da658fff 100644
--- a/tests/GenWave.Orchestration.Tests/Fakes/FakePersonaStore.cs
+++ b/tests/GenWave.TestSupport/Fakes/FakePersonaStore.cs
@@ -1,7 +1,7 @@
using GenWave.Core.Abstractions;
using GenWave.Core.Domain;
-namespace GenWave.Orchestration.Tests.Fakes;
+namespace GenWave.TestSupport.Fakes;
///
/// In-memory double (STORY-241/242, PLAN T120) for
@@ -10,7 +10,7 @@ namespace GenWave.Orchestration.Tests.Fakes;
/// member throws, mirroring the "not exercised by these scenarios" convention every other
/// double in this codebase already follows.
///
-sealed class FakePersonaStore : IPersonaStore
+public sealed class FakePersonaStore : IPersonaStore
{
readonly Dictionary personas = [];
readonly Dictionary cards = [];
@@ -24,8 +24,10 @@ sealed class FakePersonaStore : IPersonaStore
/// ResolveAsync path.
public Exception? ThrowOnGetCardById { get; set; }
+ /// Every id passed to , in call order.
public List GetByIdCalls { get; } = [];
+ /// Seeds a persona, keyed by its own id.
public void Add(Persona persona) => personas[persona.Id] = persona;
/// Round-3 review addition — simulates a persona "deleted out of band" mid-test
@@ -34,8 +36,10 @@ sealed class FakePersonaStore : IPersonaStore
/// store itself faulted" shape.
public void Remove(long id) => personas.Remove(id);
+ /// Seeds a persona card, keyed by persona id.
public void AddCard(long personaId, PersonaCard card) => cards[personaId] = card;
+ ///
public Task GetByIdAsync(long id, CancellationToken ct)
{
GetByIdCalls.Add(id);
@@ -44,6 +48,7 @@ sealed class FakePersonaStore : IPersonaStore
return Task.FromResult(personas.TryGetValue(id, out var persona) ? persona : null);
}
+ ///
public Task GetCardByIdAsync(long id, CancellationToken ct)
{
if (ThrowOnGetCardById is { } ex) throw ex;
@@ -51,18 +56,23 @@ sealed class FakePersonaStore : IPersonaStore
return Task.FromResult(cards.TryGetValue(id, out var card) ? card : null);
}
+ /// Not exercised by OnAirPersonaAccessor specs — always throws.
public Task> GetAllAsync(CancellationToken ct) =>
throw new NotSupportedException("Not exercised by OnAirPersonaAccessor specs.");
+ /// Not exercised by OnAirPersonaAccessor specs — always throws.
public Task CreateAsync(PersonaDraft draft, CancellationToken ct) =>
throw new NotSupportedException("Not exercised by OnAirPersonaAccessor specs.");
+ /// Not exercised by OnAirPersonaAccessor specs — always throws.
public Task UpdateAsync(long id, PersonaDraft draft, CancellationToken ct) =>
throw new NotSupportedException("Not exercised by OnAirPersonaAccessor specs.");
+ /// Not exercised by OnAirPersonaAccessor specs — always throws.
public Task DeleteAsync(long id, CancellationToken ct) =>
throw new NotSupportedException("Not exercised by OnAirPersonaAccessor specs.");
+ /// Not exercised by OnAirPersonaAccessor specs — always throws.
public Task GetIdBySlugAsync(string slug, CancellationToken ct) =>
throw new NotSupportedException("Not exercised by OnAirPersonaAccessor specs.");
}
diff --git a/tests/GenWave.Orchestration.Tests/Fakes/FakeRenderBudgetProvider.cs b/tests/GenWave.TestSupport/Fakes/FakeRenderBudgetProvider.cs
similarity index 67%
rename from tests/GenWave.Orchestration.Tests/Fakes/FakeRenderBudgetProvider.cs
rename to tests/GenWave.TestSupport/Fakes/FakeRenderBudgetProvider.cs
index 3fc4d41e..99382df8 100644
--- a/tests/GenWave.Orchestration.Tests/Fakes/FakeRenderBudgetProvider.cs
+++ b/tests/GenWave.TestSupport/Fakes/FakeRenderBudgetProvider.cs
@@ -1,6 +1,6 @@
using GenWave.Core.Abstractions;
-namespace GenWave.Orchestration.Tests.Fakes;
+namespace GenWave.TestSupport.Fakes;
///
/// Mutable double (SPEC F44.2, mirrors
@@ -8,9 +8,11 @@ namespace GenWave.Orchestration.Tests.Fakes;
/// IOptionsMonitor<TtsOptions> reload without standing up a real options stack in a
/// unit test.
///
-sealed class FakeRenderBudgetProvider(TimeSpan budget) : IRenderBudgetProvider
+public sealed class FakeRenderBudgetProvider(TimeSpan budget) : IRenderBudgetProvider
{
+ /// The render budget a spec can mutate between calls.
public TimeSpan Budget { get; set; } = budget;
+ ///
public TimeSpan Current => Budget;
}
diff --git a/tests/GenWave.Orchestration.Tests/Fakes/FakeRotationSettingsProvider.cs b/tests/GenWave.TestSupport/Fakes/FakeRotationSettingsProvider.cs
similarity index 67%
rename from tests/GenWave.Orchestration.Tests/Fakes/FakeRotationSettingsProvider.cs
rename to tests/GenWave.TestSupport/Fakes/FakeRotationSettingsProvider.cs
index 00d887d4..480e76bf 100644
--- a/tests/GenWave.Orchestration.Tests/Fakes/FakeRotationSettingsProvider.cs
+++ b/tests/GenWave.TestSupport/Fakes/FakeRotationSettingsProvider.cs
@@ -1,7 +1,7 @@
using GenWave.Core.Abstractions;
using GenWave.Core.Domain;
-namespace GenWave.Orchestration.Tests.Fakes;
+namespace GenWave.TestSupport.Fakes;
///
/// Mutable double (SPEC F41.6, mirrors
@@ -9,9 +9,11 @@ namespace GenWave.Orchestration.Tests.Fakes;
/// IOptionsMonitor<StationOptions> reload without standing up a real options stack in a
/// unit test.
///
-sealed class FakeRotationSettingsProvider(RotationSettings settings) : IRotationSettingsProvider
+public sealed class FakeRotationSettingsProvider(RotationSettings settings) : IRotationSettingsProvider
{
+ /// The rotation settings a spec can mutate between calls.
public RotationSettings Settings { get; set; } = settings;
+ ///
public RotationSettings Current => Settings;
}
diff --git a/tests/GenWave.Orchestration.Tests/Fakes/FakeScheduleSpecialStore.cs b/tests/GenWave.TestSupport/Fakes/FakeScheduleSpecialStore.cs
similarity index 83%
rename from tests/GenWave.Orchestration.Tests/Fakes/FakeScheduleSpecialStore.cs
rename to tests/GenWave.TestSupport/Fakes/FakeScheduleSpecialStore.cs
index 660f61cc..498a76c3 100644
--- a/tests/GenWave.Orchestration.Tests/Fakes/FakeScheduleSpecialStore.cs
+++ b/tests/GenWave.TestSupport/Fakes/FakeScheduleSpecialStore.cs
@@ -1,7 +1,7 @@
using GenWave.Core.Abstractions;
using GenWave.Core.Domain;
-namespace GenWave.Orchestration.Tests.Fakes;
+namespace GenWave.TestSupport.Fakes;
///
/// In-memory double (STORY-317, PLAN T260) — the specials-cache
@@ -22,11 +22,12 @@ namespace GenWave.Orchestration.Tests.Fakes;
/// mid-flight-invalidation race already proves for the week snapshot.
///
///
-sealed class FakeScheduleSpecialStore(IReadOnlyList? seed = null) : IScheduleSpecialStore
+public sealed class FakeScheduleSpecialStore(IReadOnlyList? seed = null) : IScheduleSpecialStore
{
IReadOnlyList current = seed ?? [];
TaskCompletionSource>? pendingLoad;
+ /// How many times has been called.
public int ListUpcomingAsyncCallCount { get; private set; }
/// Every fromDate a caller has ever passed, in call order — lets a spec assert
@@ -38,8 +39,10 @@ sealed class FakeScheduleSpecialStore(IReadOnlyList? seed = nul
/// 's own convention. Never cleared automatically.
public Exception? ThrowOnListUpcoming { get; set; }
+ /// Raised by to simulate a write from another caller.
public event Action? SpecialsChanged;
+ ///
public Task> ListUpcomingAsync(DateOnly fromDate, CancellationToken ct)
{
ListUpcomingAsyncCallCount++;
@@ -49,12 +52,15 @@ public Task> ListUpcomingAsync(DateOnly fromDate,
return pendingLoad?.Task ?? Task.FromResult(current);
}
+ /// Not needed by any T260 spec — only ever reads. Always throws.
public Task CreateAsync(ScheduleSpecial special, CancellationToken ct) =>
throw new NotSupportedException("FakeScheduleSpecialStore is a read-only double for T260 caching specs.");
+ /// Not needed by any T260 spec — only ever reads. Always throws.
public Task DeleteAsync(long id, CancellationToken ct) =>
throw new NotSupportedException("FakeScheduleSpecialStore is a read-only double for T260 caching specs.");
+ /// Simulates a specials write landing from another caller by raising .
public void RaiseSpecialsChanged() => SpecialsChanged?.Invoke();
/// Arms a gate so the NEXT call returns an incomplete task,
diff --git a/tests/GenWave.Orchestration.Tests/Fakes/FakeScheduleStore.cs b/tests/GenWave.TestSupport/Fakes/FakeScheduleStore.cs
similarity index 82%
rename from tests/GenWave.Orchestration.Tests/Fakes/FakeScheduleStore.cs
rename to tests/GenWave.TestSupport/Fakes/FakeScheduleStore.cs
index b84802f8..4e7af955 100644
--- a/tests/GenWave.Orchestration.Tests/Fakes/FakeScheduleStore.cs
+++ b/tests/GenWave.TestSupport/Fakes/FakeScheduleStore.cs
@@ -1,7 +1,7 @@
using GenWave.Core.Abstractions;
using GenWave.Core.Domain;
-namespace GenWave.Orchestration.Tests.Fakes;
+namespace GenWave.TestSupport.Fakes;
///
/// In-memory double (STORY-241, PLAN T119) that counts
@@ -18,11 +18,12 @@ namespace GenWave.Orchestration.Tests.Fakes;
/// so the NEXT (ungated) load observes it.
///
///
-sealed class FakeScheduleStore(ScheduleWeekSnapshot snapshot) : IScheduleStore
+public sealed class FakeScheduleStore(ScheduleWeekSnapshot snapshot) : IScheduleStore
{
ScheduleWeekSnapshot current = snapshot;
TaskCompletionSource? pendingLoad;
+ /// How many times has been called.
public int LoadWeekAsyncCallCount { get; private set; }
/// Set to make the next call throw, simulating an
@@ -31,8 +32,10 @@ sealed class FakeScheduleStore(ScheduleWeekSnapshot snapshot) : IScheduleStore
/// a scenario proving recovery sets this back to itself.
public Exception? ThrowOnLoadWeek { get; set; }
+ /// Raised by to simulate a write from another caller.
public event Action? WeekChanged;
+ ///
public Task LoadWeekAsync(CancellationToken ct)
{
LoadWeekAsyncCallCount++;
@@ -41,20 +44,24 @@ public Task LoadWeekAsync(CancellationToken ct)
return pendingLoad?.Task ?? Task.FromResult(current);
}
+ /// Not needed by any T119 spec — only ever reads. Always throws.
public Task ReplaceWeekAsync(
IReadOnlyList week, string? expectedVersion, CancellationToken ct) =>
throw new NotSupportedException("FakeScheduleStore is a read-only double for T119 specs.");
+ /// PLAN T240's show delete guard read — not exercised by any T119 spec. Always throws.
// PLAN T240's show delete guard read — not exercised by any T119 spec (this double never carries
// show_id-scoped rows), mirrors ReplaceWeekAsync's own NotSupportedException posture above.
public Task> GetSlotsByShowIdAsync(long showId, CancellationToken ct) =>
throw new NotSupportedException("FakeScheduleStore is a read-only double for T119 specs.");
+ /// PLAN T243's show assignment write — not exercised by any T119 spec. Always throws.
// PLAN T243's show assignment write — not exercised by any T119 spec (this double is read-only by
// design, see class remarks), mirrors ReplaceWeekAsync's own NotSupportedException posture above.
public Task AssignShowAsync(long blockId, long? showId, bool applyToRun, CancellationToken ct) =>
throw new NotSupportedException("FakeScheduleStore is a read-only double for T119 specs.");
+ /// Simulates a schedule write landing from another caller by raising .
public void RaiseWeekChanged() => WeekChanged?.Invoke();
/// Arms a gate so the NEXT call returns an incomplete task,
diff --git a/tests/GenWave.Orchestration.Tests/Fakes/FakeStationDefaultEnvelopeSource.cs b/tests/GenWave.TestSupport/Fakes/FakeStationDefaultEnvelopeSource.cs
similarity index 63%
rename from tests/GenWave.Orchestration.Tests/Fakes/FakeStationDefaultEnvelopeSource.cs
rename to tests/GenWave.TestSupport/Fakes/FakeStationDefaultEnvelopeSource.cs
index 267e871b..0b9972c0 100644
--- a/tests/GenWave.Orchestration.Tests/Fakes/FakeStationDefaultEnvelopeSource.cs
+++ b/tests/GenWave.TestSupport/Fakes/FakeStationDefaultEnvelopeSource.cs
@@ -1,13 +1,14 @@
using GenWave.Abstractions.Playout;
using GenWave.Core.Abstractions;
-namespace GenWave.Orchestration.Tests.Fakes;
+namespace GenWave.TestSupport.Fakes;
///
/// Fixed double (STORY-241, PLAN T119) — mirrors
/// one seam over.
///
-sealed class FakeStationDefaultEnvelopeSource(SegmentEnvelope envelope) : IStationDefaultEnvelopeSource
+public sealed class FakeStationDefaultEnvelopeSource(SegmentEnvelope envelope) : IStationDefaultEnvelopeSource
{
+ ///
public SegmentEnvelope Current => envelope;
}
diff --git a/tests/GenWave.Orchestration.Tests/Fakes/FakeStationIdentityProvider.cs b/tests/GenWave.TestSupport/Fakes/FakeStationIdentityProvider.cs
similarity index 68%
rename from tests/GenWave.Orchestration.Tests/Fakes/FakeStationIdentityProvider.cs
rename to tests/GenWave.TestSupport/Fakes/FakeStationIdentityProvider.cs
index ddf42be0..148eb5e2 100644
--- a/tests/GenWave.Orchestration.Tests/Fakes/FakeStationIdentityProvider.cs
+++ b/tests/GenWave.TestSupport/Fakes/FakeStationIdentityProvider.cs
@@ -1,7 +1,7 @@
using GenWave.Core.Abstractions;
using GenWave.Core.Domain;
-namespace GenWave.Orchestration.Tests.Fakes;
+namespace GenWave.TestSupport.Fakes;
///
/// Mutable double (SPEC F44.1, gitea-#196, mirrors
@@ -9,9 +9,11 @@ namespace GenWave.Orchestration.Tests.Fakes;
/// simulate a live IOptionsMonitor<StationOptions> reload without standing up a real
/// options stack in a unit test.
///
-sealed class FakeStationIdentityProvider(StationIdentity identity) : IStationIdentityProvider
+public sealed class FakeStationIdentityProvider(StationIdentity identity) : IStationIdentityProvider
{
+ /// The station identity a spec can mutate between calls.
public StationIdentity Identity { get; set; } = identity;
+ ///
public StationIdentity Current => Identity;
}
diff --git a/tests/GenWave.Orchestration.Tests/Fakes/FakeStationScopeProvider.cs b/tests/GenWave.TestSupport/Fakes/FakeStationScopeProvider.cs
similarity index 65%
rename from tests/GenWave.Orchestration.Tests/Fakes/FakeStationScopeProvider.cs
rename to tests/GenWave.TestSupport/Fakes/FakeStationScopeProvider.cs
index d585a7fa..7e13f8bf 100644
--- a/tests/GenWave.Orchestration.Tests/Fakes/FakeStationScopeProvider.cs
+++ b/tests/GenWave.TestSupport/Fakes/FakeStationScopeProvider.cs
@@ -1,16 +1,18 @@
using GenWave.Core.Abstractions;
using GenWave.Core.Domain;
-namespace GenWave.Orchestration.Tests.Fakes;
+namespace GenWave.TestSupport.Fakes;
///
/// Mutable double. Set between calls to
/// simulate a live IOptionsMonitor<StationOptions> reload (SPEC F30.1) without
/// standing up a real options stack in a unit test.
///
-sealed class FakeStationScopeProvider(LibraryScope scope) : IStationScopeProvider
+public sealed class FakeStationScopeProvider(LibraryScope scope) : IStationScopeProvider
{
+ /// The library scope a spec can mutate between calls.
public LibraryScope Scope { get; set; } = scope;
+ ///
public LibraryScope Current => Scope;
}
diff --git a/tests/GenWave.Orchestration.Tests/Fakes/FakeTtsSegmentSource.cs b/tests/GenWave.TestSupport/Fakes/FakeTtsSegmentSource.cs
similarity index 91%
rename from tests/GenWave.Orchestration.Tests/Fakes/FakeTtsSegmentSource.cs
rename to tests/GenWave.TestSupport/Fakes/FakeTtsSegmentSource.cs
index f70a93b3..0717cfdb 100644
--- a/tests/GenWave.Orchestration.Tests/Fakes/FakeTtsSegmentSource.cs
+++ b/tests/GenWave.TestSupport/Fakes/FakeTtsSegmentSource.cs
@@ -1,7 +1,7 @@
using GenWave.Core.Abstractions;
using GenWave.Core.Domain;
-namespace GenWave.Orchestration.Tests.Fakes;
+namespace GenWave.TestSupport.Fakes;
///
/// Scripted TTS source double for orchestrator unit tests. Returns a pre-built segment with a
@@ -9,10 +9,15 @@ namespace GenWave.Orchestration.Tests.Fakes;
/// When is set the task waits for that duration before completing,
/// simulating a slow render that may exceed the budget.
///
-sealed class FakeTtsSegmentSource : ITtsSegmentSource
+public sealed class FakeTtsSegmentSource : ITtsSegmentSource
{
+ /// When true, every call returns null unconditionally.
public bool AlwaysReturnNull { get; set; }
+
+ /// How many times has been called.
public int RenderCallCount { get; private set; }
+
+ /// The most recent request passed to .
public SegmentRequest? LastRequest { get; private set; }
/// Every request seen, in call order — for specs that assert on a specific
@@ -58,6 +63,7 @@ sealed class FakeTtsSegmentSource : ITtsSegmentSource
///
public Func? ShouldThrow { get; set; }
+ ///
public async Task RenderAsync(SegmentRequest request, CancellationToken ct)
{
RenderCallCount++;
diff --git a/tests/GenWave.TestSupport/GenWave.TestSupport.csproj b/tests/GenWave.TestSupport/GenWave.TestSupport.csproj
new file mode 100644
index 00000000..a738b66b
--- /dev/null
+++ b/tests/GenWave.TestSupport/GenWave.TestSupport.csproj
@@ -0,0 +1,30 @@
+
+
+
+ net10.0
+ enable
+ enable
+ false
+ true
+
+ false
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/tests/GenWave.TestSupport/OrchestratorBuilder.cs b/tests/GenWave.TestSupport/OrchestratorBuilder.cs
new file mode 100644
index 00000000..204b3ad3
--- /dev/null
+++ b/tests/GenWave.TestSupport/OrchestratorBuilder.cs
@@ -0,0 +1,273 @@
+using GenWave.Abstractions.Playout;
+using GenWave.Core.Abstractions;
+using GenWave.Core.Domain;
+using GenWave.Orchestration;
+using GenWave.TestSupport.Fakes;
+using Microsoft.Extensions.Logging;
+using Microsoft.Extensions.Logging.Abstractions;
+using Microsoft.Extensions.Time.Testing;
+
+namespace GenWave.TestSupport;
+
+///
+/// Builds a real one seam at a time (SPEC F184.2, STORY-451, PLAN T511) —
+/// every seam defaults to the same fake already uses, and every
+/// seam has a With* override, so a spec never reaches for the 27-parameter constructor
+/// directly. resolves seams whose default depends on another seam's FINAL value
+/// (the clock feeds tts/deferral-queue/schedule-resolver; the catalog feeds the music selection
+/// policy; the schedule resolver and persona store feed the persona accessor) in dependency order, so
+/// With* calls in any order before always produce a consistent chain.
+///
+///
+/// types every slot to the seam type its With* accepts, so
+/// never throws for an override that compiles. A spec that needs a fake's own
+/// members on a default seam passes its own fake through With* and keeps the reference itself.
+///
+///
+///
+/// defaults to , not a fake: the only
+/// IPatterDurationEstimator double here () always
+/// answers a fixed heuristic, which is NOT behaviorally equivalent to 's own
+/// tiered fallback — defaulting to it would silently change boundary-fit behavior for every spec that
+/// never calls .
+///
+///
+public sealed class OrchestratorBuilder
+{
+ IStationIdentityProvider? identityProvider;
+ IStationScopeProvider? scopeProvider;
+ ICadenceProvider? cadenceProvider;
+ IRotationSettingsProvider? rotationProvider;
+ MusicSelectionPolicy? musicSelectionPolicy;
+ ITtsSegmentSource? tts;
+ IActivePersonaAccessor? personaAccessor;
+ ILogger? logger;
+ IRenderBudgetProvider? renderBudgetProvider;
+ SpeechDeferralQueue? deferralQueue;
+ TimeProvider? timeProvider;
+ IBoundaryBiasProvider? boundaryBiasProvider;
+ CachingScheduleResolver? scheduleResolver;
+ IPersonaStore? personaStore;
+ IStationEventSink? events;
+ IStationClockProvider? stationClock;
+ IPatterDurationEstimator? patterEstimator;
+ IContextSettingsProvider? contextSettings;
+ IMediaCatalog? catalog;
+ IStationImagingSettingsProvider? imagingSettings;
+ CrosstalkPlanner? crosstalkPlanner;
+ IAnnouncementSource? announcementSource;
+ IVerbatimSegmentRenderer? announcementRenderer;
+ ITtsVoiceLister? voiceLister;
+ IAnnouncementCopyWriter? announcementCopyWriter;
+ IAdCadenceProvider? adCadenceProvider;
+ IAdSpotVend? adSpotVend;
+
+ // Only meaningful when scheduleResolver is unset — feeds the default schedule-resolver chain's
+ // FakeScheduleStore (see WithSchedule).
+ ScheduleWeekSnapshot snapshot = new([]);
+ bool scheduleSet;
+
+ /// Every trivial "set the seam, return this" setter below funnels through here — one
+ /// place owns the assign-and-chain boilerplate instead of 27 near-identical bodies.
+ OrchestratorBuilder With(ref T? slot, T? value) where T : class
+ {
+ slot = value;
+ return this;
+ }
+
+ /// Overrides the station identity seam. Defaults to a fixed FakeStationIdentityProvider.
+ public OrchestratorBuilder WithIdentity(IStationIdentityProvider identity) => With(ref identityProvider, identity);
+
+ /// Overrides the library scope seam. Defaults to a fixed FakeStationScopeProvider.
+ public OrchestratorBuilder WithScope(IStationScopeProvider scope) => With(ref scopeProvider, scope);
+
+ /// Overrides the cadence seam. Defaults to a FakeCadenceProvider with every knob off.
+ public OrchestratorBuilder WithCadence(ICadenceProvider cadence) => With(ref cadenceProvider, cadence);
+
+ /// Convenience — wraps in a .
+ public OrchestratorBuilder WithCadence(CadenceConfig cadence) => WithCadence(new FakeCadenceProvider(cadence));
+
+ /// Overrides the rotation settings seam. Defaults to a fixed FakeRotationSettingsProvider.
+ public OrchestratorBuilder WithRotation(IRotationSettingsProvider rotation) => With(ref rotationProvider, rotation);
+
+ /// Overrides the music selection policy seam. Defaults to a real policy over the final catalog.
+ public OrchestratorBuilder WithMusicSelectionPolicy(MusicSelectionPolicy policy) => With(ref musicSelectionPolicy, policy);
+
+ /// Overrides the TTS seam. Defaults to a sharing the final clock.
+ public OrchestratorBuilder WithTts(ITtsSegmentSource ttsSegmentSource) => With(ref tts, ttsSegmentSource);
+
+ /// Overrides the active-persona accessor seam. Defaults to a real accessor over the final schedule resolver and persona store.
+ public OrchestratorBuilder WithPersonaAccessor(IActivePersonaAccessor accessor) => With(ref personaAccessor, accessor);
+
+ /// Overrides the logger seam. Defaults to a .
+ public OrchestratorBuilder WithLogger(ILogger orchestratorLogger) => With(ref logger, orchestratorLogger);
+
+ /// Overrides the render budget seam. Defaults to a fixed 5-second FakeRenderBudgetProvider.
+ public OrchestratorBuilder WithRenderBudget(IRenderBudgetProvider renderBudget) => With(ref renderBudgetProvider, renderBudget);
+
+ /// Convenience — wraps in a .
+ public OrchestratorBuilder WithRenderBudget(TimeSpan budget) => WithRenderBudget(new FakeRenderBudgetProvider(budget));
+
+ /// Overrides the speech deferral queue seam. Defaults to a queue over the final clock.
+ public OrchestratorBuilder WithDeferralQueue(SpeechDeferralQueue queue) => With(ref deferralQueue, queue);
+
+ /// Overrides the clock seam. Defaults to a at a fixed instant.
+ public OrchestratorBuilder WithTime(TimeProvider time) => With(ref timeProvider, time);
+
+ /// Convenience — a at .
+ public OrchestratorBuilder WithNow(DateTimeOffset now) => WithTime(new FakeTimeProvider(now));
+
+ /// Overrides the boundary bias seam. Defaults to a zero-lookahead FakeBoundaryBiasProvider.
+ public OrchestratorBuilder WithBoundaryBias(IBoundaryBiasProvider boundaryBias) => With(ref boundaryBiasProvider, boundaryBias);
+
+ /// Convenience — wraps in a .
+ public OrchestratorBuilder WithLookahead(TimeSpan lookahead) => WithBoundaryBias(new FakeBoundaryBiasProvider(lookahead));
+
+ /// Overrides the schedule resolver seam outright. Exclusive with — throws if both are set.
+ public OrchestratorBuilder WithScheduleResolver(CachingScheduleResolver resolver) => With(ref scheduleResolver, resolver);
+
+ /// Convenience — keeps the default schedule-resolver chain but seeds its with . Exclusive with — throws if both are set.
+ public OrchestratorBuilder WithSchedule(ScheduleWeekSnapshot week)
+ {
+ snapshot = week;
+ scheduleSet = true;
+ return this;
+ }
+
+ /// Overrides the persona store seam. Defaults to an empty .
+ public OrchestratorBuilder WithPersonaStore(IPersonaStore store) => With(ref personaStore, store);
+
+ /// Overrides the station event sink seam. Defaults to a .
+ public OrchestratorBuilder WithEvents(IStationEventSink sink) => With(ref events, sink);
+
+ /// Overrides the station clock seam. Defaults to (feature-dark — Orchestrator falls back to 's own UTC now).
+ public OrchestratorBuilder WithStationClock(IStationClockProvider? clock) => With(ref stationClock, clock);
+
+ /// Overrides the patter duration estimator seam. Defaults to — see this class's own remarks for why the builder never defaults this to a fake.
+ public OrchestratorBuilder WithPatterEstimator(IPatterDurationEstimator? estimator) => With(ref patterEstimator, estimator);
+
+ /// Overrides the per-key context-persona seam. Defaults to (feature-dark).
+ public OrchestratorBuilder WithContextSettings(IContextSettingsProvider? settings) => With(ref contextSettings, settings);
+
+ /// Overrides the media catalog seam. Defaults to a single-track (also feeds the default ).
+ public OrchestratorBuilder WithCatalog(IMediaCatalog mediaCatalog) => With(ref catalog, mediaCatalog);
+
+ /// Overrides the station imaging settings seam. Defaults to (feature-dark).
+ public OrchestratorBuilder WithImagingSettings(IStationImagingSettingsProvider? settings) => With(ref imagingSettings, settings);
+
+ /// Overrides the crosstalk planner seam. Defaults to (feature-dark).
+ public OrchestratorBuilder WithCrosstalkPlanner(CrosstalkPlanner? planner) => With(ref crosstalkPlanner, planner);
+
+ /// Overrides the announcement source seam. Defaults to (feature-dark).
+ public OrchestratorBuilder WithAnnouncementSource(IAnnouncementSource? source) => With(ref announcementSource, source);
+
+ /// Overrides the verbatim announcement renderer seam. Defaults to (feature-dark).
+ public OrchestratorBuilder WithAnnouncementRenderer(IVerbatimSegmentRenderer? renderer) => With(ref announcementRenderer, renderer);
+
+ /// Overrides the TTS voice lister seam. Defaults to (feature-dark).
+ public OrchestratorBuilder WithVoiceLister(ITtsVoiceLister? lister) => With(ref voiceLister, lister);
+
+ /// Overrides the announcement copy writer seam. Defaults to (feature-dark).
+ public OrchestratorBuilder WithAnnouncementCopyWriter(IAnnouncementCopyWriter? writer) => With(ref announcementCopyWriter, writer);
+
+ /// Overrides the ad cadence seam. Defaults to (feature-dark — no ad ever fires).
+ public OrchestratorBuilder WithAdCadence(IAdCadenceProvider? adCadence) => With(ref adCadenceProvider, adCadence);
+
+ /// Overrides the ad spot vend seam. Defaults to (feature-dark — no ad ever fires).
+ public OrchestratorBuilder WithAdSpotVend(IAdSpotVend? vend) => With(ref adSpotVend, vend);
+
+ ///
+ /// Resolves every unset seam to its default and constructs the Orchestrator, returning it alongside
+ /// every collaborator a spec has ever needed to assert on directly.
+ ///
+ public OrchestratorChain Build()
+ {
+ if (scheduleResolver is not null && scheduleSet)
+ {
+ throw new InvalidOperationException("WithSchedule and WithScheduleResolver are exclusive");
+ }
+
+ var resolvedTime = timeProvider ?? new FakeTimeProvider(new DateTimeOffset(2026, 1, 1, 0, 0, 0, TimeSpan.Zero));
+
+ var resolvedCatalog = catalog ?? new FakeMediaCatalog(TestData.MakeTrackRef("t1"));
+
+ var resolvedMusicSelectionPolicy = musicSelectionPolicy
+ ?? new MusicSelectionPolicy(resolvedCatalog, NullLogger.Instance);
+
+ var resolvedPersonaStore = personaStore ?? new FakePersonaStore();
+
+ var resolvedScheduleResolver = scheduleResolver;
+ if (resolvedScheduleResolver is null)
+ {
+ var scheduleStore = new FakeScheduleStore(snapshot);
+ var stationDefault = new FakeStationDefaultEnvelopeSource(SegmentEnvelope.StationDefault);
+ var resolver = new ScheduleResolver(resolvedTime, stationDefault);
+ resolvedScheduleResolver = new CachingScheduleResolver(scheduleStore, resolver, new FakeScheduleSpecialStore());
+ }
+
+ var resolvedPersonaAccessor = personaAccessor
+ ?? new OnAirPersonaAccessor(resolvedScheduleResolver, resolvedPersonaStore, NullLogger.Instance);
+
+ // The render double's own RenderDelay rides the SAME fake clock as the render budget (STORY-442,
+ // PLAN T483) — mirrors ProductionChainHarness's own wiring.
+ var resolvedTts = tts ?? new FakeTtsSegmentSource { TimeProvider = resolvedTime };
+
+ var resolvedEvents = events ?? new CapturingStationEventSink();
+
+ var resolvedLogger = logger ?? new CapturingLogger();
+
+ var resolvedDeferralQueue = deferralQueue ?? new SpeechDeferralQueue(resolvedTime);
+
+ var resolvedIdentityProvider = identityProvider
+ ?? new FakeStationIdentityProvider(new StationIdentity("s1", "GenWave", "default"));
+ var resolvedScopeProvider = scopeProvider ?? new FakeStationScopeProvider(new LibraryScope([1L]));
+ var resolvedCadenceProvider = cadenceProvider ?? new FakeCadenceProvider(new CadenceConfig
+ {
+ LeadInBeforeEachTrack = false,
+ BackAnnounceAfterEachTrack = false,
+ StationIdEveryNUnits = 0,
+ });
+ var resolvedRotationProvider = rotationProvider ?? new FakeRotationSettingsProvider(new RotationSettings());
+ var resolvedRenderBudgetProvider = renderBudgetProvider ?? new FakeRenderBudgetProvider(TimeSpan.FromSeconds(5));
+ var resolvedBoundaryBiasProvider = boundaryBiasProvider ?? new FakeBoundaryBiasProvider(TimeSpan.Zero);
+
+ var orchestrator = new Orchestrator(
+ resolvedIdentityProvider,
+ resolvedScopeProvider,
+ resolvedCadenceProvider,
+ resolvedRotationProvider,
+ resolvedMusicSelectionPolicy,
+ resolvedTts,
+ resolvedPersonaAccessor,
+ resolvedLogger,
+ resolvedRenderBudgetProvider,
+ resolvedDeferralQueue,
+ resolvedTime,
+ resolvedBoundaryBiasProvider,
+ scheduleResolver: resolvedScheduleResolver,
+ personaStore: resolvedPersonaStore,
+ events: resolvedEvents,
+ stationClock: stationClock,
+ patterEstimator: patterEstimator,
+ contextSettings: contextSettings,
+ catalog: resolvedCatalog,
+ imagingSettings: imagingSettings,
+ crosstalkPlanner: crosstalkPlanner,
+ announcementSource: announcementSource,
+ announcementRenderer: announcementRenderer,
+ voiceLister: voiceLister,
+ announcementCopyWriter: announcementCopyWriter,
+ adCadenceProvider: adCadenceProvider,
+ adSpotVend: adSpotVend);
+
+ return new OrchestratorChain(
+ orchestrator,
+ resolvedDeferralQueue,
+ resolvedTime,
+ resolvedTts,
+ resolvedEvents,
+ resolvedLogger,
+ resolvedCatalog,
+ patterEstimator);
+ }
+}
diff --git a/tests/GenWave.TestSupport/OrchestratorChain.cs b/tests/GenWave.TestSupport/OrchestratorChain.cs
new file mode 100644
index 00000000..e0061299
--- /dev/null
+++ b/tests/GenWave.TestSupport/OrchestratorChain.cs
@@ -0,0 +1,27 @@
+using GenWave.Core.Abstractions;
+using GenWave.Orchestration;
+using Microsoft.Extensions.Logging;
+
+namespace GenWave.TestSupport;
+
+///
+/// What returns (SPEC F184.2, STORY-451, PLAN T511) — the
+/// Orchestrator plus every collaborator a spec has ever needed to reach into directly, so no spec
+/// has to fall back to a constructor call to get at them.
+///
+///
+/// Every slot is typed to the seam type the matching With* method accepts, so Build()
+/// never throws for an override that compiles. A spec that needs a fake's own members on a default
+/// seam (e.g. FakeTimeProvider.Advance) passes its own fake through the matching With*
+/// and keeps a reference to it — the default-typed value here stays widened to the interface.
+///
+///
+public sealed record OrchestratorChain(
+ Orchestrator Orchestrator,
+ SpeechDeferralQueue Queue,
+ TimeProvider Time,
+ ITtsSegmentSource Tts,
+ IStationEventSink Events,
+ ILogger Logger,
+ IMediaCatalog Catalog,
+ IPatterDurationEstimator? PatterEstimator);
diff --git a/tests/GenWave.TestSupport/TestData.cs b/tests/GenWave.TestSupport/TestData.cs
new file mode 100644
index 00000000..f3176212
--- /dev/null
+++ b/tests/GenWave.TestSupport/TestData.cs
@@ -0,0 +1,46 @@
+using GenWave.Core.Domain;
+using GenWave.TestSupport.Fakes;
+
+namespace GenWave.TestSupport;
+
+///
+/// Small scripted-value factories shared across Orchestrator specs (moved out of
+/// GenWave.Orchestration.Tests.Fakes.ProductionChainHarness at PLAN T511 so
+/// 's own defaults can use them too, without either side
+/// depending on the other).
+///
+public static class TestData
+{
+ /// Builds a persona with the given id/name/voice; every other field a fixed,
+ /// arbitrary-but-stable value no spec has ever needed to vary.
+ public static Persona MakePersona(long id, string name, string voice)
+ {
+ var now = DateTime.UnixEpoch;
+ return new Persona(id, name, "", "", voice, now, now);
+ }
+
+ /// Builds a playable for the given id; every other
+ /// field a fixed, arbitrary-but-stable value no spec has ever needed to vary.
+ public static MediaReference MakeTrackRef(string id) => new(
+ MediaId: id,
+ Locator: $"/media/{id}.mp3",
+ Title: $"Track {id}",
+ Loudness: new Loudness(-23.0, -1.0, true),
+ DurationMs: null,
+ SampleRate: null,
+ Channels: null,
+ BitrateKbps: null,
+ Artist: null,
+ Album: null,
+ Genre: null,
+ Year: null);
+
+ /// A seeded with exactly one persona — the common
+ /// single-DJ-station shape most specs need.
+ public static FakePersonaStore OneDjStore(long id, string name, string voice)
+ {
+ var store = new FakePersonaStore();
+ store.Add(MakePersona(id, name, voice));
+ return store;
+ }
+}
diff --git a/tests/GenWave.Tts.Tests/Specs/Story456_SnapshotDrivesTheRender.cs b/tests/GenWave.Tts.Tests/Specs/Story456_SnapshotDrivesTheRender.cs
new file mode 100644
index 00000000..16bb527a
--- /dev/null
+++ b/tests/GenWave.Tts.Tests/Specs/Story456_SnapshotDrivesTheRender.cs
@@ -0,0 +1,75 @@
+// STORY-456 — The speaker travels with the plan — the render branch (gh-#772 · SPEC F189.3, F189.6 · PLAN T525, T526)
+//
+// BDD specification — xUnit. AC2–AC5 drive TtsSegmentSource with a recording synthesizer and ambient caches that throw on refresh; AC12 drives
+// the Tts SpeakerSnapshotSource with an unknown id.
+//
+// RED at plan time: every fact is [Fact(Skip = Pending)] with a loud body — remove the Skip only in the
+// task that makes it green. The builder comments name the arrange each scenario needs.
+
+namespace GenWave.Tts.Tests.Specs;
+
+public static class FeatureSnapshotDrivesTheRender
+{
+ const string Pending = "pending: T526 — TtsSegmentSource.RenderAsync branches on SegmentRequest.Speaker (STORY-456)";
+
+ // ---------------------------------------------------------------------
+ // HAPPY PATH
+ // ---------------------------------------------------------------------
+
+ public sealed class ScenarioARequestWithASnapshot
+ {
+ // Given: Speaker pace 1.2 and one pronunciation rule
+
+ /// AC2 — the synthesizer received 1.2
+ [Fact(Skip = Pending)]
+ public void SendsTheSnapshotPace() => Assert.Fail(Pending);
+
+ /// AC2 — the rule rewrote the copy
+ [Fact(Skip = Pending)]
+ public void AppliesTheSnapshotRule() => Assert.Fail(Pending);
+ }
+
+ public sealed class ScenarioARequestWithASnapshotAndThrowingCaches
+ {
+ // Given: ambient caches throw on refresh
+
+ /// AC3 — the snapshot bypasses the caches
+ [Fact(Skip = Pending)]
+ public void RendersSuccessfully() => Assert.Fail(Pending);
+ }
+
+ public sealed class ScenarioTwoSnapshotsSameText
+ {
+ // Given: identical text, ContentHash differs
+
+ /// AC4 — the cache key carries the snapshot
+ [Fact(Skip = Pending)]
+ public void SynthesizesTwice() => Assert.Fail(Pending);
+ }
+
+ public sealed class ScenarioARequestWithoutASnapshot
+ {
+ // Given: Speaker null, ambient caches at pace 0.9
+
+ /// AC5 — today's path unchanged
+ [Fact(Skip = Pending)]
+ public void SendsTheAmbientPace() => Assert.Fail(Pending);
+ }
+
+ // ---------------------------------------------------------------------
+ // SAD PATH — segregated
+ // ---------------------------------------------------------------------
+
+ public sealed class ScenarioAnUnknownPersonaId
+ {
+ // Given: the snapshot source resolves an id that does not exist
+
+ /// AC12 —
+ [Fact(Skip = Pending)]
+ public void FallsBackToTheStationSnapshot() => Assert.Fail(Pending);
+
+ /// AC12 —
+ [Fact(Skip = Pending)]
+ public void LogsOneWarnNamingTheId() => Assert.Fail(Pending);
+ }
+}