fix(raw): a stale open can no longer feed the picker another device's config
This commit is contained in:
@@ -170,6 +170,11 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
var dto = await Svc.LoadTagForEditAsync(TagId);
|
var dto = await Svc.LoadTagForEditAsync(TagId);
|
||||||
|
// Two opens can overlap on one modal instance (the tree's Edit command is synchronous and does
|
||||||
|
// not disable while this resolves), so every post-await write is discarded unless this open is
|
||||||
|
// still the displayed one. Last-completion-wins would point the picker at another device.
|
||||||
|
if (_loadedKey != key || !Visible) { return; }
|
||||||
|
|
||||||
// RawTagEditDto is a record struct → a missing tag comes back as default (all-zero), not null.
|
// RawTagEditDto is a record struct → a missing tag comes back as default (all-zero), not null.
|
||||||
if (dto is not { } d || string.IsNullOrEmpty(d.TagId))
|
if (dto is not { } d || string.IsNullOrEmpty(d.TagId))
|
||||||
{
|
{
|
||||||
@@ -192,6 +197,8 @@
|
|||||||
try
|
try
|
||||||
{
|
{
|
||||||
var merged = await Svc.LoadMergedProbeConfigAsync(d.DeviceId);
|
var merged = await Svc.LoadMergedProbeConfigAsync(d.DeviceId);
|
||||||
|
if (_loadedKey != key || !Visible) { return; } // superseded open → never install its device
|
||||||
|
|
||||||
if (merged is { } m)
|
if (merged is { } m)
|
||||||
{
|
{
|
||||||
_mergedConfigJson = m.MergedConfigJson;
|
_mergedConfigJson = m.MergedConfigJson;
|
||||||
|
|||||||
@@ -20,14 +20,26 @@ internal sealed class FakeRawTreeService : IRawTreeService
|
|||||||
public Func<string, (string DriverType, string MergedConfigJson)?> OnLoadMergedProbeConfig { get; set; } =
|
public Func<string, (string DriverType, string MergedConfigJson)?> OnLoadMergedProbeConfig { get; set; } =
|
||||||
_ => throw new NotImplementedException();
|
_ => throw new NotImplementedException();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Optional asynchronous override of <see cref="OnLoadTagForEdit"/>. The sync delegates above
|
||||||
|
/// complete inline, which cannot express two opens overlapping on one modal instance — set this to
|
||||||
|
/// gate a load on a <see cref="TaskCompletionSource"/> and interleave the second open.
|
||||||
|
/// </summary>
|
||||||
|
public Func<string, Task<RawTagEditDto?>>? OnLoadTagForEditAsync { get; set; }
|
||||||
|
|
||||||
|
/// <summary>Optional asynchronous override of <see cref="OnLoadMergedProbeConfig"/>; see <see cref="OnLoadTagForEditAsync"/>.</summary>
|
||||||
|
public Func<string, Task<(string DriverType, string MergedConfigJson)?>>? OnLoadMergedProbeConfigAsync { get; set; }
|
||||||
|
|
||||||
/// <inheritdoc/>
|
/// <inheritdoc/>
|
||||||
public Task<RawTagEditDto?> LoadTagForEditAsync(string tagId, CancellationToken ct = default)
|
public Task<RawTagEditDto?> LoadTagForEditAsync(string tagId, CancellationToken ct = default)
|
||||||
=> Task.FromResult(OnLoadTagForEdit(tagId));
|
=> OnLoadTagForEditAsync is { } async ? async(tagId) : Task.FromResult(OnLoadTagForEdit(tagId));
|
||||||
|
|
||||||
/// <inheritdoc/>
|
/// <inheritdoc/>
|
||||||
public Task<(string DriverType, string MergedConfigJson)?> LoadMergedProbeConfigAsync(
|
public Task<(string DriverType, string MergedConfigJson)?> LoadMergedProbeConfigAsync(
|
||||||
string deviceId, CancellationToken ct = default)
|
string deviceId, CancellationToken ct = default)
|
||||||
=> Task.FromResult(OnLoadMergedProbeConfig(deviceId));
|
=> OnLoadMergedProbeConfigAsync is { } async
|
||||||
|
? async(deviceId)
|
||||||
|
: Task.FromResult(OnLoadMergedProbeConfig(deviceId));
|
||||||
|
|
||||||
/// <inheritdoc/>
|
/// <inheritdoc/>
|
||||||
public Task<IReadOnlyList<RawNode>> LoadRootsAsync(CancellationToken ct = default)
|
public Task<IReadOnlyList<RawNode>> LoadRootsAsync(CancellationToken ct = default)
|
||||||
|
|||||||
@@ -33,6 +33,9 @@ public sealed class RawTagModalMergedConfigTests
|
|||||||
{
|
{
|
||||||
private const string MergedConfig = """{"endpoint":"10.0.0.1:44818","slot":0}""";
|
private const string MergedConfig = """{"endpoint":"10.0.0.1:44818","slot":0}""";
|
||||||
|
|
||||||
|
/// <summary>A second device's config — the blob a superseded open must never install.</summary>
|
||||||
|
private const string StaleConfig = """{"endpoint":"10.0.0.9:44818","slot":3}""";
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task The_editor_receives_the_merged_device_config_not_a_stub()
|
public async Task The_editor_receives_the_merged_device_config_not_a_stub()
|
||||||
{
|
{
|
||||||
@@ -98,6 +101,37 @@ public sealed class RawTagModalMergedConfigTests
|
|||||||
firstAccessor().ShouldBe(MergedConfig);
|
firstAccessor().ShouldBe(MergedConfig);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task A_stale_open_never_feeds_the_picker_another_devices_config()
|
||||||
|
{
|
||||||
|
// RawTree's OnEditTag is synchronous and does not disable while the modal resolves, so a rapid
|
||||||
|
// double-click across two tag rows overlaps two opens on ONE modal instance. Every write after an
|
||||||
|
// await must therefore be discarded if the open it belongs to is no longer the displayed one:
|
||||||
|
// last-completion-wins would silently point a live browse at the wrong controller.
|
||||||
|
var gate = new TaskCompletionSource<(string DriverType, string MergedConfigJson)?>();
|
||||||
|
var svc = new FakeRawTreeService
|
||||||
|
{
|
||||||
|
OnLoadTagForEdit = tagId => TagOn(tagId, tagId == "tag-1" ? "dev-A" : "dev-B"),
|
||||||
|
OnLoadMergedProbeConfigAsync = deviceId => deviceId == "dev-A"
|
||||||
|
? gate.Task
|
||||||
|
: Task.FromResult<(string DriverType, string MergedConfigJson)?>(("AbCip", MergedConfig)),
|
||||||
|
};
|
||||||
|
|
||||||
|
var modal = NewModal(svc, "tag-1");
|
||||||
|
var firstOpen = InvokeParametersSetAsync(modal);
|
||||||
|
|
||||||
|
// The second open supersedes the first while the first is still parked on dev-A's resolve.
|
||||||
|
modal.TagId = "tag-2";
|
||||||
|
await InvokeParametersSetAsync(modal);
|
||||||
|
|
||||||
|
gate.SetResult(("AbCip", StaleConfig));
|
||||||
|
await firstOpen;
|
||||||
|
|
||||||
|
DriverConfigJson(modal).ShouldBe(
|
||||||
|
MergedConfig,
|
||||||
|
"the superseded open's dev-A config overwrote the displayed tag's dev-B config");
|
||||||
|
}
|
||||||
|
|
||||||
private static RawTagEditDto TagOn(string tagId, string deviceId) => new(
|
private static RawTagEditDto TagOn(string tagId, string deviceId) => new(
|
||||||
tagId,
|
tagId,
|
||||||
deviceId,
|
deviceId,
|
||||||
|
|||||||
Reference in New Issue
Block a user