CI for csaxs_bec / test (push) Successful in 3m10s
Queue-control "command job" changes: - Remove the "optimize_idgap" stub action (not commissioned, always a no-op) from both Flomni's and LamNI's action registry, and delete the now-dead TomoQueueMixin.optimize_idgap() method. - Fix the "move" action's device allow-list: "mokev" doesn't exist in the current device config at all -- the real energy device is "ccm_energy" (LamNI's copy already had this right). Also drop "ftray" (sample transfer tray), which was only ever there as a sim-testable move target. - Add a new "move_foptz_with_shift" action: moves foptz (the FZP optics-Z stage) and sets the compensating manual_shift_x/manual_shift_y in one atomic step. foptz is deliberately NOT in the plain move allow-list -- moving it shifts the aligned scan center (interferometer mirrors aren't perfectly parallel to the beam), so this is the only way to queue a foptz move, specifically so the compensating shift can never be forgotten. - The foptz target is validated against the device's real, live limits (not a hand-duplicated static number) both in the GUI spinbox's range and in the CLI's add-time validation, so a queued job can't raise when it executes unattended later; a defense-in-depth check also exists at execution time. Retargeted the e2e command-job tests from ftray to foptz (now the only sim-testable move-adjacent path) and updated the AI_docs testing/GUI-testing notes to match the new action set. Also fixes a pre-existing, unrelated broken import (plugins.omny.omny_general_tools -> plugins.OMNY_shared.omny_general_tools, already broken on main) in _bootstrap.py/test_tomo_queue.py/ test_tomo_queue_command_jobs.py that was blocking omny_e2e_tests collection entirely, needed to actually verify this change's own test rewrites. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NPZ9gzN2rt3wgiMnTKyrug
215 lines
8.4 KiB
Python
215 lines
8.4 KiB
Python
"""Step 2 -- command jobs. See AI_docs/TOMO_QUEUE_COMMAND_JOBS_PLAN.md.
|
|
|
|
ccm_energy/idgap (the plain "move" allow-list) aren't in the flomni sim
|
|
config (simulated_flomni.yaml) -- only on the real front end
|
|
(bl_frontend.yaml/bl_optics_hutch.yaml). Tests that only exercise add-time
|
|
validation/rendering (no tomo_queue_execute()) reference idgap for that
|
|
reason, while tests that actually execute the queue use the dedicated
|
|
"move_foptz_with_shift" action against foptz, which IS in the sim config
|
|
and is the only queue-movable path for foptz (it is deliberately excluded
|
|
from the plain "move" allow-list, see flomni.py's _TOMO_QUEUE_MOVE_DEVICES).
|
|
"""
|
|
|
|
import datetime
|
|
import uuid
|
|
|
|
import pytest
|
|
from _queue_helpers import FAST_STEPSIZE, add_short_job
|
|
|
|
FOPTZ_A = 15.0
|
|
FOPTZ_B = 5.0
|
|
|
|
|
|
def test_add_command_validation_errors(flomni_sim):
|
|
flomni = flomni_sim
|
|
|
|
with pytest.raises(ValueError, match="Unknown queue action"):
|
|
flomni.tomo_queue_add_command({"action": "not_a_real_action", "kwargs": {}})
|
|
|
|
with pytest.raises(ValueError, match="not queue-movable"):
|
|
flomni.tomo_queue_add_command({"action": "move", "kwargs": {"positions": {"fsamx": 0.0}}})
|
|
|
|
with pytest.raises(ValueError, match="above the maximum"):
|
|
flomni.tomo_queue_add_command(
|
|
{
|
|
"action": "move_foptz_with_shift",
|
|
"kwargs": {"foptz_target": 99.0, "manual_shift_x": 0.0, "manual_shift_y": 0.0},
|
|
}
|
|
)
|
|
|
|
with pytest.raises(ValueError, match="must be a number"):
|
|
flomni.tomo_queue_add_command(
|
|
{
|
|
"action": "move_foptz_with_shift",
|
|
"kwargs": {"foptz_target": "far", "manual_shift_x": 0.0, "manual_shift_y": 0.0},
|
|
}
|
|
)
|
|
|
|
with pytest.raises(ValueError, match="has no parameter"):
|
|
flomni.tomo_queue_add_command({"action": "move", "kwargs": {"bogus_kwarg": 1}})
|
|
|
|
# None of the rejected adds should have landed in the queue.
|
|
assert flomni._tomo_queue_proxy.as_list() == []
|
|
|
|
|
|
def test_move_execution_time_allowlist_recheck(flomni_sim):
|
|
"""Validation is two-layer (doc §3.3): tomo_queue_add_command() already
|
|
refuses a disallowed device at add time (see
|
|
test_add_command_validation_errors), but _queue_action_move() must
|
|
independently refuse it again at execution time -- the actual safety
|
|
boundary, since a job could reach the queue by some other path than the
|
|
validated CLI call (bypassed here by appending the raw dict directly, the
|
|
same way test_legacy_queue_migration simulates a pre-existing queue
|
|
entry).
|
|
"""
|
|
flomni = flomni_sim
|
|
job = {
|
|
"kind": "command",
|
|
"id": uuid.uuid4().hex,
|
|
"label": "sneaked past add-time validation",
|
|
"steps": [{"action": "move", "kwargs": {"positions": {"fsamx": 0.0}}}],
|
|
"idempotent": True,
|
|
"status": "pending",
|
|
"added_at": datetime.datetime.now().isoformat(),
|
|
}
|
|
flomni._tomo_queue_proxy.append(job)
|
|
|
|
with pytest.raises(ValueError, match="not queue-movable"):
|
|
flomni.tomo_queue_execute()
|
|
|
|
assert flomni._tomo_queue_proxy.as_list()[0]["status"] == "incomplete"
|
|
|
|
|
|
def test_tomo_queue_actions_global_var_matches_registry(flomni_sim):
|
|
flomni = flomni_sim
|
|
published = flomni.client.get_global_var("tomo_queue_actions")
|
|
assert set(published["actions"]) == {"move", "move_foptz_with_shift"}
|
|
assert published["move_devices"]["idgap"]["unit"] == "mm"
|
|
assert published["actions"]["move"]["idempotent"] is True
|
|
assert (
|
|
published["actions"]["move_foptz_with_shift"]["params"]["foptz_target"]["default"] == 17.0
|
|
)
|
|
|
|
|
|
def test_show_and_webpage_survive_command_job(flomni_sim, capsys):
|
|
flomni = flomni_sim
|
|
flomni.tomo_queue_add_command(
|
|
{"action": "move", "kwargs": {"positions": {"idgap": 6.0}}},
|
|
label="move idgap",
|
|
idempotent=True,
|
|
)
|
|
capsys.readouterr()
|
|
jobs = flomni.tomo_queue_show()
|
|
out = capsys.readouterr().out
|
|
assert "CMD move{'positions': {'idgap': 6.0}}" in out
|
|
assert "[idem]" in out
|
|
assert jobs[0]["kind"] == "command"
|
|
|
|
from csaxs_bec.bec_ipython_client.plugins.flomni.flomni_webpage_generator import _render_html
|
|
|
|
html = _render_html(phone_numbers=[], has_tomo_queue=True, tomo_types={1: "x"})
|
|
assert "isCommand" in html # the rendering branch exists; full JS isn't executed here
|
|
|
|
|
|
def test_legacy_job_no_kind_defaults_to_tomo_and_command_moves_foptz(flomni_sim):
|
|
"""B-style back-compat + the real command-job execution path, combined
|
|
into one mixed-queue run to keep the (only) real scan in this file to
|
|
one job.
|
|
"""
|
|
flomni = flomni_sim
|
|
bec = flomni.client
|
|
|
|
# Start from a known position, not whatever a previous test run happened
|
|
# to leave foptz at -- foptz's position is real device-server state that
|
|
# persists across separate pytest invocations, unlike the tomo_queue/
|
|
# tomo_progress global vars the flomni_sim fixture snapshots/restores.
|
|
flomni._queue_action_move_foptz_with_shift(FOPTZ_B, 0.0, 0.0)
|
|
foptz_before = bec.device_manager.devices.foptz.readback.get()
|
|
assert foptz_before == pytest.approx(FOPTZ_B)
|
|
|
|
add_short_job(flomni, "legacy_tomo", FAST_STEPSIZE)
|
|
# Simulate a queue entry written before the "kind" field existed.
|
|
jobs = bec.get_global_var("tomo_queue")
|
|
del jobs[0]["kind"]
|
|
bec.set_global_var("tomo_queue", jobs)
|
|
|
|
flomni.tomo_queue_add_command(
|
|
{
|
|
"action": "move_foptz_with_shift",
|
|
"kwargs": {"foptz_target": FOPTZ_A, "manual_shift_x": 1.0, "manual_shift_y": -2.0},
|
|
},
|
|
label="move foptz",
|
|
)
|
|
|
|
flomni.tomo_queue_execute()
|
|
|
|
jobs = flomni._tomo_queue_proxy.as_list()
|
|
assert [j["status"] for j in jobs] == ["done", "done"]
|
|
# Still no "kind" key -- proves it ran (and got marked done) via the
|
|
# back-compat default, not that execute() silently added one.
|
|
assert "kind" not in jobs[0]
|
|
assert "params" in jobs[0]
|
|
assert jobs[1]["kind"] == "command"
|
|
|
|
foptz_after = bec.device_manager.devices.foptz.readback.get()
|
|
assert foptz_after == pytest.approx(FOPTZ_A)
|
|
assert foptz_after != pytest.approx(foptz_before)
|
|
assert flomni.manual_shift_x == pytest.approx(1.0)
|
|
assert flomni.manual_shift_y == pytest.approx(-2.0)
|
|
|
|
|
|
def test_non_idempotent_command_resume_prompts(flomni_sim, monkeypatch):
|
|
flomni = flomni_sim
|
|
bec = flomni.client
|
|
foptz = bec.device_manager.devices.foptz
|
|
|
|
# Use the same real move path the "move_foptz_with_shift" action itself
|
|
# uses, rather than the device's underlying ophyd object directly
|
|
# (foptz's BEC wrapper is a Positioner here, unlike fsamx's DeviceBase --
|
|
# no .obj to reach for).
|
|
flomni._queue_action_move_foptz_with_shift(FOPTZ_B, 0.0, 0.0)
|
|
start = foptz.readback.get()
|
|
assert start == pytest.approx(FOPTZ_B)
|
|
|
|
index = flomni.tomo_queue_add_command(
|
|
{
|
|
"action": "move_foptz_with_shift",
|
|
"kwargs": {"foptz_target": FOPTZ_A, "manual_shift_x": 0.0, "manual_shift_y": 0.0},
|
|
},
|
|
label="non-idempotent move",
|
|
idempotent=False,
|
|
)
|
|
job_id = flomni._tomo_queue_proxy.as_list()[index]["id"]
|
|
flomni._tomo_queue_proxy.update_by_id(job_id, status="incomplete")
|
|
|
|
prompts = []
|
|
from csaxs_bec.bec_ipython_client.plugins.OMNY_shared.omny_general_tools import OMNYTools
|
|
|
|
def _yesno(resume_answer):
|
|
# tomo_queue_execute()'s own top-level "OK to start?" confirmation
|
|
# goes through the same OMNYTools.yesno -- always let that one
|
|
# through, and only record/answer the *resume* prompt from
|
|
# _run_command_job, which is what this test is actually about.
|
|
def _fn(self, message, *a, **k):
|
|
if message.startswith("Starting automatic execution"):
|
|
return True
|
|
prompts.append(message)
|
|
return resume_answer
|
|
|
|
return _fn
|
|
|
|
# "no" -> must not re-run: foptz stays put, job still ends up "done".
|
|
monkeypatch.setattr(OMNYTools, "yesno", _yesno(False))
|
|
flomni.tomo_queue_execute()
|
|
assert len(prompts) == 1
|
|
assert foptz.readback.get() == pytest.approx(start)
|
|
assert flomni._tomo_queue_proxy.as_list()[0]["status"] == "done"
|
|
|
|
# Re-arm as incomplete and try again with "yes" -> must re-run.
|
|
flomni._tomo_queue_proxy.update_by_id(job_id, status="incomplete")
|
|
monkeypatch.setattr(OMNYTools, "yesno", _yesno(True))
|
|
flomni.tomo_queue_execute()
|
|
assert len(prompts) == 2
|
|
assert foptz.readback.get() == pytest.approx(FOPTZ_A)
|
|
assert flomni._tomo_queue_proxy.as_list()[0]["status"] == "done"
|