30 KiB
Deferred Closeout Implementation Plan
For Claude: REQUIRED SUB-SKILL: Use superpowers-extended-cc:subagent-driven-development to implement this plan task-by-task (Opus implementers per user instruction; reviewer chain per each task's Classification).
Goal: Close every item the two perf-remediation plans left recorded-but-open: the dead
ISessionManager.ReadEventsAsync chain, the frame-writer lock-parking latency follow-up,
SEC-25 (per-session dashboard event ACL, design already approved in
docs/plans/2026-07-10-dashboard-session-acl-tst15.md), the structural alarm-truncation
degraded-status signal (proto change), and the wnwrap live-alarm probes (GUID identity,
ALARM_RECORDS/@COUNT semantics) that need windev state.
Architecture: Same two-phase posture as the prior plans. Gateway-side work builds and
tests on macOS via NonWindows.slnx; worker-side work (frame writer, alarm consumer,
worker command executor) is edited on the Mac and verified on windev. This plan touches
.proto contracts (Task 8) — contracts regeneration and all five clients rebuild are in
scope (Task 9), unlike the prior two plans. The windev probe task (Task 3) is gated on
external state (live alarms) and may legitimately end "blocked — recorded".
Tech stack: .NET 10 gateway / .NET Framework 4.8 x86 worker / protobuf contracts / five language clients / Blazor Server dashboard / GLAuth LDAP.
Branch: feat/deferred-closeout off main (ac3f04f).
Ground rules for every implementer subagent
- NEVER run
git stash,git reset,git clean, orgit checkout <sha/branch>. Commit with explicit pathspecs only — nevergit add -A/git commit -a. - Build/test mutual exclusion: before any
dotnet build/dotnet test, acquire the lock viamkdir /private/tmp/claude-501/-Users-dohertj2-Desktop-MxAccessGateway/f36938ae-bbca-4245-b5c9-fac512d69e22/scratchpad/buildlock(retry with backoff while it fails);rmdirit on ALL exit paths, including failures. TreatWarningsAsErrors=true,Nullable=enable— new warnings break the build; fix, don't suppress.- Follow
docs/style-guides/CSharpStyleGuide.md: file-scoped namespaces,sealedby default,Asyncsuffix, MXAccess-aligned names. - Update affected docs in the same commit as the source change.
- MXAccess parity is the contract; never synthesize events.
- Never log secrets, API keys, credentials, or tag values.
- The
Files:block is the scope contract. If the task can't be done inside it, that's a plan defect — surface it, don't silently expand scope. - On macOS build
NonWindows.slnx; the x86 Worker and fullslnxonly build on windev.
Task 1: Remove the dead ISessionManager.ReadEventsAsync chain
Classification: standard Estimated implement time: ~6 min Parallelizable with: Task 2, Task 3, Task 5
Files:
- Modify:
src/ZB.MOM.WW.MxGateway.Server/Sessions/ISessionManager.cs:42(remove member) - Modify:
src/ZB.MOM.WW.MxGateway.Server/Sessions/SessionManager.cs:191-197(remove implementation) - Modify:
src/ZB.MOM.WW.MxGateway.Server/Sessions/GatewaySession.cs(~:1517-1545 removeReadEventsAsync; ~:767-776 rewrite the "keep the two bodies in step" comment onMapWorkerEventsAsync— with the twin gone it now claims the single worker-event read path directly) - Modify: every test fake implementing
ISessionManager(grepISessionManagerundersrc/ZB.MOM.WW.MxGateway.Tests/— the as-built note indocs/plans/2026-08-15-deferred-remediation.mdestimated ~15 touches; remove the member from each fake and any tests that exercised it through the interface) - Modify:
docs/plans/2026-08-15-deferred-remediation.mdas-built note "Task 3 — ReadEventsAsync retained" (append one line: removed by this plan, date)
Spec: ISessionManager.ReadEventsAsync has zero production call sites — only test fakes
implement and exercise it. Remove the interface member, SessionManager's forwarder, and
GatewaySession.ReadEventsAsync (verify with grep first that MapWorkerEventsAsync and the
fakes are truly the only remaining references — if a production caller appears, STOP and
report; do not force it). IWorkerClient.ReadEventsAsync / WorkerClient.ReadEventsAsync
stay — that is the live worker-channel claim; most grep hits are that member, read carefully.
Tests that existed solely to exercise the pass-through die with it; tests that used a fake's
ReadEventsAsync as a convenience seam get rewired to the distributor path or removed if
redundant — judgment call, state it in the commit body.
Steps: grep callers → edit → dotnet build src/ZB.MOM.WW.MxGateway.NonWindows.slnx →
dotnet test src/ZB.MOM.WW.MxGateway.Tests/... --filter "FullyQualifiedName~GatewaySession",
~SessionManager, ~EventStreamService → commit
refactor(sessions): remove the dead ISessionManager.ReadEventsAsync chain.
Task 2: Frame-writer lock-parking — awaited control-frame completion unparked from the winner's pass
Classification: high-risk Estimated implement time: ~10 min Parallelizable with: Task 1, Task 3, Task 5
Files:
- Modify:
src/ZB.MOM.WW.MxGateway.Worker/Ipc/WorkerFrameWriter.cs - Test:
src/ZB.MOM.WW.MxGateway.Worker.Tests/Ipc/frame-writer tests (windev-run; write them now) - Modify:
docs/WorkerFrameProtocol.md(completion-latency contract paragraph) - Modify:
docs/plans/2026-08-15-deferred-remediation.mdas-built note "Task 10" (append: lock-parking closed by this plan)
Spec: Today WriteAsync enqueues the frame, then unconditionally contends for
_writeLock; a caller that loses the race stays parked in WaitAsync until the winning
drainer releases the lock — even though the winner writes and flushes the loser's control
frame mid-pass and completes its per-frame completion source at that moment
(WorkerFrameWriter.cs:112-116 documents this parking as the deliberate residual). Close it:
after enqueueing, the caller awaits its own frame's completion racing the lock
acquisition — when the completion resolves first (frame written+flushed by the winner), the
caller returns immediately; its abandoned lock-wait must not leak drain responsibility.
Shape (implementer refines, invariants below are the contract):
- Await
Task.WhenAny(frame.Completion.Task, lockWaitTask). - Completion first → detach: register a continuation on
lockWaitTaskthat, on acquisition, drains any queued frames if present and releases — the lock is never acquired-and-dropped, and a frame enqueued between the winner's last dequeue and its release still gets drained (the existing "drain everything you can see, then release" loop already covers most of this; the continuation is the backstop for the abandoned waiter). - Lock first → drain as today.
- Cancellation during the combined wait keeps the existing tombstone semantics
(
WorkerFrameWriter.cs:104-110): tombstone only if a drainer hasn't claimed the frame; a claimed frame completes normally and cancellation is not surfaced for it.
Invariants that must hold (write a test for each): every enqueued frame is eventually
written or tombstoned (no stranded frame when the completion-first path abandons its lock
wait); completions still resolve only after write+flush; batch API (WriteBatchAsync)
semantics unchanged; no double-drain / double-release; a control-frame caller racing a long
event batch observes its completion before the batch drain finishes (the latency win this
task exists for — assert with a gated slow-stream fake).
Steps: edit → macOS dotnet build of the shared-source projects is NOT possible for the
worker — verify compile on windev (dotnet build src\ZB.MOM.WW.MxGateway.Worker\... -p:Platform=x86)
via ssh windev before commit if feasible, else mark the commit "edited, windev-pending" and
Task 10/11 gates it → commit
perf(worker): unpark awaited control-frame writers from the winning drain pass.
Task 3: windev live-alarm probes — GUID identity and ALARM_RECORDS/@COUNT semantics
Classification: standard (investigation; no gateway code — findings doc only) Estimated implement time: ~15 min wall (timeboxed; may end "blocked") Parallelizable with: Task 1, Task 2, Task 4, Task 5
Files:
- Read (on windev):
src/ZB.MOM.WW.MxGateway.Worker.Tests/Probes/WnWrapConsumerProbeTests.cs(Skip-gated probe; flipSkip=nulllocally on windev, never commit the flip) - Create:
docs/AlarmProbeFindings.md(findings record, or the explicit "blocked" record) - Modify:
src/ZB.MOM.WW.MxGateway.Worker/MxAccess/WnWrapAlarmConsumer.cs— comments ONLY, and only if a finding confirms/refutes a documented assumption (no behavior change in this task)
Spec: Two questions only a live rig answers:
- GUID identity semantics: is the alarm record GUID stable for one alarm instance
across polls and across state transitions (UNACK→ACK→RTN)?
ComputeTransitionskeys the diff on GUID (latestSnapshot: Dictionary<Guid, ...>); if wnwrap mints a new GUID on a state change, a transition would read as clear+new instead. ALARM_RECORDS/@COUNT: when the fetch is capped (maxAlmCnt< actives), does the reply'sCOUNTattribute carry the total active count (a usable "more available" signal) or just the records-in-reply count? If it carries the total,IsTruncatedFetchcan become exact instead of the ≥cap heuristic — feed that finding to Task 8.
Steps:
ssh windev(lands in PowerShell; CI cloneC:\build\mxaccessgw-ci; first build after a pull may fail on stale Contracts obj — clearsrc\ZB.MOM.WW.MxGateway.Contracts\obj,binand rebuild, it is not a regression). Pull the branch.- Determine whether live alarms exist or can be raised: check provider state; try driving a
known alarmed attribute over its limit via a gateway write or a Galaxy test object.
Timebox 15 minutes. If no alarm can be made active: write
docs/AlarmProbeFindings.mdrecording exactly what was tried and that both questions remain open, commit, STOP. - With ≥1 live alarm: run the probe (
WnWrapConsumerProbeTests, Skip flipped locally, capmaxAlmCntlow, e.g. 1–2, to force truncation), ack the alarm, let it RTN, capture the XML across the transitions. Record: GUID per state,COUNTvs records-in-reply under the forced cap. - Write findings + implications (for
ComputeTransitionsandIsTruncatedFetch) intodocs/AlarmProbeFindings.md; adjustWnWrapAlarmConsumercomments where an assumption is now confirmed/refuted. Any behavioral fix the findings demand is reported to the orchestrator (feeds Task 8, or a follow-on task if it's transition-identity surgery) — not done here. - Commit
docs(alarms): wnwrap live-probe findings — GUID identity, ALARM_RECORDS COUNT.
Task 4: SEC-25 groundwork — DashboardTags on the API key, Tags on the session
Classification: high-risk
Estimated implement time: ~8 min
Parallelizable with: Task 2, Task 3, Task 5 (NOT Task 1 — both touch GatewaySession.cs and session fakes; run after Task 1 lands)
Files:
- Modify:
src/ZB.MOM.WW.MxGateway.Server/Security/Authentication/ApiKeyConstraints.cs(addIReadOnlyList<string> DashboardTags, default empty) - Modify:
src/ZB.MOM.WW.MxGateway.Server/Security/Authentication/ApiKeyConstraintSerializer.cs(round-trip the new field; absent-in-JSON → empty — old rows keep deserializing) - Modify:
src/ZB.MOM.WW.MxGateway.Server/Security/Authentication/ApiKeyAdminCommandLineParser.cs+ApiKeyAdminCliRunner.cs+ApiKeyAdminCommand.cs+ApiKeyAdminListedKey.cs(CLI--dashboard-tags team-a,team-bon create/update; shown in list output) - Modify:
src/ZB.MOM.WW.MxGateway.Server/Sessions/GatewaySession.cs(newpublic IReadOnlySet<string> Tags { get; }, set at construction from the owner key's effective constraints; empty = untagged) - Modify:
src/ZB.MOM.WW.MxGateway.Server/Sessions/SessionManager.cs(OpenSessionpath copiesApiKeyIdentity.EffectiveConstraints.DashboardTagsonto the session) - Test: serializer round-trip incl. legacy-JSON-without-field; CLI parse; session tag inheritance via the fake-worker harness
- Modify:
docs/Authorization.md(DashboardTagsis dashboard-visibility-only, never a data-access constraint)
Spec: Implements §3 of docs/plans/2026-07-10-dashboard-session-acl-tst15.md. The §9
open call is settled per the design's own recommendation: the tag rides in the existing
ApiKeyConstraints JSON blob — no SQLite schema migration. Tags are immutable per session,
assigned once at OpenSession from the owning key. No enforcement in this task — the field
and its plumbing only. Case handling: preserve tag strings as entered; comparisons later
(Task 6) are ordinal-ignore-case — note that on the property doc.
Steps: edit → build NonWindows.slnx → targeted tests (~ApiKeyConstraint, ~ApiKeyAdmin,
~SessionManager) → commit
feat(security): DashboardTags on API-key constraints; sessions inherit owner tags (SEC-25 groundwork).
Task 5: SEC-25 config — GroupToTag, UntaggedSessionVisibility, validator, mapper
Classification: standard Estimated implement time: ~6 min Parallelizable with: Task 1, Task 2, Task 3, Task 4
Files:
- Modify:
src/ZB.MOM.WW.MxGateway.Server/Configuration/—DashboardOptions(Dictionary<string,string[]> GroupToTagordinal-ignore-case;UntaggedSessionVisibilityenumAdminOnly(default)|AllViewers) +GatewayOptionsValidator(keys non-empty, tag arrays non-null/non-empty entries, enum known — mirror theGroupToRolevalidation shape) - Create: group→tag mapper as a sibling of
DashboardGroupRoleMapping(union of tags over the principal's LDAP groups; unknown group → nothing) - Test: validator accept/reject cases; mapper union/unknown-group/case-insensitivity
- Modify:
docs/GatewayConfiguration.md(Dashboard:GroupToTag,Dashboard:UntaggedSessionVisibilityrows)
Spec: §3.2 of the design doc, exactly. No coupling to GroupToRole — a group may appear
in one, the other, or both.
Steps: edit → build → --filter "FullyQualifiedName~GatewayOptionsValidator" + mapper
tests → commit feat(dashboard): GroupToTag / UntaggedSessionVisibility config (SEC-25).
Task 6: SEC-25 enforcement — ACL service, token/cookie tag claims, both subscribe seams gated
Classification: high-risk Estimated implement time: ~10 min Parallelizable with: none (needs Tasks 4 + 5)
Files:
- Create:
src/ZB.MOM.WW.MxGateway.Server/Dashboard/IDashboardSessionAcl+ implementation (CanViewSession(ClaimsPrincipal, string sessionId); DI singleton) - Modify:
src/ZB.MOM.WW.MxGateway.Server/Dashboard/HubTokenService.cs(payload gainsstring[]? Tags; mint stamps resolved granted tags aszb:dashboardtagclaims; validate rehydrates them) - Modify: dashboard cookie principal creation (
DashboardAuthenticator.CreatePrincipalper the design doc — grep for the exact site) so cookie-authenticated circuits carry the same tag claims - Modify:
src/ZB.MOM.WW.MxGateway.Server/Dashboard/Hubs/EventsHub.cs:42-60(SubscribeSessionchecks the ACL; deny →HubException, no group join; remove theTODO(per-session-acl)) - Modify:
src/ZB.MOM.WW.MxGateway.Server/Dashboard/Components/Pages/SessionDetailsPage.razor(in-process seam gate: resolve the circuit principal viaAuthenticationStateProvider, checkIDashboardSessionAcl.CanViewSessionbeforeSubscribe(SessionId); deny → render "Not authorized for this session's events." in place of the event panel, no subscription created) - Modify:
src/ZB.MOM.WW.MxGateway.Server/Dashboard/DashboardServiceCollectionExtensions.cs(register the ACL) - Test:
DashboardSessionAclTests(admin bypass; session-not-found deny; untagged under both visibility values; tag intersection match/non-match; principal-with-no-claims = empty-grant Viewer);HubTokenServicetag round-trip;EventsHubdeny-does-not-join; SessionDetailsPage gate (deny renders banner, allow subscribes — follow the existing bUnit/component test idiom inGateway/Dashboard/) - Modify:
docs/plans/2026-07-10-dashboard-session-acl-tst15.md— add an as-built note: the 2026-08 in-process feed refactor created a second seam (IDashboardSessionEventSubscriberused by SessionDetailsPage); "gating at join is sufficient — no second seam" (§4) no longer held, so the same ACL now gates both.
Spec: §§2, 4, 4.1 of the design doc, with one deviation the doc predates: the dashboard
now also consumes session events in-process (SessionDetailsPage → Subscribe(sessionId)),
so enforcement lands in two places — the hub SubscribeSession (remote surface) and the
page-side check before the in-process subscribe. ACL decision order: Admin role → allow;
session unknown → deny; untagged → UntaggedSessionVisibility == AllViewers; else
session.Tags ∩ grantedTags ≠ ∅ (ordinal-ignore-case). Anonymous localhost = Viewer with
empty grant (§4.1) — this tightens SEC-02's loopback posture by design; DisableLogin
auto-login carries both roles → admin bypass, unchanged. Fail closed everywhere. Never log
tag grants alongside credentials.
Steps: edit → build → --filter "~DashboardSessionAcl", ~EventsHub, ~HubTokenService,
~SessionDetailsPage → commit
feat(dashboard): per-session event ACL on both subscribe seams (SEC-25 / TST-15).
Task 7: SEC-25 closure — live-LDAP tests, docs sweep, design-doc status
Classification: standard Estimated implement time: ~7 min Parallelizable with: Task 8, Task 9
Files:
- Test: extend
DashboardLdapLiveTests(gatedMXGATEWAY_RUN_LIVE_LDAP_TESTS=1): map the existing GLAuth groups via config —gw-viewer's group grantedteam-athroughDashboard:GroupToTag, ateam-a-tagged session allows, ateam-b-tagged session denies;multi-role(Admin) allows both. Use the existing GLAuth users/groups (glauth.md) — no GLAuth server change. - Modify:
docs/Sessions.md(session-tag model, dashboard event visibility),gateway.mddashboard section,CLAUDE.mddashboard-auth paragraph (Viewer is tag-scoped; Admin sees all; anonymous localhost = empty-grant Viewer),glauth.md(test-grant mapping used by the live tests — config-side only),docs/GatewayDashboardDesign.md(remove/replace the two "SEC-25 outstanding" passages at ~:277 and ~:699 — the ACL now exists; describe it) - Modify:
docs/plans/2026-07-10-dashboard-session-acl-tst15.mdheader — Status: Implemented (this plan, date) - Modify:
archreviewtracking rows for TST-15/SEC-25 if aNot startedrow exists (mark Done with commit ref)
Spec: §§7–8 of the design doc. Live tests are opt-in-gated exactly like the existing
LiveLdapFactAttribute suite; they must skip cleanly where GLAuth is unreachable.
Steps: edit → build → run unit portions + (if GLAuth reachable from this Mac,
MXGATEWAY_RUN_LIVE_LDAP_TESTS=1 run; else record skipped per docs/GatewayTesting.md) →
commit test(dashboard)+docs: SEC-25 live-LDAP ACL coverage; design marked implemented.
Task 8: Alarm-truncation degraded-status signal — proto + worker + gateway + dashboard
Classification: high-risk Estimated implement time: ~10 min Parallelizable with: Task 7 (after Task 3's findings are in hand, or Task 3 recorded "blocked")
Files:
- Modify:
src/ZB.MOM.WW.MxGateway.Contracts/Protos/mxaccess_gateway.proto—ActiveAlarmSnapshotgainsbool from_truncated_snapshot(next free field number);QueryActiveAlarmsReplyPayloadgainsbool snapshot_truncated. Additive only; comment style per the file. - Regenerate:
dotnet build src/ZB.MOM.WW.MxGateway.Contracts/ZB.MOM.WW.MxGateway.Contracts.csproj(never hand-editGenerated/) - Modify:
src/ZB.MOM.WW.MxGateway.Worker/MxAccess/WnWrapAlarmConsumer.cs(expose the retained truncated state, e.g.bool LastSnapshotTruncated, maintained whereApplySnapshotUpdatealready receivestruncated) +IMxAccessAlarmConsumer.cs+ theQueryActiveAlarmsreply builder inMxAccessCommandExecutor.cs/AlarmDispatcher.cs(stamp both new fields) - Modify:
src/ZB.MOM.WW.MxGateway.Server/Alarms/GatewayAlarmMonitor.cs+IGatewayAlarmService.cs(+ its implementation) — propagate degraded state to the dashboard snapshot model (DashboardActiveAlarm.csor a sibling flag on the alarm snapshot the service exposes) - Modify:
src/ZB.MOM.WW.MxGateway.Server/Grpc/MxAccessGatewayService.cs— the publicQueryActiveAlarmsstream carriesfrom_truncated_snapshotthrough - Modify:
src/ZB.MOM.WW.MxGateway.Server/Dashboard/Components/Pages/AlarmsPage.razor— degraded banner ("Alarm snapshot may be incomplete — provider returned a capped fetch") driven by the service flag; reuse the existing alert-banner idiom, do not disturb the poll-loop structure landed indcbec97 - Test: worker XML tests (
WnWrapAlarmConsumerXmlTestsidiom) for the exposed state; gateway fake-worker test that a truncated reply surfaces the flag end-to-end; AlarmsPage banner test - Modify:
gateway.md(alarm surface paragraph),docs/DesignDecisions.md(one entry: why per-record flag + reply-payload flag, wire-compatibility, absence-inference already suppressed worker-side since the truncation cliff fix)
Spec: The truncation cliff fix (perf plan Task 23) made transitions safe but silent:
only a worker-stderr warning says the snapshot is degraded. Give it a structural signal:
worker stamps truncation state into the QueryActiveAlarms reply; gateway propagates it to
the public stream (per-record flag — the RPC returns a bare stream ActiveAlarmSnapshot
with no envelope, so a per-record boolean is the only additive carrier) and to the dashboard
alarm service; AlarmsPage shows the degraded banner. If Task 3 found that
ALARM_RECORDS/@COUNT carries the true total, also replace the IsTruncatedFetch ≥cap
heuristic with the exact comparison in the same commit (parse @COUNT, compare to records
delivered; keep the heuristic as fallback when the attribute is absent) — cite the findings
doc. If Task 3 was blocked, keep the heuristic untouched and say so in the commit body.
MXAccess parity note: this flag describes our fetch mechanics, not provider behavior — it
is additive gateway metadata, not a parity deviation.
Steps: proto edit → regenerate → worker+gateway edits → build NonWindows.slnx + gateway
tests (~Alarm, ~AlarmsPage) — worker compile/tests defer to the Task 11 windev gate →
commit feat(alarms): structural degraded-status signal for truncated alarm snapshots.
Task 9: Client regeneration + rebuild for the new alarm fields
Classification: standard Estimated implement time: ~8 min Parallelizable with: Task 7 (needs Task 8's proto committed)
Files:
- Regenerate per each client's README:
clients/dotnet,clients/python,clients/rust,clients/java, Go (clients/go/mxgw-golayout — follow its README) - Modify: each client's README alarm section IF it documents the snapshot shape (one line:
from_truncated_snapshotmeans the provider fetch was capped and absent-implies-cleared inference was suspended) - Test:
dotnet build clients/dotnet/ZB.MOM.WW.MxGateway.Client.slnx+ tests;python -m pytestinclients/python;cargo fmt && cargo check --workspace && cargo test --workspace && cargo clippy --all-targets -- -D warningsinclients/rust;gradle testinclients/java;gofmt/go build ./.../go test ./...inclients/go
Spec: Additive proto fields — codegen carries them; no typed wrapper work unless a client
already wraps ActiveAlarmSnapshot in a typed model (then add the field there too, following
how ReplayGap surfacing was done per-client). All five clients must build and test green.
Use the build lock around each build/test.
Steps: regenerate → build/test each client → commit
chore(clients): regenerate for alarm truncation fields; READMEs note the degraded flag.
Task 10: Phase gate — full gateway suite on macOS
Classification: high-risk (gate) Estimated implement time: ~5 min wall Parallelizable with: none (after Tasks 1, 4–9)
Full dotnet test src/ZB.MOM.WW.MxGateway.Tests/ZB.MOM.WW.MxGateway.Tests.csproj (with the
build lock) + dotnet build src/ZB.MOM.WW.MxGateway.NonWindows.slnx. Any failure: fix
forward with a scoped commit, re-run the failed filter, then re-run the full suite once.
Task 11: windev gate — full Windows verification
Classification: high-risk (gate) Estimated implement time: ~15 min wall Parallelizable with: none (last verification)
On windev (ssh windev, PowerShell, clone C:\build\mxaccessgw-ci; expect the stale
Contracts-obj first-build quirk — clear src\ZB.MOM.WW.MxGateway.Contracts\obj,bin and
rebuild, not a regression):
git pull # the feat/deferred-closeout branch
dotnet build src\ZB.MOM.WW.MxGateway.slnx
dotnet build src\ZB.MOM.WW.MxGateway.Worker\ZB.MOM.WW.MxGateway.Worker.csproj -p:Platform=x86
dotnet test src\ZB.MOM.WW.MxGateway.Worker.Tests\ZB.MOM.WW.MxGateway.Worker.Tests.csproj -p:Platform=x86
dotnet test src\ZB.MOM.WW.MxGateway.Tests\ZB.MOM.WW.MxGateway.Tests.csproj
This is the first verification of Task 2's writer restructure and Task 8's worker-side
changes — expect iteration; fix on the Mac, commit, re-run the failed leg. Live MXAccess
smoke (MXGATEWAY_RUN_LIVE_MXACCESS_TESTS=1) if provider state allows; else record why
skipped.
Task 12: Wrap-up — closure notes, umbrella check, final review
Classification: small Estimated implement time: ~5 min Parallelizable with: none (last)
- Append closure lines to
docs/plans/2026-08-15-deferred-remediation.md's out-of-scope table (each row: resolved by this plan + date, or — for the probe row if Task 3 blocked — "probes attempted , blocked on live alarms, findings doc records the attempt"). - Check
../scadaproj/CLAUDE.md's MxAccessGateway entry: the proto contents changed (additive fields) — update only if the index records a fact that changed (expected: no change needed; verify, don't assume). - Update
.tasks.json; update auto-memory (perf-remediation-branch.mdor successor). - Dispatch the final integration code review (Opus) over
git diff main..feat/deferred-closeout. Merge remains the user's decision.
Explicitly out of scope
| Item | Why |
|---|---|
| gRPC "all-sessions admin scope" (epic Task 16's gRPC half) | Stays with the session-resilience epic; the design doc itself scopes it out of TST-15. |
| Session-list row filtering by tag on SessionsPage | The approved design gates event subscription only; list-metadata visibility is a separate call. |
| jti-denylist token revocation | Rejected in the design (§10.3); 5-min token lifetime bounds grant staleness. |
Behavioral rework of ComputeTransitions identity if the GUID probe refutes stability |
Needs its own reviewed design against real captures; Task 3 records the evidence, a follow-on plan acts on it. |
MxAccessWriteCompletionCache clone |
Consciously kept (prior plan Task 12.5); unchanged posture. |
As-built notes (execution record, 2026-08-17)
All 12 tasks completed on feat/deferred-closeout; every implement went through its
classification's review chain to Approved. Verification: macOS build 0W/0E + gateway
1123/1123; windev full slnx 0W/0E, worker x86 523/523 (11 standing opt-in skips),
gateway 1123/1123, live MXAccess smoke 8/8 — green first try, no stale-obj quirk.
- Task 3 ended blocked, with evidence. The rig refuses the alarm-condition writes
(
SecurityErrorfrom the responding automation object — the test UDAs needAuthenticateUser+WriteSecuredor the in-engine flip script that drove the 2026-05-01 capture).docs/AlarmProbeFindings.mdrecords what was tried, the partial GUID answer (active→returned leg confirmed), and the unblock paths. - Commit
693a78dcontains two tasks' work. A concurrent implementer'sgit commitwithout pathspecs swept Task 6's staged ACL files into Task 8's alarm commit. Nothing was lost; both halves were reviewed separately and their fix rounds (7ec0b35,b9fb0dd) are clean single-task commits. Process rule tightened mid-run: pathspecs on the commit itself, not just the add. - Task 8's atomicity fix went structural. Review found the flag/snapshot pairing
relied on STA serialization while claiming a read-order guarantee; the fix made
SnapshotActiveAlarms(out bool truncated)the only accessor, so the unpaired read is unexpressible. - Task 6 review caught a real regression (async attach re-entrancy under rapid navigation) — closed with a generation guard and a mutation-verified interleaving test.
- Live-LDAP ACL tests ran green against the shared GLAuth (7/7, plus 7/7 clean-skip
without the env gate).
multi-rolesits in both GLAuth groups, so the Admin-bypass test asserts the sessions a dropped bypass would actually lose (team-b + untagged). - Task 9 found and fixed pre-existing drift:
clients/rust/protos/mxaccess_gateway.protohad diverged from Contracts (masked by the in-repo build path); refreshed byte-identical, and the client protoset descriptors were regenerated.
Follow-ups recorded, not started:
IGatewayAlarmService.StreamAsync/AlarmFeedMessagedoes not carry the truncation signal — live central-feed consumers (lmxopcua, ScadaBridge) cannot see snapshot degradation; add if those consumers need completeness reasoning.- No guard keeps
clients/rust/protos/in sync with Contracts (adiffcheck inscripts/check-codegen.ps1would close it). EffectiveDashboardConfiguration(dashboard settings page) doesn't displayGroupToTag/UntaggedSessionVisibility, though it showsGroupToRole.- The alarm probes' remaining questions (ack-leg GUID stability,
@COUNTsemantics) unblock via the paths indocs/AlarmProbeFindings.md.
Execution notes for the orchestrator
- Branch
feat/deferred-closeoutoffmainbefore Task 1. - Opus implementers per user instruction; reviewer chain per Classification (high-risk = spec-reviewer serial then code-reviewer; standard = parallel pair; small = code-reviewer only).
- Waves: Wave 1: 1, 2, 3, 5 · Wave 2: 4 (after 1) · Wave 3: 6 (after 4+5), 8 (after 3 findings/blocked) ·
Wave 4: 7, 9 · then 10 → 11 → 12. Per-task
Parallelizable withfields are the contract. - Each implementer gets its full task text + the ground rules block.
Files:is the scope contract. - Task 3 and Task 11 run against windev over
ssh windev(PowerShell); psbridge is available as fallback transport.