Broker: stop reporting a failed calibration as a successful one

Every calibration step signals failure by calling SetState and returning
normally - none of them throws, so none reached the catch in
CalibrateDetector. The unconditional SetState(Idle, "Calibration sequence
done", Success) after the try block then overwrote all of them.

/cancel during a JUNGFRAU pedestal therefore left the broker Idle and
apparently ready to measure while holding a truncated G0 and
default-constructed zeros for G1/G2, and every subsequent run was
silently mis-converted with nothing in /status to show it. The genuine
failures - "Pedestal not collected properly", "Mask not collected
properly" - were hidden the same way.

The steps now return whether they succeeded, and the sequence reports
success only if they all did. A cancellation or a failure leaves the
state Inactive with Error severity rather than Idle or Error: the
calibration is undefined, so the detector has to be initialized again,
which is what Inactive means everywhere else in the machine. The
exception path joins them, since a throw mid-sequence leaves the
calibration no better defined. Cancelled pedestals were already Inactive
but carried Warning severity, which reads as an advisory.

CalibrateJUNGFRAU now abandons the sequence at the first failure instead
of collecting G1 and G2 on top of a G0 that was never measured - the
cancel path already behaved that way - and ConfigureDetector is skipped
when there is no calibration to operate with, a cancelled sequence having
left the detector mid-abort.

Both error paths that end an Initialize now notify the condition
variable. The state has left Busy, but without the notification a client
in /wait_until_running slept out its whole timeout - up to an hour, if it
asked for one - before noticing a failure that had already happened.

Separately, dataset_settings.space_group_number allowed 1..194 in the
OpenAPI schema while the broker accepts 1..230, so every generated
client's validate() rejected all 36 cubic space groups before the request
left. The regenerated clients follow in the version bump.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Uwv9ScHtDH6g8tYgfSuApo
This commit is contained in:
2026-08-26 16:58:03 +02:00
co-authored by Claude Opus 5
parent a6a715703f
commit c29dd67b5f
4 changed files with 118 additions and 46 deletions
+40
View File
@@ -315,3 +315,43 @@ TEST_CASE("JFJochStateMachine_AsyncStartFailure") {
REQUIRE_NOTHROW(state_machine.WaitTillMeasurementDone());
REQUIRE(state_machine.GetStatus().state == JFJochState::Idle);
}
// A calibration that does not complete must not be reported as a good one. Every step signals
// failure by calling SetState and returning normally, so nothing threw and the sequence used to
// finish with an unconditional "Calibration sequence done"/Success on top of it - leaving the
// broker Idle and apparently ready while holding partial pedestals.
TEST_CASE("JFJochStateMachine_CalibrationFailure") {
Logger logger("JFJochStateMachine_CalibrationFailure");
JFJochServices services(logger);
DiffractionExperiment experiment;
JFJochStateMachine state_machine(experiment, services, logger);
state_machine.AddDetectorSetup(DetJF4M());
// Ask for a G0 pedestal. There is no receiver, so no frames come back and the import fails -
// the same outcome as a pedestal that collected nothing on real hardware.
DetectorSettings settings = state_machine.GetDetectorSettings();
settings.PedestalG0Frames(100);
REQUIRE_NOTHROW(state_machine.LoadDetectorSettings(settings));
REQUIRE_NOTHROW(state_machine.Initialize());
REQUIRE_NOTHROW(state_machine.WaitTillMeasurementDone());
// Inactive, not Idle and not Error: the calibration is undefined, so the detector has to be
// initialized again rather than looking ready to measure.
auto status = state_machine.GetStatus();
REQUIRE(status.state == JFJochState::Inactive);
REQUIRE(status.message_severity == BrokerStatus::MessageSeverity::Error);
REQUIRE(status.message == "Pedestal not collected properly");
// ... and a data collection is refused on it, instead of producing mis-converted images.
DatasetSettings setup;
REQUIRE_THROWS(state_machine.Start(setup));
// The success path is unchanged - with no pedestal to take, the sequence still reports success.
JFJochStateMachine calibrated(experiment, services, logger);
calibrated.AddDetectorSetup(DetJF4M());
REQUIRE_NOTHROW(calibrated.Initialize());
REQUIRE_NOTHROW(calibrated.WaitTillMeasurementDone());
REQUIRE(calibrated.GetStatus().state == JFJochState::Idle);
REQUIRE(calibrated.GetStatus().message_severity == BrokerStatus::MessageSeverity::Success);
}