feat(daemon): Phase D — GEM300 in-the-loop (jobs, recipes, EC changes)
Semantics settled and documented: v1 is observe-and-report. The engine keeps acking S16/S3/S7/S2F15 from its FSM tables — exactly the behaviour both reference implementations validated — while the tool observes lifecycle events on the Subscribe stream and reports physical progress back. Gating stays the documented v2 deferred-reply item. Engine: two new store observers (HandlerSlot pattern) — RecipeStore fires (ppid, body) after an add (S7F3 downloads), EquipmentConstantStore fires (id, value) on ACCEPTED S2F15 writes only. Unit-tested. Daemon: the service registers PJ/recipe/EC observers (io thread; add_ observers coexist with register_default_handlers' primaries) and fans the new HostRequest variants out via push_request (fire-and-forget, no- buffering contract). ProcessJob carries action (Start->START, Resume-> RESUME, Paused->PAUSE, Stopping->STOP, Aborting->ABORT) + recipe + material bindings read store-side on the io thread. ReportProcessJob maps SETTING_UP ->SetupComplete, COMPLETE->ProcessComplete, ABORTED->AbortComplete via read_sync; PROCESSING is informational; unknown job => INVALID_OBJECT, table-rejected transition => CANNOT_DO_NOW. Carriers deferred (CarrierStore has no observer machinery; ReportCarrier stays UNIMPLEMENTED) — roadmap. Python client: on_process_job / on_recipe / on_constant_change decorators + report_job(job_id, state); ProcessJob dataclass exported. Tests: daemon suite 141 -> 175 assertions — the full in-process loop (S16F11 create -> tool setup -> S16F5 PJSTART -> stream ProcessJob with recipe+carriers -> ReportProcessJob(COMPLETE) -> FSM at ProcessComplete), rejection paths, S7F3 -> ProcessProgram, S2F15 -> ConstantChange with the configured name. Core 475/3097 (observer units). Live regression: daemon interop 20 checks + pyclient 13 checks still green against the running daemon. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -444,3 +444,35 @@ TEST_CASE("spool persistence: clear deletes files") {
|
||||
|
||||
fs::remove_all(dir);
|
||||
}
|
||||
|
||||
TEST_CASE("RecipeStore added-observer fires after the store is updated") {
|
||||
RecipeStore r;
|
||||
std::string seen_ppid, seen_body;
|
||||
r.add_added_handler([&](const std::string& p, const std::string& b) {
|
||||
seen_ppid = p;
|
||||
seen_body = b;
|
||||
});
|
||||
r.add("R-1", "body");
|
||||
CHECK(seen_ppid == "R-1");
|
||||
CHECK(seen_body == "body");
|
||||
CHECK(r.get("R-1").has_value()); // observer saw the post-update store
|
||||
}
|
||||
|
||||
TEST_CASE("EquipmentConstantStore changed-observer fires on Accept only") {
|
||||
EquipmentConstantStore ecids;
|
||||
ecids.add({10, "Knob", "", s2::Item::u4(uint32_t{1}),
|
||||
s2::Item::u4(uint32_t{1}), "0", "5"});
|
||||
int fired = 0;
|
||||
ecids.add_changed_handler([&](uint32_t id, const s2::Item& v) {
|
||||
++fired;
|
||||
CHECK(id == 10);
|
||||
CHECK(v == s2::Item::u4(uint32_t{3}));
|
||||
});
|
||||
CHECK(ecids.set_value(10, s2::Item::u4(uint32_t{3})) == EquipmentAck::Accept);
|
||||
CHECK(fired == 1);
|
||||
CHECK(ecids.set_value(10, s2::Item::u4(uint32_t{99})) ==
|
||||
EquipmentAck::Denied_OutOfRange);
|
||||
CHECK(ecids.set_value(999, s2::Item::u4(uint32_t{1})) ==
|
||||
EquipmentAck::Denied_UnknownEcid);
|
||||
CHECK(fired == 1); // rejected writes never fire
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user