fix(focas): fail-fast at init on unimplemented backend (operator footgun)
Add IFocasClientFactory.EnsureUsable() — a config-time probe called by FocasDriver.InitializeAsync before any background loops start. The UnimplementedFocasClientFactory throws NotSupportedException immediately (faulting the driver at init), eliminating the footgun where a driver on the 'unimplemented' backend appeared Healthy then failed every read/write/ subscribe silently. WireFocasClientFactory and FakeFocasClientFactory are no-ops. Backstop Create() throw remains in place.
This commit is contained in:
@@ -169,6 +169,9 @@ internal sealed class FakeFocasClientFactory : IFocasClientFactory
|
||||
/// <summary>Gets or sets a customization function for creating clients.</summary>
|
||||
public Func<FakeFocasClient>? Customise { get; set; }
|
||||
|
||||
/// <summary>No-op usability probe — the fake factory is always usable.</summary>
|
||||
public void EnsureUsable() { }
|
||||
|
||||
/// <summary>Creates a fake FOCAS client.</summary>
|
||||
public IFocasClient Create()
|
||||
{
|
||||
|
||||
@@ -261,4 +261,36 @@ public sealed class FocasScaffoldingTests
|
||||
ex.Message.ShouldContain("wire");
|
||||
ex.Message.ShouldContain("docs/drivers/FOCAS.md");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies a driver configured with the unimplemented backend faults at
|
||||
/// InitializeAsync — not lazily on the first read. The operator must get an
|
||||
/// actionable error immediately rather than a phantom-Healthy driver that
|
||||
/// fails every read silently.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task Unimplemented_backend_faults_at_InitializeAsync_not_on_first_read()
|
||||
{
|
||||
var drv = new FocasDriver(
|
||||
new FocasDriverOptions { Devices = [new FocasDeviceOptions("focas://10.0.0.5:8193")] },
|
||||
"drv-unimpl",
|
||||
clientFactory: new UnimplementedFocasClientFactory());
|
||||
|
||||
var ex = await Should.ThrowAsync<NotSupportedException>(
|
||||
() => drv.InitializeAsync("{}", CancellationToken.None));
|
||||
|
||||
ex.Message.ShouldContain("unimplemented");
|
||||
drv.GetHealth().State.ShouldBe(DriverState.Faulted);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies WireFocasClientFactory.EnsureUsable is a no-op — the wire backend
|
||||
/// must still initialize clean (no false-positive faults from the probe).
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Wire_factory_EnsureUsable_does_not_throw()
|
||||
{
|
||||
var factory = new Wire.WireFocasClientFactory();
|
||||
Should.NotThrow(() => factory.EnsureUsable());
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user