Merge review R1 fixes: removed driver's resilience tracker released; SDK timed-unshelve no-op (engine owns expiry); commented re-ack stored as AddComment; native-ack Primary verdict published from start; drain gate SharesRowsWithPeer; malformed cache bytes don't adopt the revision; HostAbortedException passthrough; ClusterRoleInfo seed filtered

Claude-Session: https://claude.ai/code/session_01Ef2ozxS7QPanGT8uoATbTe
This commit is contained in:
Joseph Doherty
2026-10-08 07:47:57 -04:00
28 changed files with 691 additions and 136 deletions
@@ -263,14 +263,18 @@ public sealed class ConditionMethodEventTests
/// <summary>
/// A scripted TimedShelve expires: the engine's expiry comes back as an Unshelved re-projection, which
/// reports exactly one unshelve event after the one shelve event.
/// reports exactly one unshelve event after the one shelve event — and the SDK's own expiry timer routes
/// nothing (review R1 M2).
/// </summary>
/// <remarks>
/// The expiry is the engine's (computed from the command's <c>UnshelveAtUtc</c>). Since Gitea #597 the node
/// is shelved for the real duration, so the SDK's own timer fires at the same instant — but it only routes
/// an Unshelve back to the engine (ignored by this stand-in, a no-op on an unshelved engine alarm) and
/// changes no node state, so it adds no event. The stand-in engine here schedules its expiry from the real
/// <c>UnshelveAtUtc</c> the method handler computed.
/// is shelved for the real duration, so the SDK's own timer fires at that instant. It used to route an
/// operator-style <c>Unshelve</c> by <c>system</c>, which — because the real engine only checks expiry every
/// 5 s — pre-empted the engine's <c>AutoUnshelve</c> on every expiry. This stand-in engine therefore
/// (a) expires LATE, like the real one, so the SDK timer demonstrably fires first; and (b) ACTS on a routed
/// <c>Unshelve</c> as the real engine would, recording who unshelved it. The wrapped
/// <c>OnTimedUnshelve</c> is the positive control: the SDK timer did fire, and still nothing was routed and
/// the unshelve on record is the engine's own.
/// </remarks>
[Fact]
public async Task A_scripted_timed_shelve_expires_with_exactly_one_unshelve_event()
@@ -283,21 +287,47 @@ public sealed class ConditionMethodEventTests
rig.MaterialiseScripted();
var current = PreState("TimedShelve", native: false) with { Message = "base" };
Task? expiry = null;
var routed = new System.Collections.Concurrent.ConcurrentQueue<string>();
string? unshelvedBy = null;
var engineLag = TimeSpan.FromMilliseconds(1500); // the real engine's 5 s shelving-check granularity
rig.Nm.AlarmCommandRouter = cmd =>
{
routed.Enqueue(cmd.Operation);
if (cmd.Operation == "Unshelve")
{
// What the real engine does with a routed Unshelve: an operator-style unshelve.
unshelvedBy ??= $"Unshelve:{cmd.User}";
current = current with { Shelving = AlarmShelvingKind.Unshelved, UnshelveAtUtc = null };
rig.ProjectLater(native: false, current);
return;
}
if (cmd.Operation != "TimedShelve") return;
current = current with { Shelving = AlarmShelvingKind.Timed };
// The real engine's projection carries its expiry, so the re-projection keeps the SDK timer armed
// for the real remaining time (without it, it would re-arm for MaxTimeShelved and never fire here).
current = current with { Shelving = AlarmShelvingKind.Timed, UnshelveAtUtc = cmd.UnshelveAtUtc };
rig.ProjectLater(native: false, current);
var unshelveAt = cmd.UnshelveAtUtc!.Value;
expiry = Task.Run(async () =>
{
var wait = unshelveAt - DateTime.UtcNow;
var wait = unshelveAt + engineLag - DateTime.UtcNow;
if (wait > TimeSpan.Zero) await Task.Delay(wait);
current = current with { Shelving = AlarmShelvingKind.Unshelved };
if (current.Shelving != AlarmShelvingKind.Timed) return; // ApplyShelvingCheck: Timed only
unshelvedBy ??= "AutoUnshelve:system";
current = current with { Shelving = AlarmShelvingKind.Unshelved, UnshelveAtUtc = null };
rig.Project(native: false, current);
});
};
// Count the SDK's own expiry-timer firings without changing what the production handler does.
var condition = rig.Nm.TryGetAlarmCondition(ScriptedAlarmId)!;
var sdkTimerFired = 0;
var production = condition.OnTimedUnshelve!;
condition.OnTimedUnshelve = (context, c) =>
{
Interlocked.Increment(ref sdkTimerFired);
return production(context, c);
};
using var session = await rig.ConnectAsync(anonymous: false, ct);
var events = new RecordedEvents();
var subscription = await RecordedEvents.SubscribeAsync(session, events, ct);
@@ -315,6 +345,10 @@ public sealed class ConditionMethodEventTests
rig.Project(false, current = current with { Message = "sentinel" });
await RecordedEvents.WaitUntilAsync(() => events.Has(item, "sentinel"));
Volatile.Read(ref sdkTimerFired).ShouldBeGreaterThanOrEqualTo(1, "the SDK's timed-unshelve timer fired");
routed.ShouldNotContain("Unshelve", "the SDK timer must not route an operator-style Unshelve");
unshelvedBy.ShouldBe("AutoUnshelve:system", "the engine's own expiry owns the unshelve");
var shelving = events.ConditionEvents(item, "base").Select(e => e[RecordedEvents.ShelvingField].Value).ToList();
shelving.ShouldBe(
[ObjectIds.ShelvedStateMachineType_Unshelved, ObjectIds.ShelvedStateMachineType_TimedShelved,