Skip to content

fix: report a missing engine package with one severity per command - #586

Open
chinmayajha wants to merge 3 commits into
mozarkai:mainfrom
chinmayajha:fix/consistent-missing-engine-severity
Open

chinmayajha wants to merge 3 commits into
mozarkai:mainfrom
chinmayajha:fix/consistent-missing-engine-severity

Conversation

@chinmayajha

@chinmayajha chinmayajha commented Sep 22, 2026 •

Copy link
Copy Markdown
Collaborator

Closes #497

The same condition — an engine is enabled: true in config.yaml but its Python package was never installed — reached four commands three different ways. Reproduced on origin/main @ 092cf1e (after the recent doctor rework) with a minimal Appium project enabling pytesseract in text_detection, with the package genuinely absent:

Before

Command Exit What the user saw
optics doctor <proj> 0 ⚠️ Pytesseract pytesseract not installed in 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> 1 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> 0 Nothing. Generated code and a requirements.txt that omits it.
optics execute <proj> 1 Same uncaught E0601 as dry_run.

After

Command Exit What the user sees
optics doctor <proj> 0 ⚠️ config: pytesseract engine enabled in config.yaml, but its Python package is not installed (pytesseract) / → optics setup --install pytesseract, in the Project section; the reassuring config: project … look runnable row no longer fires, and the hint joins the closing ⚠️ Before your first real run: list
optics generate <proj> 0 Generated code will not run yet — 1 engine this project enables cannot be loaded: … then generates as before
optics dry_run / optics execute 1 Cannot start: 1 engine this project enables cannot be loaded: …, on stderr, before any SessionManager

Captured verbatim after the change:

$ optics dry_run repro497 ; echo exit=$?
Cannot start: 1 engine this project enables cannot be loaded:
  pytesseract: enabled in config.yaml, but its Python package is not installed (pytesseract)
    → optics setup --install pytesseract
Install what is missing, or set `enabled: false` in .../repro497/config.yaml.
exit=1

$ optics generate repro497 ; echo exit=$?
Generated code will not run yet — 1 engine this project enables cannot be loaded:
  pytesseract: enabled in config.yaml, but its Python package is not installed (pytesseract)
    → optics setup --install pytesseract
Install what is missing, or set `enabled: false` in .../repro497/config.yaml.
Generated test file: .../repro497/generated/Tests/test_generated.py
exit=0

$ optics doctor repro497 ; echo exit=$?
...
Project
  ✅ config: driver                 appium enabled
  ✅ config: appium element source  enabled: appium_find_element, appium_page_source, appium_screenshot
  ✅ config: appium settings        required settings present
  ⚠️ config: pytesseract engine     enabled in config.yaml, but its Python package is not installed (pytesseract)
      → optics setup --install pytesseract

13 ok, 7 warning(s), 0 failure(s)
⚠️ Before your first real run:
  • optics setup --install pytesseract
  ...
exit=0

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.

  • doctor and generate are allowed to finish. Both are useful with an engine missing, and failing them would break --check in CI for a machine that simply has not run optics setup yet.
  • dry_run and execute already could not get past it — both build whatever config.yaml enables, 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.py owns detection and description. Every surface renders MissingEngine.detail / .hint, and .hint is EngineBackend.install_hint — the one place optics 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::TestOneConditionOneSeverity drives doctor, generate and both runners through one fake and asserts each output contains the same detail and hint strings, 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 FAIL rather 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, and BaseRunner already has two of those that print and sys.exit(1) before the session (_exit_no_test_cases, the no-driver-enabled gate in _setup_session). The new _require_installed_engines sits with them, immediately before _setup_session.

Related, and deliberate: doctor still never constructs ConfigHandler (whose constructor creates execution_output/ in the project it is inspecting). enabled_keys reads a yaml.safe_load mapping and a Config model alike — both answer .get(section, default) — so doctor and generate keep their raw-YAML path while the runners pass their Config.

Scope notes

  • Config keys with no extra of their own — 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 a setup --install that would fail.
  • A partially installed extra reports only what is actually absent (pytesseract pulls pillow too).
  • A side effect worth naming: Diagnosis.ready is now False when the project enables a missing engine, so optics quickstart no longer offers "Run your first test now?" for a run it already knows will die on instantiation.

Commits

  1. refactor(setup): own the engine install command on the backend — EngineBackend.install_hint + engine_for; pure refactor, green on its own (1356 passed).
  2. fix: report a missing engine package with one severity per command — the shared module and the four call sites, with tests.
  3. docs: document the missing-engine severity contract — CLAUDE.md section + docs/usage/CLI_usage.md note, and refreshed execute.py/generate.py line 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 #385 xfails). New: tests/units/helpers/test_engine_requirements.py (29 tests, including the cross-command severity guard); tests/units/helpers/test_doctor.py gains coverage for _project_engine_hints and follows _mandatory_hints' new third argument. pre-commit run --files <changed> clean.

Known gaps (not addressed here)

  • optics serve, optics mcp and optics live build sessions directly rather than through BaseRunner, so they still surface the raw Code.E0601 from GenericFactory._load_module. The honest single choke point would be SessionManager.create_session, but that changes HTTP response behaviour and deserves its own change.
  • The E0601 message 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 in base_factory would improve every remaining path.
  • optics generate's emitted requirements.txt is a hardcoded list (easyocr, Appium-Python-Client, pyserial) that ignores what the project actually enables — a separate inconsistency in the same area.
  • Detection is metadata-only: a package that is installed but broken (bad native lib, e.g. a pytesseract with no tesseract binary on PATH) still fails at instantiation.

RetriggerConfidence Score: 4/5

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 All in Claude CodeFindings

  1. P1 Inactive LLM Blocks Runs ▶
Fix with agent prompt
### Issue 1
optics_framework/helper/engine_requirements.py:29
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.
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.

  • Adds package-metadata-based engine requirement detection and shared install guidance.
  • Makes doctor and generate warn while allowing completion.
  • Makes execute and dry-run stop before session creation.
  • Updates tests and documentation for the cross-command severity contract.
  • Needs adjustment so inactive LLM configuration does not become a new mandatory runtime dependency.
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]
Loading

Reviews (1) · Last reviewed commit: "docs: document the missing-engine severi..."

`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.
@sonarqubecloud

Copy link
Copy Markdown

@github-actions

Copy link
Copy Markdown
Contributor

Documentation preview

Updated automatically when the docs build succeeds; removed when the PR is closed.

"elements_sources",
"text_detection",
"image_detection",
"llm_models",

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 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.

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.

Fix in Claude Code Fix in Cursor Fix in Codex

@malto101

Copy link
Copy Markdown
Member

I think for dry_run we can just ignore this?
I see it as a light weight linter then a setup verifier

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Inconsistent severity for "engine enabled but package missing" across doctor/dry_run/generate

2 participants