fix: report a missing engine package with one severity per command - #586
chinmayajha wants to merge 3 commits into
Conversation
`optics setup --install <extra>` was assembled at each site that had to tell the user how to install a missing engine. Move it onto EngineBackend, and add `engine_for` so a config.yaml key resolves to its backend through the same alias index the CLI tokens use. Both are the vocabulary the next commit needs to describe a missing engine identically from four commands.
An engine set to `enabled: true` in config.yaml whose Python package was
never installed hit four commands three different ways: doctor warned in
a machine-wide inventory row and still called the project runnable,
generate said nothing at all, and dry_run/execute died mid-run on an
uncaught E0601 naming an optics module that is in fact present — the
missing piece being its third-party dependency.
engine_requirements now owns both the detection and the wording, so the
commands differ only in the severity their job warrants:
doctor warn, exit 0, and the hint joins the blocking
call-to-action (the project asked for the engine,
so it is not an optional extra)
generate warn, exit 0 — the code it writes still hits the
wall, so saying nothing is worse than late
dry_run / execute stderr and exit 1 before any session
The runner gate is a project-level precondition, in the same family as
the existing no-driver-enabled gate, not a dry-run validation finding: a
missing engine belongs to no keyword, and both commands instantiate
whatever config.yaml enables, so neither can get past it.
Detection reads a parsed-YAML mapping and a Config model alike, since
doctor must not build a ConfigHandler over the project it inspects.
Config keys with no extra of their own — templatematch, remote_ocr, an
element source riding on its driver's — resolve to no backend and are
never flagged.
Closes mozarkai#497
Record which command warns, which stops, and where the single source of the wording lives, plus the surfaces still uncovered (serve, mcp, live build sessions without BaseRunner, so they keep the raw E0601). Also refreshes the execute.py/generate.py line anchors this change shifted — and the handful that had already drifted in those two files.
|
Documentation preview
Updated automatically when the docs build succeeds; removed when the PR is closed. |
| "elements_sources", | ||
| "text_detection", | ||
| "image_detection", | ||
| "llm_models", |
There was a problem hiding this comment.
When Gemini is enabled, ai_self_heal is disabled, and google-genai is absent, this preflight treats the LLM package as required and exits before creating a session. The runner only calls get_llm() when self-healing is enabled, so this configuration could previously execute or dry-run without loading the LLM. Please make the LLM requirement follow the runtime loading condition so an unused dependency does not block the command.
Prompt To Fix With AI
This is a comment left during a code review.
Path: optics_framework/helper/engine_requirements.py
Line: 29
Comment:
**Inactive LLM Blocks Runs**
When Gemini is enabled, `ai_self_heal` is disabled, and `google-genai` is absent, this preflight treats the LLM package as required and exits before creating a session. The runner only calls `get_llm()` when self-healing is enabled, so this configuration could previously execute or dry-run without loading the LLM. Please make the LLM requirement follow the runtime loading condition so an unused dependency does not block the command.
---
For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.|
I think for dry_run we can just ignore this? |



Closes #497
The same condition — an engine is
enabled: trueinconfig.yamlbut its Python package was never installed — reached four commands three different ways. Reproduced onorigin/main@ 092cf1e (after the recent doctor rework) with a minimal Appium project enablingpytesseractintext_detection, with the package genuinely absent:Before
optics doctor <proj>⚠️ Pytesseract pytesseract not installedin the machine-wide Engines inventory, indistinguishable from BLE/Selenium extras nobody asked for. Project section:✅ config: project driver, element sources and settings look runnable. Not listed under "Before your first real run".optics dry_run <proj>Unexpected error (OpticsError): Code.E0601: Module 'pytesseract' not found in package 'optics_framework.engines.vision_models.ocr_models'— which is wrong twice over: that optics module is present, and "unexpected" is not how a missing optional extra should read.optics generate <proj>requirements.txtthat omits it.optics execute <proj>E0601as dry_run.After
optics doctor <proj>⚠️ config: pytesseract engine enabled in config.yaml, but its Python package is not installed (pytesseract)/→ optics setup --install pytesseract, in the Project section; the reassuringconfig: project … look runnablerow no longer fires, and the hint joins the closing⚠️ Before your first real run:listoptics generate <proj>Generated code will not run yet — 1 engine this project enables cannot be loaded: …then generates as beforeoptics dry_run/optics executeCannot start: 1 engine this project enables cannot be loaded: …, on stderr, before anySessionManagerCaptured verbatim after the change:
Direction, and why
The issue's prescription, unchanged: warn where the command only inspects or authors, stop where it is about to load the engine.
doctorandgenerateare allowed to finish. Both are useful with an engine missing, and failing them would break--checkin CI for a machine that simply has not runoptics setupyet.dry_runandexecutealready could not get past it — both build whateverconfig.yamlenables, so the only question was whether the user gets a sentence or a stack of internals. They now get the sentence, before any session is constructed.Two design points a reviewer will want to check:
The wording is not duplicated.
optics_framework/helper/engine_requirements.pyowns detection and description. Every surface rendersMissingEngine.detail/.hint, and.hintisEngineBackend.install_hint— the one placeoptics setup --install <extra>is built (the first commit pulls that string out of doctor, where it had been assembled inline).tests/units/helpers/test_engine_requirements.py::TestOneConditionOneSeveritydrives doctor, generate and both runners through one fake and asserts each output contains the samedetailandhintstrings, so a future edit to one cannot silently diverge."Fail fast" does not fight dry-run's validator design. CLAUDE.md is explicit that dry-run records a per-keyword
FAILrather than raising. That applies to problems a keyword owns — an unknown keyword, an unresolved${var}. A missing engine belongs to no keyword; it is a project-level precondition, andBaseRunneralready has two of those that print andsys.exit(1)before the session (_exit_no_test_cases, the no-driver-enabled gate in_setup_session). The new_require_installed_enginessits with them, immediately before_setup_session.Related, and deliberate:
doctorstill never constructsConfigHandler(whose constructor createsexecution_output/in the project it is inspecting).enabled_keysreads ayaml.safe_loadmapping and aConfigmodel alike — both answer.get(section, default)— so doctor and generate keep their raw-YAML path while the runners pass theirConfig.Scope notes
templatematch,remote_ocr,remote_oir, and element sources riding on their driver's extra — resolve to no backend and are never flagged, so nobody is sent to asetup --installthat would fail.pytesseractpullspillowtoo).Diagnosis.readyis now False when the project enables a missing engine, sooptics quickstartno longer offers "Run your first test now?" for a run it already knows will die on instantiation.Commits
refactor(setup): own the engine install command on the backend—EngineBackend.install_hint+engine_for; pure refactor, green on its own (1356 passed).fix: report a missing engine package with one severity per command— the shared module and the four call sites, with tests.docs: document the missing-engine severity contract— CLAUDE.md section +docs/usage/CLI_usage.mdnote, and refreshedexecute.py/generate.pyline anchors (this change shifted some; a few in those two files had already drifted).Tests
poetry run pytest— 1393 passed, 2 xfailed (the pre-existing#385xfails). New:tests/units/helpers/test_engine_requirements.py(29 tests, including the cross-command severity guard);tests/units/helpers/test_doctor.pygains coverage for_project_engine_hintsand follows_mandatory_hints' new third argument.pre-commit run --files <changed>clean.Known gaps (not addressed here)
optics serve,optics mcpandoptics livebuild sessions directly rather than throughBaseRunner, so they still surface the rawCode.E0601fromGenericFactory._load_module. The honest single choke point would beSessionManager.create_session, but that changes HTTP response behaviour and deserves its own change.E0601message itself ("Module '' not found in package …") still names an optics module that is present when the real cause is a missing third-party dependency. Distinguishing the two inbase_factorywould improve every remaining path.optics generate's emittedrequirements.txtis a hardcoded list (easyocr,Appium-Python-Client,pyserial) that ignores what the project actually enables — a separate inconsistency in the same area.pytesseractwith no tesseract binary on PATH) still fails at instantiation.The PR is not yet safe to merge because execute and dry-run can newly reject projects for an enabled but inactive LLM dependency.
Fix with agent prompt
Summary
This PR centralizes detection and wording for configured engines whose optional Python packages are missing, then applies command-specific behavior across doctor, generation, dry-run, and execution. The change is needed to replace misleading or absent diagnostics for OCR and other eagerly loaded backends, but its shared detector currently overstates when an enabled LLM is required.
Diagram
%%{init: {'theme': 'neutral'}}%% flowchart TD C[Read config.yaml] --> D[Find enabled engine keys] D --> M[Map keys to installable backends] M --> P{Required package metadata present?} P -->|Yes| R[Continue command] P -->|No| K{Command} K -->|doctor| W1[Project warning and readiness hint] K -->|generate| W2[Warn, then generate] K -->|execute / dry_run| X[Print guidance and exit 1] D --> L[Enabled LLM] L --> A{ai_self_heal enabled?} A -->|Yes| M A -->|No| B[Runtime does not load LLM]Reviews (1) · Last reviewed commit: "docs: document the missing-engine severi..."