Overview
TestAdapter currently records only adapter lifecycle calls (InitializeAsync, ShutdownAsync). All provider properties — RenderProvider, InputProvider, AssetProvider, AudioPlayer — delegate directly to the inner HeadlessAdapter without recording.
This issue adds four internal recording decorator classes that intercept every provider method call, append a RecordedCall entry to TestAdapter's shared list, then delegate to the inner provider. It also adds a public Inner property so test code can access provider-specific typed collections (for example HeadlessRenderProvider.RecordedSprites).
Plan issue
Plan status
Not started
Definition of terms
| Term |
Meaning |
| Recording decorator |
A class that wraps a provider interface, forwards all calls to an inner implementation, and appends a RecordedCall entry to a shared list for each call. |
RecordedCall |
A record DTO with ProviderName, MethodName, and IReadOnlyList<object?> Arguments already defined in the codebase. |
Architectural considerations and constraints
- Shared list: All four decorators write to the same
List<RecordedCall> owned by TestAdapter, allowing cross-provider sequence assertions.
- Internal visibility: Decorator classes are
internal sealed — implementation details not exposed as public API.
- Additive, not replacement:
HeadlessAudioPlayer and HeadlessRenderProvider already have their own typed recording collections. The TestAdapter layer provides a unified call log alongside those, not instead of them.
IUserInterfaceProvider is empty: No decorator is required until the interface is finalised; UserInterfaceProvider continues to delegate directly.
Inner property: Exposes the inner HeadlessAdapter so test code can access provider-specific typed recording collections where needed.
- All existing
TestAdapter tests must continue to pass without modification.
Implementation guide
Plan requirements
-
(Not started) TestAdapter records render provider calls
- GIVEN a
TestAdapter
- WHEN
RenderProvider.SubmitSprite(dto) is called
- THEN
RecordedCalls contains a RecordedCall("Render", "SubmitSprite", [dto]) entry
-
(Not started) TestAdapter records audio player calls
- GIVEN a
TestAdapter
- WHEN
AudioPlayer.StartPlayback("sfx", false) is called
- THEN
RecordedCalls contains a RecordedCall("Audio", "StartPlayback", ["sfx", false]) entry
-
(Not started) TestAdapter records asset provider calls
- GIVEN a
TestAdapter
- WHEN
AssetProvider.GetAsset("id") is called
- THEN
RecordedCalls contains a RecordedCall("Asset", "GetAsset", ["id"]) entry
-
(Not started) TestAdapter records input provider calls
- GIVEN a
TestAdapter
- WHEN
InputProvider.IsKeyDown("Space") is called
- THEN
RecordedCalls contains a RecordedCall("Input", "IsKeyDown", ["Space"]) entry
-
(Not started) Lifecycle calls remain in order alongside provider calls
- GIVEN a
TestAdapter
- WHEN
InitializeAsync, then RenderProvider.SubmitSprite, then ShutdownAsync are called in sequence
- THEN
RecordedCalls contains all three entries in that order
-
(Not started) Inner adapter is accessible for provider-level inspection
- GIVEN a
TestAdapter
- WHEN
adapter.Inner.RenderProvider is cast to HeadlessRenderProvider
- THEN
RecordedSprites reflects sprite submissions made via the recording decorator
Phase 1 — Create four decorator classes
Not started
Objective
Create RecordingRenderProvider, RecordingInputProvider, RecordingAssetProvider, and RecordingAudioPlayer in src/GameEngineAdapter.Headless/.
Each class: accepts an inner provider and a List<RecordedCall> via primary constructor; for each interface method, appends to the list then delegates to the inner.
Technical details
ProviderName values per decorator:
| Decorator |
ProviderName |
RecordingRenderProvider |
"Render" |
RecordingInputProvider |
"Input" |
RecordingAssetProvider |
"Asset" |
RecordingAudioPlayer |
"Audio" |
| Adapter lifecycle (existing) |
"Adapter" |
Phase requirements
- (Not started) All four decorator classes compile
- GIVEN the new source files
- WHEN
dotnet build is run
- THEN the solution builds without errors
Examples
// src/GameEngineAdapter.Headless/RecordingRenderProvider.cs
internal sealed class RecordingRenderProvider(
IRenderProvider inner,
List<RecordedCall> calls) : IRenderProvider
{
public FrameScope BeginFrame(in CameraDescriptor camera)
{
calls.Add(new RecordedCall("Render", "BeginFrame", [camera]));
return inner.BeginFrame(in camera);
}
public void SubmitSprite(in SpriteDrawDto dto)
{
calls.Add(new RecordedCall("Render", "SubmitSprite", [dto]));
inner.SubmitSprite(in dto);
}
public void SubmitText(in TextDrawDto dto)
{
calls.Add(new RecordedCall("Render", "SubmitText", [dto]));
inner.SubmitText(in dto);
}
public void SubmitMesh(in MeshDrawDto dto)
{
calls.Add(new RecordedCall("Render", "SubmitMesh", [dto]));
inner.SubmitMesh(in dto);
}
public void EndFrame()
{
calls.Add(new RecordedCall("Render", "EndFrame", []));
inner.EndFrame();
}
public void Present()
{
calls.Add(new RecordedCall("Render", "Present", []));
inner.Present();
}
}
// src/GameEngineAdapter.Headless/RecordingAudioPlayer.cs
internal sealed class RecordingAudioPlayer(
IAudioPlayer inner,
List<RecordedCall> calls) : IAudioPlayer
{
public void StartPlayback(string audioAssetId, bool loopPlayback = false)
{
calls.Add(new RecordedCall("Audio", "StartPlayback", [audioAssetId, loopPlayback]));
inner.StartPlayback(audioAssetId, loopPlayback);
}
public void StopPlayback(string audioAssetId)
{
calls.Add(new RecordedCall("Audio", "StopPlayback", [audioAssetId]));
inner.StopPlayback(audioAssetId);
}
public void SetVolume(string audioAssetId, float volume)
{
calls.Add(new RecordedCall("Audio", "SetVolume", [audioAssetId, volume]));
inner.SetVolume(audioAssetId, volume);
}
}
Phase 2 — Update TestAdapter to use decorators
Not started
Objective
Wire the four decorators into TestAdapter: add private fields, construct them in the constructor, override provider properties to return decorators, add the Inner property.
Technical details
Changes to TestAdapter.cs:
- Add
private readonly RecordingRenderProvider _renderProvider; (and three equivalent fields).
- Add
public HeadlessAdapter Inner => _inner;.
- In the constructor, initialise each decorator:
_renderProvider = new RecordingRenderProvider(_inner.RenderProvider, _recordedCalls);
- Change each provider property to return the decorator:
public IRenderProvider RenderProvider => _renderProvider;
Examples
// src/GameEngineAdapter.Headless/TestAdapter.cs (updated)
public sealed class TestAdapter : IEngineAdapter
{
private readonly HeadlessAdapter _inner;
private readonly List<RecordedCall> _recordedCalls = [];
private readonly RecordingRenderProvider _renderProvider;
private readonly RecordingInputProvider _inputProvider;
private readonly RecordingAssetProvider _assetProvider;
private readonly RecordingAudioPlayer _audioPlayer;
public IReadOnlyList<RecordedCall> RecordedCalls => _recordedCalls;
public HeadlessAdapter Inner => _inner;
public EngineCapabilities Capabilities => _inner.Capabilities;
public IRenderProvider RenderProvider => _renderProvider;
public IInputProvider InputProvider => _inputProvider;
public IUserInterfaceProvider UserInterfaceProvider => _inner.UserInterfaceProvider;
public IAssetProvider AssetProvider => _assetProvider;
public IAudioPlayer AudioPlayer => _audioPlayer;
public TestAdapter(EngineConfig config)
{
_inner = new HeadlessAdapter(config);
_renderProvider = new RecordingRenderProvider(_inner.RenderProvider, _recordedCalls);
_inputProvider = new RecordingInputProvider(_inner.InputProvider, _recordedCalls);
_assetProvider = new RecordingAssetProvider(_inner.AssetProvider, _recordedCalls);
_audioPlayer = new RecordingAudioPlayer(_inner.AudioPlayer, _recordedCalls);
}
public Task InitializeAsync(EngineConfig config, CancellationToken ct = default)
{
_recordedCalls.Add(new RecordedCall("Adapter", "InitializeAsync", [config]));
return _inner.InitializeAsync(config, ct);
}
public Task ShutdownAsync(CancellationToken ct = default)
{
_recordedCalls.Add(new RecordedCall("Adapter", "ShutdownAsync", []));
return _inner.ShutdownAsync(ct);
}
public void Dispose() => _inner.Dispose();
}
Testing and compatibility
Unit tests required per decorator:
- Correct
ProviderName and MethodName in recorded entries.
- Arguments captured accurately (for both
in value-type parameters and reference parameters).
- Delegation verified — inner method is called; return values forwarded for read methods.
- All existing
TestAdapter tests continue to pass without modification.
Performance: Decorators allocate one RecordedCall per call (argument array via collection expression [...]). This is acceptable in test contexts. No additional allocations beyond the RecordedCall itself.
See also
Overview
TestAdaptercurrently records only adapter lifecycle calls (InitializeAsync,ShutdownAsync). All provider properties —RenderProvider,InputProvider,AssetProvider,AudioPlayer— delegate directly to the innerHeadlessAdapterwithout recording.This issue adds four internal recording decorator classes that intercept every provider method call, append a
RecordedCallentry toTestAdapter's shared list, then delegate to the inner provider. It also adds a publicInnerproperty so test code can access provider-specific typed collections (for exampleHeadlessRenderProvider.RecordedSprites).Plan issue
Plan status
Not started
Definition of terms
RecordedCallentry to a shared list for each call.RecordedCallProviderName,MethodName, andIReadOnlyList<object?> Argumentsalready defined in the codebase.Architectural considerations and constraints
List<RecordedCall>owned byTestAdapter, allowing cross-provider sequence assertions.internal sealed— implementation details not exposed as public API.HeadlessAudioPlayerandHeadlessRenderProvideralready have their own typed recording collections. TheTestAdapterlayer provides a unified call log alongside those, not instead of them.IUserInterfaceProvideris empty: No decorator is required until the interface is finalised;UserInterfaceProvidercontinues to delegate directly.Innerproperty: Exposes the innerHeadlessAdapterso test code can access provider-specific typed recording collections where needed.TestAdaptertests must continue to pass without modification.Implementation guide
Plan requirements
(Not started)
TestAdapterrecords render provider callsTestAdapterRenderProvider.SubmitSprite(dto)is calledRecordedCallscontains aRecordedCall("Render", "SubmitSprite", [dto])entry(Not started)
TestAdapterrecords audio player callsTestAdapterAudioPlayer.StartPlayback("sfx", false)is calledRecordedCallscontains aRecordedCall("Audio", "StartPlayback", ["sfx", false])entry(Not started)
TestAdapterrecords asset provider callsTestAdapterAssetProvider.GetAsset("id")is calledRecordedCallscontains aRecordedCall("Asset", "GetAsset", ["id"])entry(Not started)
TestAdapterrecords input provider callsTestAdapterInputProvider.IsKeyDown("Space")is calledRecordedCallscontains aRecordedCall("Input", "IsKeyDown", ["Space"])entry(Not started) Lifecycle calls remain in order alongside provider calls
TestAdapterInitializeAsync, thenRenderProvider.SubmitSprite, thenShutdownAsyncare called in sequenceRecordedCallscontains all three entries in that order(Not started) Inner adapter is accessible for provider-level inspection
TestAdapteradapter.Inner.RenderProvideris cast toHeadlessRenderProviderRecordedSpritesreflects sprite submissions made via the recording decoratorPhase 1 — Create four decorator classes
Not started
Objective
Create
RecordingRenderProvider,RecordingInputProvider,RecordingAssetProvider, andRecordingAudioPlayerinsrc/GameEngineAdapter.Headless/.Each class: accepts an inner provider and a
List<RecordedCall>via primary constructor; for each interface method, appends to the list then delegates to the inner.Technical details
ProviderNamevalues per decorator:ProviderNameRecordingRenderProvider"Render"RecordingInputProvider"Input"RecordingAssetProvider"Asset"RecordingAudioPlayer"Audio""Adapter"Phase requirements
dotnet buildis runExamples
Phase 2 — Update TestAdapter to use decorators
Not started
Objective
Wire the four decorators into
TestAdapter: add private fields, construct them in the constructor, override provider properties to return decorators, add theInnerproperty.Technical details
Changes to
TestAdapter.cs:private readonly RecordingRenderProvider _renderProvider;(and three equivalent fields).public HeadlessAdapter Inner => _inner;._renderProvider = new RecordingRenderProvider(_inner.RenderProvider, _recordedCalls);public IRenderProvider RenderProvider => _renderProvider;Examples
Testing and compatibility
Unit tests required per decorator:
ProviderNameandMethodNamein recorded entries.invalue-type parameters and reference parameters).TestAdaptertests continue to pass without modification.Performance: Decorators allocate one
RecordedCallper call (argument array via collection expression[...]). This is acceptable in test contexts. No additional allocations beyond theRecordedCallitself.See also
DeterministicEngineRunner— SimulationTime, tick callback, configurable camera (docs/plans/phase-2-completion.mdPhase 2) #15