K2: SubstrateIDStatus (third E90 axis)

Adds the substrate-ID verification FSM that E90 §6.4.6 calls for:

  NotConfirmed   initial; equipment hasn't read the ID yet
  WaitingForHost ID has been read; awaiting host accept/reject
  Confirmed      host confirmed (or force-bound)
  Mismatched     host rejected — recoverable via Bind

Events:
  Read     NotConfirmed -> WaitingForHost
  Confirm  WaitingForHost -> Confirmed
  Mismatch WaitingForHost -> Mismatched
  Bind     any -> Confirmed (force-bind)
  Reset    any -> NotConfirmed

Wire-byte values pinned via static_assert.  The third axis is now
exposed on SubstrateStateMachine alongside location_state() and
processing_state(); set_id_handler() observes transitions.  Existing
two-axis API is unchanged.

4 new test cases cover the happy path, Mismatch+Bind recovery, Reset
from any state, and same-state event handler suppression.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-06-08 08:46:10 +02:00
parent a28a8b5982
commit a52d44ade5
3 changed files with 134 additions and 1 deletions
+42
View File
@@ -71,6 +71,48 @@ TEST_CASE("SubstrateStore: create + dedup + location update") {
CHECK(s.get("W-001")->fsm->location_state() == SubstrateState::AtWork);
}
// ---- Substrate ID Status (E90 §6.4.6) -----------------------------------
TEST_CASE("Substrate IDS: NotConfirmed -> WaitingForHost -> Confirmed") {
SubstrateStateMachine s;
CHECK(s.id_state() == SubstrateIDStatus::NotConfirmed);
CHECK(s.on_id_event(SubstrateIDEvent::Read));
CHECK(s.id_state() == SubstrateIDStatus::WaitingForHost);
CHECK(s.on_id_event(SubstrateIDEvent::Confirm));
CHECK(s.id_state() == SubstrateIDStatus::Confirmed);
}
TEST_CASE("Substrate IDS: Mismatch + Bind recovers") {
SubstrateStateMachine s;
s.on_id_event(SubstrateIDEvent::Read);
CHECK(s.on_id_event(SubstrateIDEvent::Mismatch));
CHECK(s.id_state() == SubstrateIDStatus::Mismatched);
CHECK(s.on_id_event(SubstrateIDEvent::Bind));
CHECK(s.id_state() == SubstrateIDStatus::Confirmed);
}
TEST_CASE("Substrate IDS: Reset from any state returns to NotConfirmed") {
SubstrateStateMachine s;
s.on_id_event(SubstrateIDEvent::Read);
s.on_id_event(SubstrateIDEvent::Confirm);
CHECK(s.on_id_event(SubstrateIDEvent::Reset));
CHECK(s.id_state() == SubstrateIDStatus::NotConfirmed);
}
TEST_CASE("Substrate IDS: change handler observes transitions only") {
SubstrateStateMachine s;
int calls = 0;
s.set_id_handler([&](SubstrateIDStatus, SubstrateIDStatus, SubstrateIDEvent) {
++calls;
});
s.on_id_event(SubstrateIDEvent::Read);
s.on_id_event(SubstrateIDEvent::Confirm);
CHECK(calls == 2);
// Re-confirming from Confirmed: no row, no call.
s.on_id_event(SubstrateIDEvent::Confirm);
CHECK(calls == 2);
}
TEST_CASE("SubstrateStore: history records every transition") {
SubstrateStore s;
s.create("W-H", "CAR-X", 3);