fix(daemon)+test: accurate duplicate-ARRIVED message; broaden E90/E157 + names coverage

The duplicate-ARRIVED fix from the previous commit returned INVALID_OBJECT
with the message "no substrate 'X'" — a lie, since the substrate exists.
Rewrite ReportSubstrate so ARRIVED has its own ack mapping: a duplicate is
CANNOT_DO_NOW with "substrate 'X' already exists" (a state conflict, not a
missing object), and we never silently re-create over live FSM state.

Coverage gaps closed:
- C++: ARRIVED records carrier_id/slot (now asserted); module NOT_EXECUTING
  reset transition; duplicate-ARRIVED expects CANNOT_DO_NOW.
- Interop: @eq.command now drives the real host S2F41 path (was @eq.on, so
  the headline decorator had zero wire coverage); @eq.command NameError on
  unknown name; eq.names var/alarm + dir() + typo-suggestion; replaced the
  two `check(..., True)` tautologies with full E90 journey + AT_DESTINATION
  and real error paths (ghost wafer raises, illegal module jump raises).

All 8 daemon test cases (248 assertions) and 24 pyclient interop checks pass.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-26 22:01:20 +02:00
parent d22bbc4ab2
commit 2218b854ce
3 changed files with 110 additions and 28 deletions
+22 -6
View File
@@ -533,13 +533,29 @@ class EquipmentService final : public pb::Equipment::Service {
const std::string cid = req->carrier_id();
const auto slot = static_cast<uint8_t>(req->slot());
const auto m = req->milestone();
auto outcome = rt_.read_sync([this, sid, cid, slot, m]() -> std::optional<bool> {
auto& subs = rt_.model().substrates;
if (m == pb::SubstrateReport::ARRIVED) {
if (subs.create(sid, cid, slot) == gem::SubstrateStore::CreateResult::Denied_AlreadyExists)
return std::nullopt; // → INVALID_OBJECT: duplicate substrate ID
return true;
// ARRIVED creates the substrate. A duplicate is a real conflict (re-creating
// would silently wipe the wafer's FSM state + history), so reject it with an
// accurate message rather than the generic "no such object" path.
if (m == pb::SubstrateReport::ARRIVED) {
auto created = rt_.read_sync([this, sid, cid, slot]() {
return rt_.model().substrates.create(sid, cid, slot) ==
gem::SubstrateStore::CreateResult::Created;
});
if (!created) {
resp->set_code(pb::Ack::CANNOT_DO_NOW);
resp->set_message("engine io thread did not answer (not running?)");
} else if (!*created) {
resp->set_code(pb::Ack::CANNOT_DO_NOW);
resp->set_message("substrate '" + sid + "' already exists");
} else {
resp->set_code(pb::Ack::ACCEPT);
}
return grpc::Status::OK;
}
auto outcome = rt_.read_sync([this, sid, m]() -> std::optional<bool> {
auto& subs = rt_.model().substrates;
if (!subs.has(sid)) return std::nullopt;
switch (m) {
case pb::SubstrateReport::AT_WORK: