From d246a7e44d30f360145c0398eabcaae2b8bcbefd Mon Sep 17 00:00:00 2001 From: Joseph Doherty Date: Wed, 22 Jul 2026 17:18:49 -0400 Subject: [PATCH] =?UTF-8?q?docs(mesh):=20live-gate=20step=208=20=E2=80=94?= =?UTF-8?q?=20single-oldest-central=20rotation=20discriminator=20for=20the?= =?UTF-8?q?=20per-node=20comm-actor=20property=20(replaces=20the=20dropped?= =?UTF-8?q?=20Task=206=20assertion)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...6-07-22-mesh-phase2-clusterclient-transport.md | 15 ++++++++++++--- ...h-phase2-clusterclient-transport.md.tasks.json | 2 +- 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/docs/plans/2026-07-22-mesh-phase2-clusterclient-transport.md b/docs/plans/2026-07-22-mesh-phase2-clusterclient-transport.md index c0a2a129..b46f9015 100644 --- a/docs/plans/2026-07-22-mesh-phase2-clusterclient-transport.md +++ b/docs/plans/2026-07-22-mesh-phase2-clusterclient-transport.md @@ -917,6 +917,14 @@ Use `AwaitAssert` with explicit timeouts throughout; no `Thread.Sleep`. > The Phase 1 lesson that earns this task: *"the bad thing didn't happen"* is not evidence the mechanism works. Assertion 3 is what converts assertions 1–2 from a hope into a measurement. +> **Scope limit — what this test can NOT see:** with exactly one node per side, "registered per +> node" and "registered as a singleton" are indistinguishable by construction, so this test does +> NOT cover the per-node property whose structural assertions Task 6 dropped (both discriminators +> were unfalsifiable — see the dead-end record there). That property's discriminating coverage is +> **live-gate step 8** (stop the OLDEST central; the survivor's comm actor must receive a node ack +> inside the downing window — a singleton comm actor, hosted on the stopped oldest, cannot). Do not +> close the phase with step 8 skipped: it is the honest replacement for the dropped Task 6 test. + If this test proves irreducibly flaky, **do not delete it and do not weaken it into a relay test** — mark it `[Trait("Category","MeshBoundary")]`, exclude it from the default run, and record the flakiness in the live-gate doc as an open item. A quarantined honest test beats a green vacuous one. **Commit** @@ -969,9 +977,10 @@ git commit -m "docs(mesh): MeshTransport config, Phase 2 as shipped, design-doc | 5 | Set that node's `MaintenanceMode = 1`, redeploy | Seals green without it | | 6 | AdminUI Restart + Reconnect on a driver | Reaches the owning node; UI reports "dispatched" | | 7 | AdminUI alarm ack on `/alerts` | Reaches the owning node's engine | -| 8 | Stop **both** central nodes, then start a driver node | Node boots; ack is dropped with a Warning and **not** buffered; no crash loop | -| 9 | Restart central, redeploy | Recovers with no operator action beyond the redeploy | -| 10 | Grep every node's logs for frame-size warnings | None (the deploy path is payload-free — this is the canary that stays quiet) | +| 8 | **Contact rotation / the per-node discriminator.** Stop the **OLDEST** central node only (the singleton host — stopping the younger proves nothing, a singleton would also live on the still-up oldest), then immediately restart one driver node so it emits its boot-time ack | The **surviving** central's `central-communication` actor logs receipt of the ack **within seconds — inside the 15 s downing window**, before singleton migration could explain delivery. This is the discriminating test for "registered per node, NOT a singleton" — the property Task 6's structural assertions could not falsify (both dead ends recorded there): a singleton-shaped comm actor was hosted on the node just stopped, so nothing could receive on the survivor until downing + migration (~25 s). Receipt at the comm actor is the pass signal; the onward `Forward` to the coordinator singleton proxy is EXPECTED to buffer until the singleton migrates — do not count that against the step (full recovery is step 10's job) | +| 9 | Stop **both** central nodes, then start a driver node | Node boots; ack is dropped with a Warning and **not** buffered; no crash loop | +| 10 | Restart central, redeploy | Recovers with no operator action beyond the redeploy | +| 11 | Grep every node's logs for frame-size warnings | None (the deploy path is payload-free — this is the canary that stays quiet) | Step 8 is the one that proves the `buffer-size = 0` decision. If a command *does* arrive late after central returns, buffering is still on somewhere and the DB is no longer the single record of truth. diff --git a/docs/plans/2026-07-22-mesh-phase2-clusterclient-transport.md.tasks.json b/docs/plans/2026-07-22-mesh-phase2-clusterclient-transport.md.tasks.json index c1fd6795..de080501 100644 --- a/docs/plans/2026-07-22-mesh-phase2-clusterclient-transport.md.tasks.json +++ b/docs/plans/2026-07-22-mesh-phase2-clusterclient-transport.md.tasks.json @@ -123,7 +123,7 @@ }, { "id": 10, - "subject": "Task 10: live gate on docker-dev (10 steps; step 4 and step 8 are the ones that matter)", + "subject": "Task 10: live gate on docker-dev (11 steps; step 4 = deploy-seal semantics, step 8 = per-node/contact-rotation discriminator [replaces the dropped Task 6 assertion \u2014 do not skip], step 9 = both-centrals drop behavior)", "status": "pending", "classification": "high-risk", "blockedBy": [