docs(plan): implementation plan for the raw-modal picker live-config fix
This commit is contained in:
@@ -0,0 +1,555 @@
|
||||
# Raw-modal picker live config — implementation plan
|
||||
|
||||
> **For Claude:** REQUIRED SUB-SKILL: Use superpowers-extended-cc:executing-plans to implement this plan task-by-task.
|
||||
|
||||
**Goal:** Replace the `() => "{}"` stub `GetDriverConfigJson` in `RawTagModal` and
|
||||
`RawManualTagEntryModal` with the merged Driver+Device config, so the AbCip/TwinCAT/FOCAS
|
||||
address pickers' "Browse controller" button captures against the real device.
|
||||
|
||||
**Architecture:** Each modal resolves `IRawTreeService.LoadMergedProbeConfigAsync(deviceId)` once
|
||||
per open (the same merge Test-connect and `RawBrowseModal` dial), caches the JSON in a private
|
||||
field, and passes a **live** `Func<string>` that reads the cache with a `"{}"` fallback. Fail-soft:
|
||||
a failed resolve leaves the stub behaviour (graceful error chip in the picker), never blocks the
|
||||
modal. Design: `docs/plans/2026-08-05-raw-modal-picker-live-config-design.md`.
|
||||
|
||||
**Tech Stack:** Blazor Server (razor component classes tested directly — no bUnit; the AdminUI
|
||||
csproj has `InternalsVisibleTo("ZB.MOM.WW.OtOpcUa.AdminUI.Tests")`), xUnit + Shouldly, hand-written
|
||||
fakes (no mocking library in the test project).
|
||||
|
||||
**Key facts for the implementer (zero-context):**
|
||||
|
||||
- The two stubs are at `RawTagModal.razor` `BuildEditorParameters()` (~line 190) and
|
||||
`RawManualTagEntryModal.razor` `BuildEditorParameters(RowModel)` (~line 182):
|
||||
`["GetDriverConfigJson"] = (Func<string>)(() => "{}")`.
|
||||
- Both modals already `@inject IRawTreeService Svc`. The resolver exists:
|
||||
`Task<(string DriverType, string MergedConfigJson)?> LoadMergedProbeConfigAsync(string deviceId, CancellationToken ct = default)`
|
||||
(`src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Uns/IRawTreeService.cs:396`).
|
||||
- `RawTagModal` learns its device from `RawTagEditDto.DeviceId` (returned by
|
||||
`Svc.LoadTagForEditAsync(TagId)` in `OnParametersSetAsync`). `RawManualTagEntryModal` takes
|
||||
`DeviceId` as a `[Parameter]`.
|
||||
- The `Func<string>` MUST stay a live accessor (read the field at call time, not capture a value):
|
||||
the picker body (`AbCipAddressPickerBody.OpenBrowseAsync`) calls it at click time, which is what
|
||||
makes intermediate renders during the async resolve harmless.
|
||||
- Both modals guard against re-render re-initialisation (`_loadedKey` / `_open`) — the resolve must
|
||||
ride the same guard so an in-progress edit is never clobbered by a re-resolve.
|
||||
- Test precedent for reaching into razor component classes:
|
||||
`tests/Server/ZB.MOM.WW.OtOpcUa.AdminUI.Tests/Raw/RawDriverTypeDialogParityTests.cs` (reflection;
|
||||
razor components are ordinary classes in `ZB.MOM.WW.OtOpcUa.AdminUI.Components.Shared.Raw`).
|
||||
`@inject` generates a **private** `Svc` property — set it with reflection
|
||||
(`BindingFlags.NonPublic | BindingFlags.Instance`). `OnParametersSetAsync` is protected — invoke
|
||||
via reflection. `[Parameter]` properties are public — set directly.
|
||||
- Repo conventions: xUnit + Shouldly, sentence-style test names
|
||||
(`The_editor_receives_the_merged_config`), comments state constraints not narration.
|
||||
|
||||
---
|
||||
|
||||
### Task 0: FakeRawTreeService test helper
|
||||
|
||||
**Classification:** small
|
||||
**Estimated implement time:** ~4 min
|
||||
**Parallelizable with:** none (Tasks 1–2 depend on it)
|
||||
|
||||
**Files:**
|
||||
- Create: `tests/Server/ZB.MOM.WW.OtOpcUa.AdminUI.Tests/Raw/FakeRawTreeService.cs`
|
||||
- Read (reference only): `src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Uns/IRawTreeService.cs`
|
||||
|
||||
**Step 1: Write the fake**
|
||||
|
||||
A hand-written fake implementing `IRawTreeService` (the repo has no mocking library). Two members
|
||||
are driven by settable delegates; every other interface member throws
|
||||
`NotImplementedException` (one-liners — generate them from the interface file):
|
||||
|
||||
```csharp
|
||||
using ZB.MOM.WW.OtOpcUa.AdminUI.Uns;
|
||||
|
||||
namespace ZB.MOM.WW.OtOpcUa.AdminUI.Tests.Raw;
|
||||
|
||||
/// <summary>
|
||||
/// Minimal <see cref="IRawTreeService"/> fake for component-class tests. Only the members the
|
||||
/// raw modals' open path touches are configurable; everything else throws so an unexpected
|
||||
/// call fails the test instead of vacuously passing.
|
||||
/// </summary>
|
||||
internal sealed class FakeRawTreeService : IRawTreeService
|
||||
{
|
||||
public Func<string, RawTagEditDto?> OnLoadTagForEdit { get; set; } =
|
||||
_ => throw new NotImplementedException();
|
||||
|
||||
public Func<string, (string DriverType, string MergedConfigJson)?> OnLoadMergedProbeConfig { get; set; } =
|
||||
_ => throw new NotImplementedException();
|
||||
|
||||
public Task<RawTagEditDto?> LoadTagForEditAsync(string tagId, CancellationToken ct = default)
|
||||
=> Task.FromResult(OnLoadTagForEdit(tagId));
|
||||
|
||||
public Task<(string DriverType, string MergedConfigJson)?> LoadMergedProbeConfigAsync(
|
||||
string deviceId, CancellationToken ct = default)
|
||||
=> Task.FromResult(OnLoadMergedProbeConfig(deviceId));
|
||||
|
||||
// --- every other IRawTreeService member: throw new NotImplementedException(); ---
|
||||
}
|
||||
```
|
||||
|
||||
Implement the remaining members exactly as declared in `IRawTreeService.cs` (expression-bodied
|
||||
`=> throw new NotImplementedException();`). Do not add behaviour to any of them.
|
||||
|
||||
**Step 2: Compile the test project**
|
||||
|
||||
Run: `dotnet build tests/Server/ZB.MOM.WW.OtOpcUa.AdminUI.Tests`
|
||||
Expected: 0 errors.
|
||||
|
||||
**Step 3: Commit**
|
||||
|
||||
```bash
|
||||
git add tests/Server/ZB.MOM.WW.OtOpcUa.AdminUI.Tests/Raw/FakeRawTreeService.cs
|
||||
git commit -m "test(raw): FakeRawTreeService for raw-modal component tests"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 1: RawTagModal — resolve and forward the merged config
|
||||
|
||||
**Classification:** standard
|
||||
**Estimated implement time:** ~5 min
|
||||
**Parallelizable with:** Task 2
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Components/Shared/Raw/RawTagModal.razor` (the
|
||||
`@code` block: `OnParametersSetAsync` ~lines 143–183, `BuildEditorParameters` ~lines 185–191)
|
||||
- Test: `tests/Server/ZB.MOM.WW.OtOpcUa.AdminUI.Tests/Raw/RawTagModalMergedConfigTests.cs` (create)
|
||||
|
||||
**Step 1: Write the failing tests**
|
||||
|
||||
```csharp
|
||||
using System.Reflection;
|
||||
using Shouldly;
|
||||
using Xunit;
|
||||
using ZB.MOM.WW.OtOpcUa.AdminUI.Components.Shared.Raw;
|
||||
using ZB.MOM.WW.OtOpcUa.AdminUI.Uns;
|
||||
|
||||
namespace ZB.MOM.WW.OtOpcUa.AdminUI.Tests.Raw;
|
||||
|
||||
/// <summary>
|
||||
/// Pins the #468 follow-up fix: the typed tag editor inside the raw tag edit modal must
|
||||
/// receive the merged Driver+Device config — not the historical "{}" stub — or the
|
||||
/// AbCip/TwinCAT/FOCAS pickers' "Browse controller" captures against no endpoint and can
|
||||
/// never succeed. The forwarding is razor wiring no rendered-output test reaches (no bUnit),
|
||||
/// so this drives the component class directly, as RawDriverTypeDialogParityTests does.
|
||||
/// </summary>
|
||||
public sealed class RawTagModalMergedConfigTests
|
||||
{
|
||||
private const string Merged = /*lang=json*/ """{"endpoint":"10.0.0.1:44818","slot":0}""";
|
||||
|
||||
private static RawTagEditDto Dto(string deviceId) => new(
|
||||
"tag-1", deviceId, null, "T1", "Float",
|
||||
default, false, null, "{}", Array.Empty<byte>());
|
||||
|
||||
private static void InjectSvc(RawTagModal modal, IRawTreeService svc)
|
||||
{
|
||||
var p = typeof(RawTagModal).GetProperty("Svc",
|
||||
BindingFlags.NonPublic | BindingFlags.Instance);
|
||||
p.ShouldNotBeNull("RawTagModal's @inject property was renamed — this test sets it by name.");
|
||||
p!.SetValue(modal, svc);
|
||||
}
|
||||
|
||||
private static Task OpenAsync(RawTagModal modal)
|
||||
{
|
||||
var m = typeof(RawTagModal).GetMethod("OnParametersSetAsync",
|
||||
BindingFlags.NonPublic | BindingFlags.Instance);
|
||||
m.ShouldNotBeNull();
|
||||
return (Task)m!.Invoke(modal, null)!;
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task The_editor_receives_the_merged_device_config_not_a_stub()
|
||||
{
|
||||
var modal = new RawTagModal { Visible = true, TagId = "tag-1", DriverType = "AbCip" };
|
||||
InjectSvc(modal, new FakeRawTreeService
|
||||
{
|
||||
OnLoadTagForEdit = _ => Dto("dev-1"),
|
||||
OnLoadMergedProbeConfig = id => id == "dev-1" ? ("AbCip", Merged) : null,
|
||||
});
|
||||
|
||||
await OpenAsync(modal);
|
||||
|
||||
var func = (Func<string>)modal.BuildEditorParameters()["GetDriverConfigJson"];
|
||||
func().ShouldBe(Merged);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task A_failed_resolve_degrades_to_the_empty_stub_and_does_not_throw()
|
||||
{
|
||||
var modal = new RawTagModal { Visible = true, TagId = "tag-1", DriverType = "AbCip" };
|
||||
InjectSvc(modal, new FakeRawTreeService
|
||||
{
|
||||
OnLoadTagForEdit = _ => Dto("dev-1"),
|
||||
OnLoadMergedProbeConfig = _ => throw new InvalidOperationException("device store down"),
|
||||
});
|
||||
|
||||
await OpenAsync(modal); // must not throw — fail-soft is the design (§3)
|
||||
|
||||
var func = (Func<string>)modal.BuildEditorParameters()["GetDriverConfigJson"];
|
||||
func().ShouldBe("{}");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task The_accessor_is_live_not_a_captured_snapshot()
|
||||
{
|
||||
// The picker body reads the Func at click time; intermediate renders during the async
|
||||
// resolve are only harmless because the accessor reads the field, not a captured value.
|
||||
var modal = new RawTagModal { Visible = true, TagId = "tag-1", DriverType = "AbCip" };
|
||||
var svc = new FakeRawTreeService
|
||||
{
|
||||
OnLoadTagForEdit = _ => Dto("dev-1"),
|
||||
OnLoadMergedProbeConfig = _ => null, // first open resolves nothing
|
||||
};
|
||||
InjectSvc(modal, svc);
|
||||
await OpenAsync(modal);
|
||||
var func = (Func<string>)modal.BuildEditorParameters()["GetDriverConfigJson"];
|
||||
func().ShouldBe("{}");
|
||||
|
||||
// Re-open for a different tag with the store now answering.
|
||||
modal.Visible = false;
|
||||
await OpenAsync(modal);
|
||||
modal.Visible = true;
|
||||
modal.TagId = "tag-2";
|
||||
svc.OnLoadTagForEdit = _ => Dto("dev-1") with { TagId = "tag-2" };
|
||||
svc.OnLoadMergedProbeConfig = _ => ("AbCip", Merged);
|
||||
await OpenAsync(modal);
|
||||
|
||||
func = (Func<string>)modal.BuildEditorParameters()["GetDriverConfigJson"];
|
||||
func().ShouldBe(Merged);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Notes for the implementer:
|
||||
- `RawTagEditDto` is a `readonly record struct` in `ZB.MOM.WW.OtOpcUa.AdminUI.Uns` — check its
|
||||
ctor order in `IRawTreeService.cs:20` and the `AccessLevel` parameter's enum type/namespace
|
||||
(use `default` for it as above if the name differs).
|
||||
- If `BuildEditorParameters` is private today, Step 3 makes it `internal` — the test compiles
|
||||
against `InternalsVisibleTo`.
|
||||
|
||||
**Step 2: Run tests to verify they fail**
|
||||
|
||||
Run: `dotnet test tests/Server/ZB.MOM.WW.OtOpcUa.AdminUI.Tests --filter "FullyQualifiedName~RawTagModalMergedConfigTests"`
|
||||
Expected: FAIL — `BuildEditorParameters` inaccessible (compile) first; after making it internal
|
||||
(do that now if needed to get a red run), the first and third tests fail with `"{}"` vs merged.
|
||||
The stub must be observed failing before Step 3's wiring lands.
|
||||
|
||||
**Step 3: Implement**
|
||||
|
||||
In `RawTagModal.razor` `@code`:
|
||||
|
||||
1. Add the cache field next to `_loadedKey`:
|
||||
|
||||
```csharp
|
||||
// Merged Driver+Device config for the typed editor's address picker — the same blob
|
||||
// Test-connect and RawBrowseModal dial, resolved once per open. Null (→ "{}") when the
|
||||
// resolve failed: the picker's browse then degrades to its graceful error chip.
|
||||
private string? _mergedConfigJson;
|
||||
```
|
||||
|
||||
2. In `OnParametersSetAsync`, clear it in the `!Visible` branch (`_mergedConfigJson = null;` next
|
||||
to `_loadedKey = null;`), and resolve it after the dto load succeeds (after `_rowVersion = d.RowVersion;`):
|
||||
|
||||
```csharp
|
||||
_mergedConfigJson = null;
|
||||
try
|
||||
{
|
||||
var merged = await Svc.LoadMergedProbeConfigAsync(d.DeviceId);
|
||||
if (merged is { } m) { _mergedConfigJson = m.MergedConfigJson; }
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Fail-soft by design: the modal must open and save regardless — only the browse
|
||||
// affordance inside the picker is degraded, and it reports its own error on use.
|
||||
}
|
||||
```
|
||||
|
||||
3. Make `BuildEditorParameters` **internal** and forward the live accessor:
|
||||
|
||||
```csharp
|
||||
internal IDictionary<string, object> BuildEditorParameters() => new Dictionary<string, object>
|
||||
{
|
||||
["ConfigJson"] = _form.TagConfig,
|
||||
["ConfigJsonChanged"] = EventCallback.Factory.Create<string>(this, v => _form.TagConfig = v),
|
||||
["DriverType"] = DriverType ?? "",
|
||||
["GetDriverConfigJson"] = (Func<string>)(() => _mergedConfigJson ?? "{}"),
|
||||
};
|
||||
```
|
||||
|
||||
Also update the modal's header comment (lines 1–6) to mention that the editor receives the merged
|
||||
probe config for browse.
|
||||
|
||||
**Step 4: Run tests to verify they pass**
|
||||
|
||||
Run: `dotnet test tests/Server/ZB.MOM.WW.OtOpcUa.AdminUI.Tests --filter "FullyQualifiedName~RawTagModalMergedConfigTests"`
|
||||
Expected: 3 passed.
|
||||
|
||||
**Step 5: Commit**
|
||||
|
||||
```bash
|
||||
git add src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Components/Shared/Raw/RawTagModal.razor \
|
||||
tests/Server/ZB.MOM.WW.OtOpcUa.AdminUI.Tests/Raw/RawTagModalMergedConfigTests.cs
|
||||
git commit -m "fix(raw): tag edit modal feeds the picker the merged device config, not '{}' (#468 follow-up)"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 2: RawManualTagEntryModal — resolve and forward the merged config
|
||||
|
||||
**Classification:** standard
|
||||
**Estimated implement time:** ~5 min
|
||||
**Parallelizable with:** Task 1
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Components/Shared/Raw/RawManualTagEntryModal.razor`
|
||||
(the `@code` block: `OnParametersSet` ~lines 162–175, `BuildEditorParameters` ~lines 177–183)
|
||||
- Test: `tests/Server/ZB.MOM.WW.OtOpcUa.AdminUI.Tests/Raw/RawManualTagEntryModalMergedConfigTests.cs` (create)
|
||||
|
||||
**Step 1: Write the failing tests**
|
||||
|
||||
Same shape as Task 1 (reuse its reflection helpers, adapted to this component — duplicate the two
|
||||
small private helpers rather than building a shared abstraction; two call sites do not earn one):
|
||||
|
||||
```csharp
|
||||
using System.Reflection;
|
||||
using Shouldly;
|
||||
using Xunit;
|
||||
using ZB.MOM.WW.OtOpcUa.AdminUI.Components.Shared.Raw;
|
||||
using ZB.MOM.WW.OtOpcUa.AdminUI.Uns;
|
||||
|
||||
namespace ZB.MOM.WW.OtOpcUa.AdminUI.Tests.Raw;
|
||||
|
||||
/// <summary>
|
||||
/// Same defect, same pin as <see cref="RawTagModalMergedConfigTests"/>, for the bulk
|
||||
/// manual-entry grid: each row's typed editor must receive the merged Driver+Device config.
|
||||
/// Unlike the edit modal, the device is a direct parameter here — no tag load precedes it.
|
||||
/// </summary>
|
||||
public sealed class RawManualTagEntryModalMergedConfigTests
|
||||
{
|
||||
private const string Merged = /*lang=json*/ """{"endpoint":"10.0.0.1:44818","slot":0}""";
|
||||
|
||||
private static void InjectSvc(RawManualTagEntryModal modal, IRawTreeService svc)
|
||||
{
|
||||
var p = typeof(RawManualTagEntryModal).GetProperty("Svc",
|
||||
BindingFlags.NonPublic | BindingFlags.Instance);
|
||||
p.ShouldNotBeNull("RawManualTagEntryModal's @inject property was renamed — this test sets it by name.");
|
||||
p!.SetValue(modal, svc);
|
||||
}
|
||||
|
||||
private static Task OpenAsync(RawManualTagEntryModal modal)
|
||||
{
|
||||
var m = typeof(RawManualTagEntryModal).GetMethod("OnParametersSetAsync",
|
||||
BindingFlags.NonPublic | BindingFlags.Instance);
|
||||
m.ShouldNotBeNull("the open path moved off OnParametersSetAsync — retarget this driver.");
|
||||
return (Task)m!.Invoke(modal, null)!;
|
||||
}
|
||||
|
||||
private static Func<string> EditorConfigAccessor(RawManualTagEntryModal modal)
|
||||
{
|
||||
// BuildEditorParameters takes the private RowModel — go through it by reflection with the
|
||||
// first seeded row, exactly as the razor markup does per row.
|
||||
var rows = typeof(RawManualTagEntryModal)
|
||||
.GetField("_rows", BindingFlags.NonPublic | BindingFlags.Instance)!
|
||||
.GetValue(modal)!;
|
||||
var row = ((System.Collections.IEnumerable)rows).Cast<object>().First();
|
||||
var build = typeof(RawManualTagEntryModal).GetMethod("BuildEditorParameters",
|
||||
BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Public);
|
||||
build.ShouldNotBeNull();
|
||||
var dict = (IDictionary<string, object>)build!.Invoke(modal, new[] { row })!;
|
||||
return (Func<string>)dict["GetDriverConfigJson"];
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Each_rows_editor_receives_the_merged_device_config_not_a_stub()
|
||||
{
|
||||
var modal = new RawManualTagEntryModal { Visible = true, DeviceId = "dev-1", DriverType = "AbCip" };
|
||||
InjectSvc(modal, new FakeRawTreeService
|
||||
{
|
||||
OnLoadMergedProbeConfig = id => id == "dev-1" ? ("AbCip", Merged) : null,
|
||||
});
|
||||
|
||||
await OpenAsync(modal);
|
||||
|
||||
EditorConfigAccessor(modal)().ShouldBe(Merged);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task A_failed_resolve_degrades_to_the_empty_stub_and_does_not_throw()
|
||||
{
|
||||
var modal = new RawManualTagEntryModal { Visible = true, DeviceId = "dev-1", DriverType = "AbCip" };
|
||||
InjectSvc(modal, new FakeRawTreeService
|
||||
{
|
||||
OnLoadMergedProbeConfig = _ => throw new InvalidOperationException("device store down"),
|
||||
});
|
||||
|
||||
await OpenAsync(modal); // must not throw — fail-soft is the design (§3)
|
||||
|
||||
EditorConfigAccessor(modal)().ShouldBe("{}");
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Step 2: Run tests to verify they fail**
|
||||
|
||||
Run: `dotnet test tests/Server/ZB.MOM.WW.OtOpcUa.AdminUI.Tests --filter "FullyQualifiedName~RawManualTagEntryModalMergedConfigTests"`
|
||||
Expected: FAIL — `OnParametersSetAsync` does not exist yet (the component overrides the sync
|
||||
`OnParametersSet`), so the `OpenAsync` helper's ShouldNotBeNull trips. That IS the red state.
|
||||
|
||||
**Step 3: Implement**
|
||||
|
||||
In `RawManualTagEntryModal.razor` `@code`:
|
||||
|
||||
1. Add the same cache field as Task 1 (same comment).
|
||||
2. Convert `OnParametersSet` to `OnParametersSetAsync`, preserving the `_open` guard and row
|
||||
seeding exactly, and appending the resolve:
|
||||
|
||||
```csharp
|
||||
protected override async Task OnParametersSetAsync()
|
||||
{
|
||||
if (!Visible)
|
||||
{
|
||||
_open = false;
|
||||
_mergedConfigJson = null;
|
||||
return;
|
||||
}
|
||||
if (_open) { return; } // same open, re-render → preserve in-progress rows
|
||||
_open = true;
|
||||
_rows.Clear();
|
||||
_rows.Add(new RowModel());
|
||||
_summary = null;
|
||||
_summaryOk = false;
|
||||
|
||||
_mergedConfigJson = null;
|
||||
if (!string.IsNullOrEmpty(DeviceId))
|
||||
{
|
||||
try
|
||||
{
|
||||
var merged = await Svc.LoadMergedProbeConfigAsync(DeviceId);
|
||||
if (merged is { } m) { _mergedConfigJson = m.MergedConfigJson; }
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Fail-soft by design: the grid must stay usable regardless — only the browse
|
||||
// affordance inside each row's picker is degraded, and it reports its own error.
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
3. Forward the live accessor in `BuildEditorParameters(RowModel row)`:
|
||||
|
||||
```csharp
|
||||
["GetDriverConfigJson"] = (Func<string>)(() => _mergedConfigJson ?? "{}"),
|
||||
```
|
||||
|
||||
Update the header comment as in Task 1.
|
||||
|
||||
**Step 4: Run tests to verify they pass**
|
||||
|
||||
Run: `dotnet test tests/Server/ZB.MOM.WW.OtOpcUa.AdminUI.Tests --filter "FullyQualifiedName~RawManualTagEntryModalMergedConfigTests"`
|
||||
Expected: 2 passed.
|
||||
|
||||
**Step 5: Commit**
|
||||
|
||||
```bash
|
||||
git add src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Components/Shared/Raw/RawManualTagEntryModal.razor \
|
||||
tests/Server/ZB.MOM.WW.OtOpcUa.AdminUI.Tests/Raw/RawManualTagEntryModalMergedConfigTests.cs
|
||||
git commit -m "fix(raw): manual tag entry feeds each row's picker the merged device config (#468 follow-up)"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 3: Full-suite verification
|
||||
|
||||
**Classification:** small
|
||||
**Estimated implement time:** ~4 min (mostly wall-clock on the suite)
|
||||
**Parallelizable with:** none (needs Tasks 1–2 merged in the worktree)
|
||||
|
||||
**Files:** none new — verification only.
|
||||
|
||||
**Step 1: Build the full solution**
|
||||
|
||||
Run: `dotnet build ZB.MOM.WW.OtOpcUa.slnx`
|
||||
Expected: 0 errors.
|
||||
|
||||
**Step 2: Run the AdminUI test project**
|
||||
|
||||
Run: `dotnet test tests/Server/ZB.MOM.WW.OtOpcUa.AdminUI.Tests`
|
||||
Expected: all passed, 0 failed (baseline before this work: 749+; now +5).
|
||||
|
||||
**Step 3: Commit anything outstanding**
|
||||
|
||||
Nothing should be outstanding; if a fix was needed to get green, commit it with an explanatory
|
||||
message. Do NOT commit a red state.
|
||||
|
||||
---
|
||||
|
||||
### Task 4: Documentation bookkeeping
|
||||
|
||||
**Classification:** trivial
|
||||
**Estimated implement time:** ~3 min
|
||||
**Parallelizable with:** none
|
||||
|
||||
**Files:**
|
||||
- Modify: `docs/plans/2026-07-24-driver-expansion-tracking.md` (Wave 0 section, "Residual
|
||||
follow-up" bullet)
|
||||
- Modify: `docs/plans/2026-07-15-universal-discovery-browser-implementation.md` ("Follow-ups
|
||||
discovered during the first gate" item 2)
|
||||
- Modify: `docs/plans/2026-08-05-raw-modal-picker-live-config-design.md` (Status line)
|
||||
|
||||
**Step 1: Update all three**
|
||||
|
||||
- Tracking doc: rewrite the "Residual follow-up (open)" bullet as closed — the named
|
||||
`*DriverPage` shells were already deleted by v3 Batch 2; the surviving instance (the raw modals'
|
||||
`"{}"` stubs) is fixed by this change (name the commits), unit-pinned by
|
||||
`RawTagModalMergedConfigTests` / `RawManualTagEntryModalMergedConfigTests`, live gate per the
|
||||
design doc §4.
|
||||
- Universal-browser implementation plan: mark first-gate follow-up 2 closed with the same summary
|
||||
(strikethrough + CLOSED note, matching the doc's existing style for items 1/2 of the second gate).
|
||||
- Design doc: Status → implemented, tests green, live gate pending/PASSED as applicable.
|
||||
|
||||
**Step 2: Commit**
|
||||
|
||||
```bash
|
||||
git add docs/plans/2026-07-24-driver-expansion-tracking.md \
|
||||
docs/plans/2026-07-15-universal-discovery-browser-implementation.md \
|
||||
docs/plans/2026-08-05-raw-modal-picker-live-config-design.md
|
||||
git commit -m "docs(raw): close the #468 follow-up — raw modals now feed pickers live merged config"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 5: Live `/run` gate (main session — NOT a subagent task)
|
||||
|
||||
**Classification:** high-risk (it is the class of verification unit tests cannot provide here)
|
||||
**Estimated implement time:** ~15 min wall-clock
|
||||
**Parallelizable with:** none
|
||||
|
||||
This is executed by the **main session** after Tasks 0–4 merge, using the Chrome automation tools
|
||||
against the local docker-dev rig (login disabled — no sign-in needed) and the patched AbCip fixture.
|
||||
|
||||
**Recipe:**
|
||||
|
||||
1. Rig up (OrbStack local): `docker-dev/docker-compose.yml` stack; AdminUI at
|
||||
`http://localhost:9200`. AbCip fixture on the shared host: `ssh dohertj2@10.100.0.35 'cd /opt/otopcua-abcip && docker compose --profile controllogix up -d'`
|
||||
(endpoint `10.100.0.35:44818` — the #468 second-gate patched `ab_server` serving `@tags`).
|
||||
2. In `/raw`, on an AbCip device pointed at the fixture with at least one authored tag:
|
||||
- **Edit modal leg:** open the tag's edit modal → the AbCip editor's "Browse controller" →
|
||||
expect the **nine-tag tree** (`Motor_Run` … `TestDINT`), not an error chip. Select a leaf,
|
||||
confirm the address lands in the Tag-name field.
|
||||
- **Manual entry leg:** device context menu → Add tags (manual) → expand a row's config editor →
|
||||
"Browse controller" → same nine-tag tree.
|
||||
- **Negative leg:** on a device with an unreachable endpoint, the button still renders and the
|
||||
click yields the graceful error chip; the modal stays usable and can still save.
|
||||
3. Record the result in the design doc §4 (and the tracking doc bullet from Task 4).
|
||||
|
||||
---
|
||||
|
||||
## Execution notes
|
||||
|
||||
- Worktree: run via superpowers-extended-cc:subagent-driven-development in a fresh worktree
|
||||
(Tasks 1 and 2 may dispatch in parallel after Task 0; Tasks 3 → 4 serial after them).
|
||||
- Subagent model: **opus** (user-specified).
|
||||
- The `Files:` blocks are the `files_to_edit` contract; needing other files is a plan defect —
|
||||
surface it.
|
||||
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"planPath": "docs/plans/2026-08-05-raw-modal-picker-live-config.md",
|
||||
"tasks": [
|
||||
{"id": 0, "subject": "Task 0: FakeRawTreeService test helper", "status": "pending"},
|
||||
{"id": 1, "subject": "Task 1: RawTagModal — resolve and forward merged config", "status": "pending", "blockedBy": [0]},
|
||||
{"id": 2, "subject": "Task 2: RawManualTagEntryModal — resolve and forward merged config", "status": "pending", "blockedBy": [0]},
|
||||
{"id": 3, "subject": "Task 3: Full-suite verification", "status": "pending", "blockedBy": [1, 2]},
|
||||
{"id": 4, "subject": "Task 4: Documentation bookkeeping", "status": "pending", "blockedBy": [3]},
|
||||
{"id": 5, "subject": "Task 5: Live /run gate (main session)", "status": "pending", "blockedBy": [4]}
|
||||
],
|
||||
"lastUpdated": "2026-08-05T00:00:00Z"
|
||||
}
|
||||
Reference in New Issue
Block a user