docs(m8): Transport T18/T20/#16 implementation plan (15 tasks, waves A-E + INT)
This commit is contained in:
@@ -0,0 +1,457 @@
|
||||
# M8 — Transport (T18, T20) Implementation Plan
|
||||
|
||||
> **For Claude:** REQUIRED SUB-SKILL: Use superpowers-extended-cc:subagent-driven-development to implement this plan task-by-task.
|
||||
|
||||
**Goal:** Extend the Transport bundle subsystem to promote **site-scoped** and **instance-scoped** artifacts across environments via a **name-mapping subsystem** (T18), replace the coarse line-count delta with a real **per-line Myers diff** (T20), and make the **stale-instance enumeration** real (#16).
|
||||
|
||||
**Architecture:** Additive extension of the existing `.scadabundle` format (`bundleFormatVersion` stays 1, `schemaVersion` 1.0→1.1). New bundle DTOs (`SiteDto`/`DataConnectionDto`/`InstanceDto`), a transient `BundleNameMap` resolved at import (interactive wizard step + CLI flags), and a custom pure `LineDiffer`. Identity is resolved by name-map (Site/DataConnection) + `UniqueName` (Instance); conflict resolution (Add/Overwrite/Skip/Rename) is unchanged. No new EF tables/columns — Site/DataConnection/Instance already exist in the central DB.
|
||||
|
||||
**Tech Stack:** C#/.NET 10, EF Core 10 (MS SQL central / SQLite tests), Blazor Server (Bootstrap, no third-party component libs), System.Text.Json, xUnit + bUnit + NSubstitute + Playwright. `TreatWarningsAsErrors=true`; central package management (no new packages — Myers diff is hand-rolled, cf. custom-SVG `KpiTrendChart`).
|
||||
|
||||
**Design doc:** `docs/plans/2026-06-18-m8-transport-design.md` (decisions D1–D3).
|
||||
|
||||
**No EF migration:** confirmed — the feature persists no new schema. Sites/DataConnections/Instances/overrides/bindings already exist; `BundleNameMap` and `LineDiffer` output are transient.
|
||||
|
||||
**Execution conventions (from CLAUDE.md + standing constraints):**
|
||||
- Work in worktree `worktree-m8-transport` (branch `worktree-worktree-m8-transport`, off `main` @ `0c7774ac`). Worktree root: `/Users/dohertj2/Desktop/ScadaBridge/.claude/worktrees/worktree-m8-transport/`. Implementers do **NOT** create their own worktrees — they edit files under this worktree root.
|
||||
- Commit **pathspec** form only: `git commit -m "msg" -- <paths>` (`-m` BEFORE `--`). New files need `git add <path>` first. **Never** `git add -A`/`-a`. Retry on `index.lock`.
|
||||
- ≤2–3 concurrent committers per wave; post-wave HEAD-presence check (`git merge-base --is-ancestor <sha> HEAD`).
|
||||
- Targeted builds/tests per task; full-solution build + `bash docker/deploy.sh` + live smoke only at integration.
|
||||
|
||||
---
|
||||
|
||||
## Wave A — Format + name-map foundation
|
||||
|
||||
### Task A1: Commons name-map types + preview/selection/summary extensions
|
||||
|
||||
**Classification:** high-risk
|
||||
**Estimated implement time:** ~5 min
|
||||
**Parallelizable with:** A2, A3
|
||||
|
||||
**Files:**
|
||||
- Create: `src/ZB.MOM.WW.ScadaBridge.Commons/Types/Transport/BundleNameMap.cs`
|
||||
- Modify: `src/ZB.MOM.WW.ScadaBridge.Commons/Types/Transport/ExportSelection.cs`
|
||||
- Modify: `src/ZB.MOM.WW.ScadaBridge.Commons/Types/Transport/BundleSummary.cs`
|
||||
- Modify: `src/ZB.MOM.WW.ScadaBridge.Commons/Types/Transport/ImportPreview.cs`
|
||||
- Test: `tests/ZB.MOM.WW.ScadaBridge.Commons.Tests/Types/Transport/BundleNameMapTests.cs` (new)
|
||||
|
||||
**Step 1 — Add the name-map types** (`BundleNameMap.cs`):
|
||||
|
||||
```csharp
|
||||
namespace ZB.MOM.WW.ScadaBridge.Commons.Types.Transport;
|
||||
|
||||
public enum MappingAction { MapToExisting, CreateNew }
|
||||
|
||||
public sealed record SiteMapping(
|
||||
string SourceSiteIdentifier,
|
||||
MappingAction Action,
|
||||
string? TargetSiteIdentifier); // required when Action == MapToExisting
|
||||
|
||||
public sealed record ConnectionMapping(
|
||||
string SourceSiteIdentifier,
|
||||
string SourceConnectionName,
|
||||
MappingAction Action,
|
||||
string? TargetConnectionName); // required when Action == MapToExisting
|
||||
|
||||
public sealed record BundleNameMap(
|
||||
IReadOnlyList<SiteMapping> Sites,
|
||||
IReadOnlyList<ConnectionMapping> Connections)
|
||||
{
|
||||
public static BundleNameMap Empty { get; } =
|
||||
new(Array.Empty<SiteMapping>(), Array.Empty<ConnectionMapping>());
|
||||
}
|
||||
```
|
||||
|
||||
**Step 2 — Extend `ExportSelection`** — add `IReadOnlyList<int> SiteIds` and `IReadOnlyList<int> InstanceIds` (default to empty in any factory/usages; additive — keep existing positional members at the front to avoid breaking call sites, or convert to a record `with` friendly shape).
|
||||
|
||||
**Step 3 — Extend `BundleSummary`** — add `int Sites`, `int DataConnections`, `int Instances` (additive trailing members; default 0 so existing manifest deserialization of older bundles still works).
|
||||
|
||||
**Step 4 — Extend `ImportPreview`** — add the required-mapping surface:
|
||||
|
||||
```csharp
|
||||
public sealed record RequiredSiteMapping(string SourceSiteIdentifier, string SourceSiteName, string? AutoMatchTargetIdentifier);
|
||||
public sealed record RequiredConnectionMapping(string SourceSiteIdentifier, string SourceConnectionName, string? AutoMatchTargetName);
|
||||
|
||||
// ImportPreview gains:
|
||||
// IReadOnlyList<RequiredSiteMapping> RequiredSiteMappings
|
||||
// IReadOnlyList<RequiredConnectionMapping> RequiredConnectionMappings
|
||||
// Default both to empty for central-config-only bundles.
|
||||
```
|
||||
|
||||
**Step 5 — Tests** — `BundleNameMapTests`: `Empty` is empty; record equality; `MapToExisting` carries a target; round-trips through `System.Text.Json` with `BundleJsonOptions`.
|
||||
|
||||
**Step 6 — Build + test:** `dotnet build src/ZB.MOM.WW.ScadaBridge.Commons` then `dotnet test tests/ZB.MOM.WW.ScadaBridge.Commons.Tests --filter BundleNameMap`. Both green.
|
||||
|
||||
**Step 7 — Commit:** `git add` the new files, then `git commit -m "feat(transport): name-map types + preview/selection/summary extensions (M8 A1)" -- src/ZB.MOM.WW.ScadaBridge.Commons/Types/Transport/BundleNameMap.cs src/ZB.MOM.WW.ScadaBridge.Commons/Types/Transport/ExportSelection.cs src/ZB.MOM.WW.ScadaBridge.Commons/Types/Transport/BundleSummary.cs src/ZB.MOM.WW.ScadaBridge.Commons/Types/Transport/ImportPreview.cs tests/ZB.MOM.WW.ScadaBridge.Commons.Tests/Types/Transport/BundleNameMapTests.cs`
|
||||
|
||||
---
|
||||
|
||||
### Task A2: Transport bundle DTOs — Site/DataConnection/Instance + BundleContentDto
|
||||
|
||||
**Classification:** high-risk
|
||||
**Estimated implement time:** ~5 min
|
||||
**Parallelizable with:** A1, A3
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/ZB.MOM.WW.ScadaBridge.Transport/Serialization/EntityDtos.cs`
|
||||
- Test: `tests/ZB.MOM.WW.ScadaBridge.Transport.Tests/Serialization/BundleDtoSerializationTests.cs` (new — or extend an existing serialization test)
|
||||
|
||||
**Step 1 — Add DTO records** (in `EntityDtos.cs`, namespace `ZB.MOM.WW.ScadaBridge.Transport.Serialization`):
|
||||
|
||||
```csharp
|
||||
public sealed record SiteDto(
|
||||
string SiteIdentifier, string Name, string? Description,
|
||||
string? NodeAAddress, string? NodeBAddress,
|
||||
string? GrpcNodeAAddress, string? GrpcNodeBAddress);
|
||||
|
||||
public sealed record DataConnectionDto(
|
||||
string SiteIdentifier, string Name, string Protocol,
|
||||
int FailoverRetryCount, SecretsBlock? Secrets); // Primary/BackupConfiguration ride Secrets
|
||||
|
||||
public sealed record InstanceAttributeOverrideDto(string AttributeName, string? OverrideValue, DataType? ElementDataType);
|
||||
public sealed record InstanceAlarmOverrideDto(string AlarmCanonicalName, string? TriggerConfigurationOverride, int? PriorityLevelOverride);
|
||||
public sealed record InstanceNativeAlarmSourceOverrideDto(string SourceCanonicalName, string? ConnectionNameOverride, string? SourceReferenceOverride, string? ConditionFilterOverride);
|
||||
public sealed record InstanceConnectionBindingDto(string AttributeName, string ConnectionName, string? DataSourceReferenceOverride);
|
||||
|
||||
public sealed record InstanceDto(
|
||||
string UniqueName, string TemplateName, string SiteIdentifier, string? AreaName,
|
||||
InstanceState State,
|
||||
IReadOnlyList<InstanceAttributeOverrideDto> AttributeOverrides,
|
||||
IReadOnlyList<InstanceAlarmOverrideDto> AlarmOverrides,
|
||||
IReadOnlyList<InstanceNativeAlarmSourceOverrideDto> NativeAlarmSourceOverrides,
|
||||
IReadOnlyList<InstanceConnectionBindingDto> ConnectionBindings);
|
||||
```
|
||||
|
||||
(Use the correct `using`s for `DataType` and `InstanceState` from Commons. `SecretsBlock` already exists in this file.)
|
||||
|
||||
**Step 2 — Extend `BundleContentDto`** — add additive trailing members:
|
||||
`IReadOnlyList<SiteDto> Sites`, `IReadOnlyList<DataConnectionDto> DataConnections`, `IReadOnlyList<InstanceDto> Instances` (give each a `= ...` default of an empty array so deserializing older bundles yields empty lists, never null).
|
||||
|
||||
**Step 3 — Extend `EntityAggregate`** (the persistence-shaped twin) — add the matching entity collections (`IReadOnlyList<Site>`, `IReadOnlyList<DataConnection>`, `IReadOnlyList<Instance>`), defaulting to empty.
|
||||
|
||||
**Step 4 — Tests** — serialize a `BundleContentDto` with sites/connections/instances populated + a legacy content blob with only central-config arrays; assert round-trip equality and that legacy blobs deserialize with empty (non-null) new arrays.
|
||||
|
||||
**Step 5 — Build + test:** `dotnet build src/ZB.MOM.WW.ScadaBridge.Transport`; `dotnet test tests/ZB.MOM.WW.ScadaBridge.Transport.Tests --filter BundleDtoSerialization`. Green.
|
||||
|
||||
**Step 6 — Commit:** pathspec commit of `EntityDtos.cs` + the test. Message `feat(transport): site/connection/instance bundle DTOs (M8 A2)`.
|
||||
|
||||
---
|
||||
|
||||
### Task A3: `LineDiffer` — pure Myers per-line diff helper (T20)
|
||||
|
||||
**Classification:** standard
|
||||
**Estimated implement time:** ~5 min
|
||||
**Parallelizable with:** A1, A2
|
||||
|
||||
**Files:**
|
||||
- Create: `src/ZB.MOM.WW.ScadaBridge.Transport/Import/LineDiffer.cs`
|
||||
- Test: `tests/ZB.MOM.WW.ScadaBridge.Transport.Tests/Import/LineDifferTests.cs` (new)
|
||||
|
||||
**Step 1 — Implement** a pure static `LineDiffer` using the Myers O(ND) algorithm over `\n`-split lines (normalize CRLF→LF before splitting). Public surface:
|
||||
|
||||
```csharp
|
||||
public enum LineDiffOp { Context, Add, Remove }
|
||||
public sealed record LineDiffLine(LineDiffOp Op, string Text, int? OldLineNo, int? NewLineNo);
|
||||
public sealed record LineDiffResult(IReadOnlyList<LineDiffLine> Lines, bool Truncated, int AddedCount, int RemovedCount);
|
||||
|
||||
public static class LineDiffer
|
||||
{
|
||||
// maxLines caps the emitted Lines; beyond it, Truncated=true and Lines are cut
|
||||
// while AddedCount/RemovedCount still reflect the FULL diff totals.
|
||||
public static LineDiffResult Diff(string? oldText, string? newText, int maxLines = 400);
|
||||
}
|
||||
```
|
||||
|
||||
**Step 2 — Tests** (table-driven where sensible):
|
||||
- identical → no Add/Remove lines, all Context (or empty), `Truncated=false`.
|
||||
- pure add (old empty) → all Add; pure remove (new empty) → all Remove.
|
||||
- single-line change → 1 Remove + 1 Add with correct line numbers.
|
||||
- interleaved change preserves LCS context.
|
||||
- CRLF normalization: `"a\r\nb"` vs `"a\nb"` → no diff.
|
||||
- null inputs handled (null treated as empty).
|
||||
- size cap: a 1000-line all-add diff with `maxLines=400` → `Lines.Count<=400`, `Truncated=true`, `AddedCount==1000`.
|
||||
|
||||
**Step 3 — Build + test:** `dotnet test tests/ZB.MOM.WW.ScadaBridge.Transport.Tests --filter LineDiffer`. Green.
|
||||
|
||||
**Step 4 — Commit:** pathspec commit. Message `feat(transport): pure Myers LineDiffer helper (M8 A3, T20)`.
|
||||
|
||||
---
|
||||
|
||||
## Wave B — Export
|
||||
|
||||
### Task B1: DependencyResolver site/instance expansion
|
||||
|
||||
**Classification:** high-risk
|
||||
**Estimated implement time:** ~5 min
|
||||
**Parallelizable with:** B2, B3 (disjoint files; all consume A2's `EntityAggregate` shape)
|
||||
**Blocked by:** A2
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/ZB.MOM.WW.ScadaBridge.Transport/Export/DependencyResolver.cs`
|
||||
- Modify: `src/ZB.MOM.WW.ScadaBridge.Transport/Export/ResolvedExport.cs`
|
||||
- Test: `tests/ZB.MOM.WW.ScadaBridge.Transport.Tests/Export/DependencyResolverTests.cs` (extend)
|
||||
|
||||
**Steps:**
|
||||
1. Inject `ISiteRepository` (sites + data connections) and `IDeploymentManagerRepository`/`ITemplateEngineRepository` (instances + overrides + bindings) as needed.
|
||||
2. Gather selected `SiteIds` → `Site` + `GetDataConnectionsBySiteIdAsync` + `GetInstancesBySiteIdAsync`. Gather selected `InstanceIds` → `Instance` (+ overrides, bindings via the repo getters from the explore report: `GetOverridesByInstanceIdAsync`, `GetAlarmOverridesByInstanceIdAsync`, `GetNativeAlarmSourceOverridesByInstanceIdAsync`, `GetConnectionBindingsByInstanceIdAsync`).
|
||||
3. Dependency expansion (when `IncludeDependencies`): instance → its `Site`; instance → the `DataConnection`s referenced by its bindings (`DataConnectionId`) + native-alarm overrides (`ConnectionNameOverride` resolved within the instance's site); instance → its `Template` (feed into the existing template closure so shared scripts / external systems expand transitively).
|
||||
4. Add sites/connections/instances to `ResolvedExport` (new members) in a stable, deterministic order (sites by `SiteIdentifier`; connections by `(SiteIdentifier, Name)`; instances by `UniqueName`). Emit `ManifestContentEntry` rows with `dependsOn` edges (`Instance:{u} dependsOn ["Template:{t}","Site:{s}","DataConnection:{s}/{c}"]`).
|
||||
5. Tests: select a site → resolves its connections + instances; select an instance with deps → pulls site + connections + template; dedup when both a site and one of its instances are selected.
|
||||
6. Build + targeted test; pathspec commit `feat(transport): resolve site/instance export selection + deps (M8 B1)`.
|
||||
|
||||
---
|
||||
|
||||
### Task B2: EntitySerializer site/connection/instance mapping (both directions)
|
||||
|
||||
**Classification:** high-risk
|
||||
**Estimated implement time:** ~5 min
|
||||
**Parallelizable with:** B1, B3
|
||||
**Blocked by:** A2
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/ZB.MOM.WW.ScadaBridge.Transport/Serialization/EntitySerializer.cs`
|
||||
- Test: `tests/ZB.MOM.WW.ScadaBridge.Transport.Tests/Serialization/EntitySerializerTests.cs` (extend)
|
||||
|
||||
**Steps:**
|
||||
1. `ToBundleContent` (export): map `Site`→`SiteDto`; `DataConnection`→`DataConnectionDto` carrying `Primary/BackupConfiguration` into `Secrets` (keys e.g. `"PrimaryConfiguration"`, `"BackupConfiguration"`); `Instance`→`InstanceDto` resolving `TemplateId`→template name, `SiteId`→`SiteIdentifier`, `AreaId`→area name, and each `ConnectionBinding.DataConnectionId`→`(SiteIdentifier, ConnectionName)` (look up the connection within the aggregate's connections / repo-provided map). Map all four override/binding child collections.
|
||||
2. `FromBundleContent` (import deserialize): reconstruct `Site`/`DataConnection`/`Instance` entities with synthetic ids (ordinal), restore `Primary/BackupConfiguration` from `Secrets`, keep `ConnectionName` (string) on bindings for the apply-time rewire (do NOT try to assign a numeric `DataConnectionId` here — the importer resolves it against the target via the name-map).
|
||||
3. Tests: round-trip a site+connection+instance aggregate; assert connection config lands in `Secrets` (not echoed elsewhere); assert binding carries `ConnectionName`; assert override collections survive.
|
||||
4. Build + targeted test; pathspec commit `feat(transport): serialize site/connection/instance entities↔DTOs (M8 B2)`.
|
||||
|
||||
---
|
||||
|
||||
### Task B3: ManifestBuilder + summary counts for new types
|
||||
|
||||
**Classification:** standard
|
||||
**Estimated implement time:** ~3 min
|
||||
**Parallelizable with:** B1, B2
|
||||
**Blocked by:** A1
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/ZB.MOM.WW.ScadaBridge.Transport/Serialization/ManifestBuilder.cs`
|
||||
- Modify: `src/ZB.MOM.WW.ScadaBridge.Transport/Export/BundleExporter.cs` (the `BundleSummary` construction block, ~L90–98)
|
||||
- Test: `tests/ZB.MOM.WW.ScadaBridge.Transport.Tests/Serialization/ManifestBuilderTests.cs` (extend)
|
||||
|
||||
**Steps:**
|
||||
1. Populate the new `BundleSummary` counts (Sites/DataConnections/Instances) at export.
|
||||
2. Bump `CurrentSchemaVersion` `"1.0"` → `"1.1"` (keep `CurrentBundleFormatVersion = 1`). Confirm `ManifestValidator` accepts 1.1 (minor increments are additive-accepted — verify the version-gating logic only hard-refuses on `bundleFormatVersion`, not `schemaVersion`).
|
||||
3. Tests: summary counts reflect new arrays; schemaVersion is "1.1"; a manifest with schemaVersion "1.0" still validates Ok.
|
||||
4. Build + targeted test; pathspec commit `feat(transport): manifest summary + schemaVersion 1.1 (M8 B3)`.
|
||||
|
||||
---
|
||||
|
||||
### Task B4: Export plumbing — selection wiring, command, ManagementActor, CLI
|
||||
|
||||
**Classification:** high-risk
|
||||
**Estimated implement time:** ~5 min
|
||||
**Parallelizable with:** C1 (disjoint files)
|
||||
**Blocked by:** B1, B2, B3
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/ZB.MOM.WW.ScadaBridge.Transport/Export/BundleExporter.cs` (accept site/instance selection)
|
||||
- Modify: `src/ZB.MOM.WW.ScadaBridge.Commons/Messages/Management/TransportCommands.cs` (`ExportBundleCommand` gains `SiteNames`, `InstanceNames`)
|
||||
- Modify: `src/ZB.MOM.WW.ScadaBridge.ManagementService/ManagementActor.cs` (`HandleExportBundle`: resolve site/instance **names→IDs** like the existing `ResolveIds<T>` helper; populate `ExportSelection.SiteIds/InstanceIds`)
|
||||
- Modify: `src/ZB.MOM.WW.ScadaBridge.CLI/Commands/BundleCommands.cs` (`bundle export` gains `--sites A,B` and `--instances X,Y`)
|
||||
- Test: `tests/ZB.MOM.WW.ScadaBridge.CLI.Tests/Commands/BundleCommandsStreamingTests.cs` (extend) + ManagementActor handler coverage if present.
|
||||
|
||||
**Steps:**
|
||||
1. Thread the new selection through `ExportBundleCommand` → `HandleExportBundle` (name→ID resolution; sites by `SiteIdentifier` or Name, instances by `UniqueName`) → `ExportSelection`.
|
||||
2. CLI: parse `--sites`/`--instances` (comma-split, same style as `--templates`).
|
||||
3. Tests: CLI parses the flags; handler resolves names; `--all` includes sites/instances too (decide + document: `--all` should export every site + instance as well — confirm in the handler).
|
||||
4. Build affected projects + targeted tests; pathspec commit `feat(transport): export site/instance selection via CLI + ManagementActor (M8 B4)`.
|
||||
|
||||
---
|
||||
|
||||
## Wave C — Diff + preview
|
||||
|
||||
### Task C1: ArtifactDiff — Myers integration (T20) + Site/Connection/Instance compares
|
||||
|
||||
**Classification:** standard
|
||||
**Estimated implement time:** ~5 min
|
||||
**Parallelizable with:** B4
|
||||
**Blocked by:** A2, A3
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/ZB.MOM.WW.ScadaBridge.Transport/Import/ArtifactDiff.cs`
|
||||
- Test: `tests/ZB.MOM.WW.ScadaBridge.Transport.Tests/Import/ArtifactDiffTests.cs` (new or extend)
|
||||
|
||||
**Steps:**
|
||||
1. **T20:** replace `AddCodeChangeIfDifferent`'s `<{n} lines>` marker with a `LineDiffer.Diff(old, new, options.MaxDiffLines)` call, embedding the structured `LineDiffResult` into the `FieldChange` (extend `FieldChange` with an optional `lineDiff` payload, or serialize the hunks into `NewValue`/a new field — keep `FieldDiffJson` JSON-shaped and capped). Apply to `TemplateScript.Code`, `SharedScript.Code`, `ApiMethod.Script`. Keep `DiffScriptChildren` line view consistent.
|
||||
2. Add `CompareSite(SiteDto, Site?)`, `CompareDataConnection(DataConnectionDto, DataConnection?)`, `CompareInstance(InstanceDto, Instance?, existing children)` producing `ImportPreviewItem`s. Connection config diffs are **presence-only** on `Secrets` (mirror the existing external-system/db-connection secret handling — never echo endpoint/creds). Instance compares the override/binding child collections by name (reuse `DiffChildren`).
|
||||
3. Tests: code field now yields a structured `+/-` diff (assert hunks); secret presence-only; new-vs-modified-vs-identical for site/connection/instance.
|
||||
4. Build + targeted test; pathspec commit `feat(transport): Myers code diff + site/connection/instance compare (M8 C1, T20)`.
|
||||
|
||||
---
|
||||
|
||||
### Task C2: BundleImporter.PreviewAsync — new-type diff + required-mapping detection + blockers
|
||||
|
||||
**Classification:** high-risk
|
||||
**Estimated implement time:** ~5 min
|
||||
**Parallelizable with:** none (BundleImporter.cs is the critical-path file for C2→D1→D2)
|
||||
**Blocked by:** B2, C1
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/ZB.MOM.WW.ScadaBridge.Transport/Import/BundleImporter.cs` (`PreviewAsync` + `DetectBlockersAsync`)
|
||||
- Test: `tests/ZB.MOM.WW.ScadaBridge.Transport.IntegrationTests/Import/BundleImporterPreviewTests.cs` (extend)
|
||||
|
||||
**Steps:**
|
||||
1. Deserialize the new arrays; call `ArtifactDiff.CompareSite/CompareDataConnection/CompareInstance` for each (bulk-fetch target sites/connections/instances by identifier/name to avoid N+1).
|
||||
2. **Required-mapping detection:** collect distinct source `SiteIdentifier`s and `(site, connection)` pairs referenced by the bundle's instances (bindings + native-alarm overrides) and carried directly; auto-match against target `ISiteRepository.GetSiteByIdentifierAsync` / `GetDataConnectionsBySiteIdAsync`; populate `ImportPreview.RequiredSiteMappings`/`RequiredConnectionMappings`.
|
||||
3. **Blocker scan extension:** an instance whose `TemplateName` is neither in the bundle nor in the target → blocker; a referenced connection that maps to neither an existing target (auto/explicit) nor an in-bundle connection → blocker (`BlockerReason` text).
|
||||
4. Tests: preview surfaces required mappings with correct auto-match; blocker on missing template; blocker on unresolvable connection.
|
||||
5. Build + targeted integration test; pathspec commit `feat(transport): preview diff + required-mapping detection + blockers (M8 C2)`.
|
||||
|
||||
---
|
||||
|
||||
## Wave D — Apply + name-map + #16
|
||||
|
||||
### Task D1: BundleImporter.ApplyAsync — nameMap, resolve-or-create, instance upsert + FK rewire
|
||||
|
||||
**Classification:** high-risk
|
||||
**Estimated implement time:** ~8 min (heaviest task — critical path; serial reviews + final integration review)
|
||||
**Parallelizable with:** none
|
||||
**Blocked by:** C2
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/ZB.MOM.WW.ScadaBridge.Transport/Import/BundleImporter.cs` (`ApplyAsync` + new `Apply{Sites,DataConnections,Instances}Async` helpers + rewire)
|
||||
- Modify: `src/ZB.MOM.WW.ScadaBridge.Commons/Interfaces/Services/IBundleImporter.cs` (add `nameMap` param to `ApplyAsync`)
|
||||
- Test: `tests/ZB.MOM.WW.ScadaBridge.Transport.IntegrationTests/Import/BundleImporterApplyTests.cs` (extend) + a new `SiteInstanceImportTests.cs`
|
||||
|
||||
**Steps:**
|
||||
1. Extend `ApplyAsync(sessionId, resolutions, **BundleNameMap nameMap**, user, ct)`. Inject `ISiteRepository` + the instance repos.
|
||||
2. Inside the existing transaction, **before** central-config apply: resolve/create target `Site`s per `nameMap.Sites` (`CreateNew`→`AddSiteAsync` with full `SiteDto` config; `MapToExisting`→load target, honour the site's conflict resolution for Overwrite/Skip). Build a `sourceSiteIdentifier → targetSite` map.
|
||||
3. Resolve/create target `DataConnection`s per `nameMap.Connections` (config from `Secrets`); build a `(sourceSiteIdentifier, sourceConnName) → targetDataConnectionId` map.
|
||||
4. After central-config apply + the existing intermediate flush (so template/area ids are materialized): **upsert instances** — resolve `TemplateName`→target template id, `SiteIdentifier`→target site id (from map), `AreaName`→area (create-if-missing within target site), force `State=NotDeployed`; write `AttributeOverride`/`AlarmOverride`/`NativeAlarmSourceOverride` rows; write `ConnectionBinding`s with `DataConnectionId` resolved via the connection map; rewrite each `NativeAlarmSourceOverride.ConnectionNameOverride` to the mapped **target** connection name.
|
||||
5. Mirror the existing audited-write pattern (follow `ApplyTemplatesAsync`) so per-entity rows carry `BundleImportId` via `IAuditCorrelationContext`. Honour `EnforceSiteScope`-equivalent checks for the target sites if the importer has access to the caller principal (else note as a follow-up — import is already `RequireAdmin`).
|
||||
6. Tests: import a site+connections+instances into a **fresh** target with a create-new map → instances exist, `NotDeployed`, bindings point at created connections, native-alarm overrides reference target connection names; import into a **populated** target with map-to-existing + Overwrite/Skip; assert FK remap explicitly; rollback on a forced failure leaves nothing partial.
|
||||
7. Build + targeted integration tests; pathspec commit `feat(transport): apply site/instance import with name-map + FK rewire (M8 D1, T18)`.
|
||||
|
||||
---
|
||||
|
||||
### Task D2: #16 — real stale-instance enumeration
|
||||
|
||||
**Classification:** standard
|
||||
**Estimated implement time:** ~4 min
|
||||
**Parallelizable with:** D3 (disjoint files)
|
||||
**Blocked by:** D1
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/ZB.MOM.WW.ScadaBridge.Transport/Import/BundleImporter.cs` (compute `StaleInstanceIds` before commit)
|
||||
- Test: `tests/ZB.MOM.WW.ScadaBridge.Transport.IntegrationTests/Import/BundleImporterApplyTests.cs` (extend)
|
||||
|
||||
**Steps:**
|
||||
1. Before commit, for every **Template Overwritten** by this import, enumerate existing **deployed** target instances of that template (`GetInstancesByTemplateIdAsync`) and compute hash drift — reuse the flatten + `DeployedConfigSnapshot.RevisionHash` comparison the Deployments page uses (inject the flattening pipeline / a small `IStaleInstanceProbe`, or reuse `DeploymentService.GetDeploymentComparisonAsync`-equivalent against staged state). Collect drifting instance ids into `ImportResult.StaleInstanceIds` (replace the `Array.Empty<int>()` stub at ~L736).
|
||||
2. Directly-imported instances are `NotDeployed` → must NOT be counted as stale (they're new). Assert this in tests.
|
||||
3. Tests: overwrite a template that has a deployed instance → that instance id appears in `StaleInstanceIds`; an unchanged template's deployed instances do not; freshly-imported instances are absent.
|
||||
4. Build + targeted test; pathspec commit `fix(transport): real stale-instance enumeration in ImportResult (M8 D2, #16)`.
|
||||
|
||||
---
|
||||
|
||||
### Task D3: Import plumbing — name-map through command, ManagementActor, CLI
|
||||
|
||||
**Classification:** high-risk
|
||||
**Estimated implement time:** ~5 min
|
||||
**Parallelizable with:** D2 (disjoint files)
|
||||
**Blocked by:** D1, B4 (shares ManagementActor.cs / BundleCommands.cs / TransportCommands.cs with B4 — must run after B4)
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/ZB.MOM.WW.ScadaBridge.Commons/Messages/Management/TransportCommands.cs` (`PreviewBundleResult` carries required mappings; `ImportBundleCommand` carries an optional serialized `BundleNameMap` + create-missing flags)
|
||||
- Modify: `src/ZB.MOM.WW.ScadaBridge.ManagementService/ManagementActor.cs` (`HandlePreviewBundle` returns required mappings; `HandleImportBundle` builds the `BundleNameMap` from CLI flags / auto-match and passes it to `ApplyAsync`)
|
||||
- Modify: `src/ZB.MOM.WW.ScadaBridge.CLI/Commands/BundleCommands.cs` (`--map-site src=dst` repeatable, `--map-connection site/src=dst` repeatable, `--create-missing-sites`, `--create-missing-connections`)
|
||||
- Test: `tests/ZB.MOM.WW.ScadaBridge.CLI.Tests/Commands/BundleCommandsStreamingTests.cs` (extend) + ManagementActor coverage.
|
||||
|
||||
**Steps:**
|
||||
1. Extend the command contracts (additive). `HandleImportBundle`: after preview, construct `BundleNameMap` from `--map-*` flags; unmatched required mappings default to `CreateNew` only when `--create-missing-*` is set, else abort with a blocker-style error (fail-safe for automation).
|
||||
2. CLI flag parsing (`Dictionary`-style repeatable options; parse `site/src=dst`).
|
||||
3. Tests: flags parse into a `BundleNameMap`; preview result carries required mappings; import with `--map-site` round-trips end-to-end against a test target.
|
||||
4. Build + targeted tests; pathspec commit `feat(transport): name-map plumbing via CLI + ManagementActor (M8 D3)`.
|
||||
|
||||
---
|
||||
|
||||
## Wave E — UI
|
||||
|
||||
### Task E1: Export wizard — Sites/Instances selection
|
||||
|
||||
**Classification:** standard
|
||||
**Estimated implement time:** ~5 min
|
||||
**Parallelizable with:** E2 (disjoint razor files)
|
||||
**Blocked by:** B4
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/ZB.MOM.WW.ScadaBridge.CentralUI/Components/Pages/Design/TransportExport.razor` (+ `.razor.cs` if present)
|
||||
- Test: `tests/ZB.MOM.WW.ScadaBridge.CentralUI.Tests/Pages/Design/TransportExportPageTests.cs` (extend)
|
||||
|
||||
**Steps:**
|
||||
1. Step 1 of the export wizard gains a **Sites** group (each site expandable to its instances, checkbox-selectable) and a flat **Instances** selector. Wire selection into the export call (site/instance names). Step 2 dependency review shows the new edges.
|
||||
2. bUnit: sites/instances render + selection flows into the export request; `RequireDesign` enforced.
|
||||
3. Build + targeted bUnit; pathspec commit `feat(transport-ui): export wizard site/instance selection (M8 E1)`.
|
||||
|
||||
---
|
||||
|
||||
### Task E2: Import wizard — Map step + Modified `+/-` diff render
|
||||
|
||||
**Classification:** standard
|
||||
**Estimated implement time:** ~5 min
|
||||
**Parallelizable with:** E1
|
||||
**Blocked by:** D3
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/ZB.MOM.WW.ScadaBridge.CentralUI/Components/Pages/Design/TransportImport.razor` (+ `.razor.cs`)
|
||||
- Create (optional): `src/ZB.MOM.WW.ScadaBridge.CentralUI/Components/Shared/LineDiffView.razor` (renders `LineDiffResult`)
|
||||
- Test: `tests/ZB.MOM.WW.ScadaBridge.CentralUI.Tests/Pages/Design/TransportImportPageTests.cs` (extend)
|
||||
|
||||
**Steps:**
|
||||
1. Insert a **Map** step (between Passphrase and Diff) shown only when `RequiredSiteMappings`/`RequiredConnectionMappings` are non-empty: a table with auto-match defaults + per-row dropdown (existing target | Create new). Feed the chosen `BundleNameMap` into preview-refresh / apply.
|
||||
2. Modified rows render the `+/-` line diff (parse `FieldDiffJson` line-diff payload → `LineDiffView`). Show the truncation marker when `Truncated`.
|
||||
3. bUnit: Map step appears only for site/instance bundles + auto-match defaults; Modified row shows `+/-` lines.
|
||||
4. Build + targeted bUnit; pathspec commit `feat(transport-ui): import Map step + line-diff view (M8 E2)`.
|
||||
|
||||
---
|
||||
|
||||
## Integration
|
||||
|
||||
### Task INT: Docs, full build, docker rebuild, Playwright, live smoke, end-to-end trace
|
||||
|
||||
**Classification:** high-risk
|
||||
**Estimated implement time:** ~10 min
|
||||
**Parallelizable with:** none
|
||||
**Blocked by:** E1, E2
|
||||
|
||||
**Files:**
|
||||
- Modify: `docs/requirements/Component-Transport.md` (site/instance scope, name-mapping, line diff; remove "does not move site-scoped artifacts" stance; update format §, wizard §, error table, schemaVersion 1.1)
|
||||
- Modify: `docs/requirements/Component-CLI.md` + `src/ZB.MOM.WW.ScadaBridge.CLI/README.md` (new `bundle export --sites/--instances`, `bundle import --map-site/--map-connection/--create-missing-*`)
|
||||
- Modify: `README.md` (Transport row if scope summary changed)
|
||||
- Modify: `stillpending.md` (mark T18/T20 delivered; #16 fixed)
|
||||
- Modify: `docs/plans/2026-06-15-stillpending-completion-design.md` (mark M8 delivered)
|
||||
- Modify: `CLAUDE.md` (Transport key-decision bullet if warranted)
|
||||
|
||||
**Steps:**
|
||||
1. **Full-solution build:** `dotnet build ZB.MOM.WW.ScadaBridge.slnx` → 0 warnings / 0 errors (TreatWarningsAsErrors).
|
||||
2. **Full targeted test sweep:** Transport unit + integration, Commons, CLI, CentralUI bUnit. Record pass counts; triage any pre-existing reds (don't attribute new failures to "pre-existing" without proof).
|
||||
3. **Final whole-branch integration review** (code-reviewer over `git diff main..HEAD`) focused on: the cross-cluster/name-map remap chain end-to-end (export FK→name → import name→FK); secret hygiene (no endpoint/creds echoed in diffs/logs); transaction all-or-nothing with the new entity writes; `#16` not double-counting NotDeployed instances; schemaVersion forward-compat.
|
||||
4. **Docker rebuild:** `bash docker/deploy.sh`; verify central-a/central-b `/health/ready` 200 + traefik `/health/active` 200.
|
||||
5. **Live smoke:** CLI `bundle export --sites …` against the running cluster → `bundle preview` → `bundle import --map-site …` round-trip; confirm instances land + Deployments page shows them.
|
||||
6. **Playwright:** export-with-site + import-with-mapping happy path + line-diff render.
|
||||
7. Update docs; pathspec commit docs + any final fixes. Message `docs+integration(m8): Transport site/instance transport, name-map, Myers diff, stale enum (M8 INT)`.
|
||||
8. Hand off to `finishing-a-development-branch`.
|
||||
|
||||
---
|
||||
|
||||
## Task dependency summary
|
||||
|
||||
```
|
||||
A1 ─┐
|
||||
A2 ─┼─(parallel)
|
||||
A3 ─┘
|
||||
B1 ─┐
|
||||
B2 ─┼─(parallel, after A2; B3 after A1)
|
||||
B3 ─┘
|
||||
B4 (after B1,B2,B3) ∥ C1 (after A2,A3)
|
||||
C2 (after B2,C1)
|
||||
D1 (after C2)
|
||||
D2 (after D1) ∥ D3 (after D1,B4)
|
||||
E1 (after B4) ∥ E2 (after D3)
|
||||
INT (after E1,E2)
|
||||
```
|
||||
|
||||
Committer concurrency stays ≤3 per wave. `BundleImporter.cs` is the serial spine (C2→D1→D2); `ManagementActor.cs`/`BundleCommands.cs`/`TransportCommands.cs` are serialized across B4→D3.
|
||||
Reference in New Issue
Block a user