fix(mtconnect): Unsupported latch inside the checked section; live-index reads do not clear a write degradation (review r1 N-4, N-5)

- N-4: the /sample Unsupported latch is set inside TryDegradeStream's
  generation-checked critical section (latchUnsupported), so a superseded pump can
  no longer latch it on the session that replaced it.
- N-5: a read served from the live pump index clears only the read degradation -
  it is stream evidence, like a chunk, which by the I-3 ruling does not clear an
  unanswered write. The 'cannot stick' wording in the XML doc and
  docs/drivers/MTConnect.md now states the latch-until-next-answered-/current-read-
  or-write behaviour, incl. the subscription-only consequence. Tested (chunk +
  live-index read keep Degraded; an answered write clears it).

Refs #743

Claude-Session: https://claude.ai/code/session_01Ub9hGmYLQvn9XETzw7CMsT
This commit is contained in:
Joseph Doherty
2026-10-22 16:58:37 -04:00
parent c39bcd6e24
commit bc1f341e8a
3 changed files with 57 additions and 14 deletions
@@ -165,4 +165,37 @@ public sealed class MTConnectBatchedWriteTests
driver.GetHealth().State.ShouldBe(DriverState.Degraded);
}
/// <summary>
/// r1 N-5: stream evidence does not clear an unanswered write — neither a delivered /sample chunk nor a read
/// served from the live pump index. The next answered write does (the positive control).
/// </summary>
[Fact]
public async Task Stream_evidence_does_not_clear_an_unanswered_write_but_an_answered_write_does()
{
var (driver, client) = await WritableAsync();
try
{
await driver.SubscribeAsync([PosRaw], TimeSpan.FromMilliseconds(50), Ct);
await client.PumpAsync(MTConnectPumpGenerationChunks.Pos(108, 113, "1.5")).WaitAsync(TimeSpan.FromSeconds(30), Ct);
client.WriteFailure = new HttpRequestException("connection refused");
await driver.WriteAsync([new WriteRequest(PosRaw, 9.5)], Ct);
driver.GetHealth().State.ShouldBe(DriverState.Degraded);
var currentCalls = client.CurrentCallCount;
await client.PumpAsync(MTConnectPumpGenerationChunks.Pos(113, 118, "2.5")).WaitAsync(TimeSpan.FromSeconds(30), Ct);
(await driver.ReadAsync([PosRaw], Ct)).Single().StatusCode.ShouldBe(Good);
client.CurrentCallCount.ShouldBe(currentCalls, "the read was served from the live index");
driver.GetHealth().State.ShouldBe(DriverState.Degraded, "stream evidence is not an answer to the write");
client.WriteFailure = null;
await driver.WriteAsync([new WriteRequest(PosRaw, 3.5)], Ct);
driver.GetHealth().State.ShouldBe(DriverState.Healthy);
}
finally
{
await driver.ShutdownAsync(CancellationToken.None);
}
}
}