Appends the full audit findings to the spec's new 'Test Suite Audit Context' section: 27 test-engine upgrade candidates (with per-test classification), ~44 tests fine as-is, ~10 new capabilities enabled, the 3-dimension ordering taxonomy proposal (criticality x fixture x subsystem), and the 4-track campaign sequence informed by the audit. Source: docs/reports/test_suite_audit_20260627.md
24 KiB
Track Specification: ImGui Test Engine Integration (Bridge via API Hooks)
Overview
Integrate the Dear ImGui Test Engine (imgui_bundle.imgui.test_engine) into Manual Slop's test infrastructure to enable high-fidelity simulation of user interactions — docking, window focus, panel visibility, drag-and-drop, keyboard input — that the current Hook API cannot express.
The design principle: the API hooks layer (HookServer on :8999 + ApiHookClient) remains the single communication boundary between the test process (pytest) and the GUI subprocess. The test engine is integrated behind the API hooks, not alongside them. New /api/test_engine/* endpoints bridge the test process to the engine's queue_test / get_result_summary API. The engine's test_func closures run on the engine's scenario thread (GIL-transferred by hello_imgui/immapp); they use ctx.item_click("**/Label"), ctx.dock_into(src, dst, dir), ctx.window_focus(ref) etc. to post simulated input events to the GUI render thread. The existing _pending_gui_tasks queue and the engine's input simulation are two separate event injection paths into the same GUI thread; they compose without conflict.
This is Track 1 of 3 in the test engine campaign. Track 1 = enable the engine + build the bridge + smoke test. Track 2 (follow-up) = migrate docking/focus/panel tests. Track 3 (follow-up) = visual regression via screenshot capture.
Current State Audit (as of master 77b70226)
Already Implemented (DO NOT re-implement)
-
imgui_bundlev1.92.5 (pinned inpyproject.toml:7) ships the test engine compiled into the nanobind binary. Verified:from imgui_bundle import imgui; imgui.test_engine.TestEngineis a live class;imgui.test_engine.register_test,imgui.test_engine.queue_test,imgui.test_engine.get_result_summary,imgui.test_engine.TestContextwithdock_into,window_focus,item_click,capture_screenshot_window, etc. are all present (verified viadir()enumeration — ~95TestContextmethods + ~35 module-level functions). The.pyistub at.venv/Lib/site-packages/imgui_bundle/imgui/test_engine.pyidocuments the full API. -
hello_imgui.RunnerParams.use_imgui_test_engine: bool = False(.venv/Lib/site-packages/imgui_bundle/hello_imgui.pyi:2969) — the flag that enables the engine. WhenTrue,hello_imgui/immappcompiles the engine into the runner and provides the GIL-transfer mechanism for the scenario thread. The engine is already compiled into the wheel (the C++ build flag-DHELLOIMGUI_WITH_TEST_ENGINE=ONwas set for the published wheel); the Python-side flag just activates it. -
hello_imgui.get_imgui_test_engine()(.venv/Lib/site-packages/imgui_bundle/hello_imgui.pyi:3355) — returns the liveTestEngineinstance afteruse_imgui_test_engine = True. Verified callable. -
RunnerCallbacks.register_tests: VoidFunction(.venv/Lib/site-packages/imgui_bundle/hello_imgui.pyi:1809) — the callback thathello_imguiinvokes at startup to let the app register tests viaimgui.test_engine.register_test(engine, group, name). The demo at.venv/Lib/site-packages/imgui_bundle/demos_python/demos_immapp/demo_testengine.pyshows the full pattern. -
imgui_bundle.imgui.test_engine_checks.CHECK(result: bool)— the assertion primitive that emits pass/fail to the engine's result log with file:line traceback. Verified importable. -
The app already uses
hello_imgui.RunnerParams+immapp.run()— the exact integration path the test engine requires:src/gui_2.py:641:self.runner_params = _hi.RunnerParams()src/gui_2.py:684-688:self.runner_params.callbacks.show_gui/show_menus/load_additional_fonts/setup_imgui_style/post_initare setsrc/gui_2.py:1486:immapp.run(app.runner_params, ...)— the main loop entry point- The GIL-transfer mechanism is built into
immapp.runwhenuse_imgui_test_engine = True; no additional threading code is needed on the Python side.
-
HookServer(src/api_hooks.py:857) — the HTTP server on127.0.0.1:8999, started when--enable-test-hooksis passed. Thedo_GETmethod (line 157) anddo_POSTmethod (line 490) use a flatif/elif self.path == "/api/..."dispatch. The server holdsself.app(theAppinstance) and accesses it via_get_app_attr(app, ...)helpers. The_pending_gui_tasksqueue (app_controller.py:900) +_pending_gui_tasks_lock(app_controller.py:822) +_process_pending_gui_tasks()(app_controller.py:1844, called per-frame fromgui_2.py:1759) is the existing thread-safe command queue from HTTP handler thread → main render thread. -
ApiHookClient(src/api_hook_client.py) — the Python client with retry logic, health-check polling,wait_for_server(timeout),push_event(action, payload),get_value(item),set_value(item, value),click(item),wait_for_event(event_type, timeout), etc. Used by alllive_guitests. -
live_guifixture (tests/conftest.py:641) — session-scoped; spawnssloppy.py --enable-test-hooks --config=<temp>as a subprocess; pollshttp://127.0.0.1:8999/statusuntil ready; yields a_LiveGuiHandlewith.client(anApiHookClient),.process,.workspace. The fixture's subprocess args are atconftest.py:792:gui_args = ["uv", "run", "python", "-u", gui_script, "--enable-test-hooks"]. -
sloppy.py(79 lines) — the entry point. CLI flags at lines 31-36:--headless,--web-host,--web-port,--enable-test-hooks,--config. Theelsebranch at line 75 (the normal GUI mode) callsfrom src.gui_2 import main; main(). -
AppController.test_hooks_enabled(src/app_controller.py:1042) — set via"--enable-test-hooks" in sys.argvorSLOP_TEST_HOOKS=1env var. Same pattern works for--enable-test-engine.
Gaps to Fill (This Track's Scope)
-
GAP-1: The test engine is not enabled.
runner_params.use_imgui_test_engineis never set toTrue. Nocallbacks.register_testscallback exists. The engine's scenario thread + GIL-transfer mechanism are dormant. -
GAP-2: No
/api/test_engine/*bridge endpoints. TheHookServerhas no way for the test process to queue a test, poll results, or abort a running test. The test engine API (queue_test,get_result_summary,is_test_queue_empty,abort_current_test) is only accessible from inside the GUI process — not from the HTTP boundary. -
GAP-3: No
ApiHookClientmethods for test engine operations. The client hasclick,set_value,push_event,wait_for_event— but noqueue_test,wait_for_test_results,get_test_results. -
GAP-4:
live_guifixture doesn't pass--enable-test-engine. The subprocess atconftest.py:792only passes--enable-test-hooks. Without the engine flag, the engine won't activate even after GAP-1 is fixed. -
GAP-5: No smoke test proving the end-to-end threading model works. The test engine's scenario thread + GIL transfer is the highest-risk piece. A minimal smoke test (register a trivial test that clicks a known button + asserts a state change, queue it via the API, poll for results, assert pass) is needed to prove the bridge works before Track 2 migrates real tests.
Architecture: Why the API hooks + test engine compose
pytest test process
└── ApiHookClient (HTTP :8999) ← single communication boundary (KEPT)
└── HookServer.do_POST ← new /api/test_engine/* endpoints
└── imgui.test_engine.queue_test(engine, test) ← schedules on engine
└── TestContext.test_func(ctx) ← runs on engine scenario thread
└── ctx.item_click("**/Label") ← posts simulated input to GUI thread
└── GUI render thread processes the simulated event
└── _process_pending_gui_tasks() still runs per-frame
(existing queue; unaffected; two separate injection paths)
The test engine's test_func runs on its own thread (the scenario thread). The ctx.* primitives post simulated input events to the ImGui input queue on the GUI render thread. This is the same destination as real user input and the same destination as _pending_gui_tasks — but a different injection mechanism. The two paths are independent; they don't share state, locks, or queues. The test engine doesn't touch _pending_gui_tasks and vice versa.
The GIL-transfer caveat (documented at the top of test_engine.pyi) is handled by hello_imgui/immapp when use_imgui_test_engine = True — the C++ layer transfers the GIL between the main thread and the scenario thread. No additional Python-side threading code is needed. The test_func callback runs with the GIL held; it can safely call ctx.* primitives (which are C++ nanobind calls that release the GIL during the simulated input wait).
Goals
-
G1.
sloppy.pyaccepts--enable-test-engineCLI flag; when set,App.run()setsrunner_params.use_imgui_test_engine = True+ assignsrunner_params.callbacks.register_teststo a method that registers tests. -
G2.
Apphas a_register_imgui_tests(self)method (called byhello_imguiat startup via theregister_testscallback) that registers at least one smoke test ("Smoke Tests", "Click Increment Button") viaimgui.test_engine.register_test(engine, group, name). The smoke test'stest_func(ctx)callsctx.set_ref("...")+ctx.item_click("**/...")+CHECK(...). -
G3.
HookServer(insrc/api_hooks.py) has 4 new endpoints:POST /api/test_engine/queue— body{"group": "...", "name": "..."}; finds the test by group+name viaimgui.test_engine.find_test_by_name(engine, group, name); callsqueue_test(engine, test); responds{"status": "queued"}.GET /api/test_engine/status— callsis_test_queue_empty(engine); responds{"queue_empty": true/false}.GET /api/test_engine/results— callsget_result_summary(engine, out_results); responds{"count_tested": N, "count_success": N, "count_in_queue": N}.POST /api/test_engine/abort— callsabort_current_test(engine); responds{"status": "aborted"}.
-
G4.
ApiHookClient(insrc/api_hook_client.py) has 4 new methods:queue_test(group: str, name: str) -> dict— POST to/api/test_engine/queue.get_test_status() -> dict— GET/api/test_engine/status.get_test_results() -> dict— GET/api/test_engine/results.wait_for_test_results(timeout: float = 30.0) -> dict— pollsget_test_status()untilqueue_empty == Trueor timeout; then returnsget_test_results().
-
G5. The
live_guifixture passes--enable-test-enginein addition to--enable-test-hooksin the subprocess args (conftest.py:792). The engine activates on everylive_guitest run. -
G6. A smoke test in
tests/test_test_engine_smoke.pythat:- Uses the
live_guifixture. - Queues the smoke test via
client.queue_test("Smoke Tests", "Click Increment Button"). - Polls via
client.wait_for_test_results(timeout=30). - Asserts
results["count_success"] >= 1andresults["count_tested"] >= 1. This proves the full bridge works: pytest → HTTP → HookServer → engine → scenario thread →ctx.item_click→ GUI thread → state change →CHECK→ result log →get_result_summary→ HTTP → pytest assert.
- Uses the
-
G7. End-of-track report at
docs/reports/TRACK_COMPLETION_test_engine_integration_20260627.mddocumenting: what shipped, the threading model verification, any GIL-transfer issues encountered, and the handoff to Track 2 (docking test migration).
Functional Requirements
FR1: --enable-test-engine CLI flag
sloppy.py: addparser.add_argument("--enable-test-engine", action="store_true", help="Enable the Dear ImGui Test Engine for automated UI testing")alongside the existing--enable-test-hooksflag (line 35).src/app_controller.py: addself.test_engine_enabled: bool = ("--enable-test-engine" in sys.argv)near line 1042 (same pattern astest_hooks_enabled).src/gui_2.pyApp.run()(line 619): between theRunnerParams()construction (line 641) and thecallbacks.show_gui = ...assignments (line 684), add:This is guarded by the flag so normal runs are unaffected.if getattr(self.controller, "test_engine_enabled", False): self.runner_params.use_imgui_test_engine = True self.runner_params.callbacks.register_tests = self._register_imgui_tests
FR2: App._register_imgui_tests(self) method
- New method on
Appinsrc/gui_2.py(near the other callback registrations, ~line 700):The exact button + state to click + verify is determined during implementation by inspecting the running GUI's item tree (usedef _register_imgui_tests(self) -> None: """Called by hello_imgui at startup to register ImGui Test Engine tests. Reads the live engine via hello_imgui.get_imgui_test_engine(). [C: src/gui_2.py:App.run (via callbacks.register_tests)] """ from imgui_bundle import hello_imgui from imgui_bundle.imgui import test_engine engine = hello_imgui.get_imgui_test_engine() # Smoke test: click a known button and verify state change test = test_engine.register_test(engine, "Smoke Tests", "Click Increment Button") def smoke_func(ctx) -> None: from imgui_bundle.imgui.test_engine_checks import CHECK ctx.set_ref("...") # TODO: set to a known window ctx.item_click("**/...") # TODO: click a known button CHECK(True) # TODO: verify state change test.test_func = smoke_funcctx.window_info/imgui.show_id_stack_tool_windowto find labels). The smoke test should click something harmless (e.g., a tab switch, a checkbox toggle) and verify the state changed.
FR3: /api/test_engine/* endpoints in HookServer
- In
src/api_hooks.pydo_POST(line 490): add 2 newelifbranches forPOST /api/test_engine/queueandPOST /api/test_engine/abort. - In
src/api_hooks.pydo_GET(line 157): add 2 newelifbranches forGET /api/test_engine/statusandGET /api/test_engine/results. - All 4 endpoints guard on
test_engine_enabled— if the engine is not active, respond{"error": "test engine not enabled", "enabled": false}with HTTP 503. - The engine instance is obtained via
hello_imgui.get_imgui_test_engine()inside the handler (lazy import; the handler runs on the HTTP thread, butget_imgui_test_engine()is a C++ accessor that returns a pointer — safe to call from any thread).
FR4: ApiHookClient methods
- In
src/api_hook_client.py: add 4 methods per G4. Follow the existing method pattern (e.g.,get_status,push_event): construct the URL,requests.post/get, retry on connection error, parse JSON, return the dict.
FR5: live_gui fixture update
- In
tests/conftest.py:792: changegui_argsto include"--enable-test-engine"when the fixture spawns the subprocess. The flag flows through toAppController.test_engine_enabled→App.run()→runner_params.use_imgui_test_engine = True.
FR6: Smoke test
tests/test_test_engine_smoke.py(NEW) — 2-3 tests:test_engine_enabled:client.get_value("test_engine_enabled")returns True (or verify via a new gettable field).test_queue_and_run_smoke_test: queue the smoke test, poll for results, assert success.test_engine_results_shape:get_test_results()returns the expected dict shape.
Non-Functional Requirements
- 1-space indentation for all Python code.
- No comments in body per AGENTS.md.
- CRLF line endings preserved.
- Atomic per-task commits.
- Thread safety: the
test_funcruns on the engine scenario thread. It must NOT directly mutateApporAppControllerstate — it must usectx.*primitives (which post simulated input to the GUI thread). Reading state viahello_imgui.get_imgui_test_engine()or engine queries (ctx.item_info,ctx.window_info) is safe. TheCHECK()assertion runs on the scenario thread but only writes to the engine's result log (thread-safe C++ structure). - No
live_guiregression: the--enable-test-engineflag must not affect normal GUI behavior whenlive_guitests are NOT using the engine. The engine's scenario thread is idle when no tests are queued. Theshow_test_engine_windowspanel is NOT shown by default (only via explicit call). - Performance: the engine adds a per-frame overhead when active. The
fps_idlingsettings inrunner_paramsremain unchanged. The engine's overhead is sub-millisecond per frame when no tests are running.
Architecture Reference
docs/guide_testing.md— thelive_guifixture, the structural testing contract, the Puppeteer pattern.docs/guide_api_hooks.md— the Hook API surface, the/api/askprotocol, theApiHookClientmethod reference.docs/guide_gui_2.md— theAppclass lifecycle, therunner_paramsconstruction, thecallbackssystem..venv/Lib/site-packages/imgui_bundle/demos_python/demos_immapp/demo_testengine.py— the canonical demo for the test engine integration pattern (register_tests callback + test_func closures + CHECK)..venv/Lib/site-packages/imgui_bundle/imgui/test_engine.pyi— the full API stub (2644 lines). Key sections:TestContextmethods (lines 1445-2096), module-level functions (lines 433-500, 2639+),TestEngineResultSummary(3 fields: count_tested, count_success, count_in_queue)..venv/Lib/site-packages/imgui_bundle/imgui/test_engine_checks.py— theCHECK(result: bool)assertion primitive.conductor/workflow.md"Live_gui Test Fragility" + "Async Setters Need Poll-For-State" — the existing patterns forlive_guitests; the test engine'swait_for_test_resultsreplacestime.sleep+get_valuepolling with a single engine-side poll.
Out of Scope
- Migrating existing
live_guitests to the test engine. That's Track 2 (test_engine_docking_tests_<date>). This track only builds the bridge + proves it works with 1 smoke test. - Visual regression via screenshot capture. That's Track 3 (
test_engine_capture_regression_<date>). Thectx.capture_screenshot_windowAPI is available but not wired in this track. - Headless test execution (no GUI window). The test engine requires a live GLFW window (the scenario thread drives the actual ImGui render loop). Headless mode is a future research item, not this track.
- The test engine's interactive UI panel (
show_test_engine_windows). Not shown by default. Can be added as a debug toggle in a follow-up. - Test engine license audit. Per the stub: "free for individuals, educational, open-source, and small businesses. Paid for larger businesses." This project is personal-use; no audit needed. Flagged for awareness only.
- CI wiring of the test engine. The
live_guifixture already runs in CI via the batched runner. The--enable-test-engineflag is additive. No CI config changes needed. - Touching
src/models.pyor any taxonomy files. Zero overlap with the runningtier2/post_module_taxonomy_de_cruft_20260627branch or theenforcement_gap_closure_20260627track.
Test Suite Audit Context (added 2026-06-27)
A full audit of the test suite was conducted on 2026-06-27 (docs/reports/test_suite_audit_20260627.md). The findings directly inform the test engine campaign's scope and sequencing:
Cruft findings (the upgrade surface)
- 393 test files total, run by
run_tests_batched.pywith a 2-level sort (fixture class → batch group). No assertion-criticality ordering exists. - 6 skip markers — 4 of which are the same root cause (Gemini 503 in
summarize.summarise_file). One track mocking the Gemini API eliminates all 4. - 60 files use
time.sleep(38 of them live_gui) — the anti-pattern explicitly banned inworkflow.md. Each is a latent race condition. The test engine'swait_for_test_results(timeout)replaces these. - ~12-14 one-shot phase tests are cruft (verifying completed phases like
test_phase_3_final_verify.py,test_code_path_audit_phase78.py). - 3 redundant clusters: history (5 files), theme (6 files), markdown tables (5 files) — likely overlapping coverage.
- The
corebatch is 245 files (62% of the suite) in a single xdist run — the bottleneck for targeted verification.
Test engine upgrade candidates (27 of 58 live_gui tests)
These tests exercise interactions the Hook API cannot express well (docking, focus, panel visibility, pop-out, keyboard). The test engine's ctx.dock_into, ctx.window_focus, ctx.window_resize, ctx.key_press, ctx.capture_screenshot_window would upgrade them:
- Docking/layout:
test_workspace_profiles_sim.py,test_auto_switch_sim.py,test_preset_windows_layout.py,test_gui_text_viewer.py - Pop-out panels:
test_task_dag_popout_sim.py,test_usage_analytics_popout_sim.py - Command palette + keyboard:
test_command_palette_sim.py,test_undo_redo_sim.py - MMA UI flows:
test_mma_step_mode_sim.py,test_mma_concurrent_tracks_sim.py,test_visual_mma.py,test_visual_sim_mma_v2.py - Visual regression candidates:
test_visual_orchestration.py,test_visual_sim_gui_ux.py,test_live_markdown_render.py,test_gui_stress_performance.py - Hook API integration:
test_hooks.py,test_reset_session_clears_mma_and_rag.py,test_live_workflow.py,test_extended_sims.py - Other UI interactions:
test_gui_context_presets.py,test_tool_management_layout.py,test_selectable_ui.py,test_saved_presets_sim.py,test_system_prompt_sim.py,test_z_negative_flows.py
~44 live_gui tests are fine as-is (provider tests, API endpoint tests, model/logic tests) — the test engine adds no value for pure-logic tests.
New test capabilities enabled ONLY by the test engine
- Drag-and-drop docking (
ctx.dock_into) - Window focus order (
ctx.window_focus) - Window resize (
ctx.window_resize) - Keyboard shortcuts (
ctx.key_press— Ctrl+Z, Ctrl+Shift+P, etc.) - Tab close (
ctx.tab_close) - Screenshot visual regression (
ctx.capture_screenshot_window+ baseline diff) - Tree open/close (
ctx.item_open_all) - Multi-step input (
ctx.key_chars+ctx.key_press(Enter)) - Item hover + tooltip (
ctx.item_hold) - Table column resize (
ctx.table_resize_column)
Proposed ordering taxonomy (assertion-criticality-based)
The audit proposes a 3-dimension sort: (criticality, fixture_class, subsystem) with 6 criticality levels:
| Level | Name | Description | Approx count |
|---|---|---|---|
| C0 | Smoke | "Does the app start and respond?" | ~3 |
| C1 | Structural | "Do core subsystems exist and have the right shape?" | ~45 |
| C2 | Behavioral | "Do subsystems work in isolation?" | ~200 |
| C3 | Integration | "Do subsystems compose correctly?" | ~50 |
| C4 | UI/Visual | "Does the GUI render + respond to user input?" | 27 (test engine candidates) |
| C5 | Stress/Perf | "Does it hold under load?" | ~8 |
The key insight: the current live_gui tier (58 tests) is a monolithic batch mixing C0/C3/C4/C5. Splitting by criticality enables fast-fail (C0 runs first; if it fails, skip the rest) + targeted verification (run only C4-ui when testing a GUI change).
Recommended campaign sequence (informed by the audit)
test_engine_integration_20260627(this track) — build the bridgetest_suite_cruft_cleanup_<date>(new, not yet initialized) — delete one-shot cruft, fix Gemini 503 skips, consolidate redundant clusters, replacetime.sleepwith poll loopstest_ordering_taxonomy_<date>(new, not yet initialized) — add the criticality dimension to the batched runner (categorizer.py+batcher.py+test_categories.toml)test_engine_migration_<date>(Campaign A Track 2) — migrate the 27 high-value live_gui tests to the test engine, re-classifying them as C4-ui in the new ordering
Full audit at: docs/reports/test_suite_audit_20260627.md