diff --git a/conductor/tracks/test_engine_integration_20260627/metadata.json b/conductor/tracks/test_engine_integration_20260627/metadata.json new file mode 100644 index 00000000..20145d51 --- /dev/null +++ b/conductor/tracks/test_engine_integration_20260627/metadata.json @@ -0,0 +1,107 @@ +{ + "track_id": "test_engine_integration_20260627", + "name": "ImGui Test Engine Integration (Bridge via API Hooks)", + "status": "active", + "branch": "master", + "created": "2026-06-27", + "owner": "Tier 1 (initialized); implementation delegated to Tier 2/3.", + "blocked_by": [], + "blocks": ["test_engine_docking_tests (Track 2)", "test_engine_capture_regression (Track 3)"], + "scope": { + "new_files": [ + "tests/test_test_engine_smoke.py", + "docs/reports/TRACK_COMPLETION_test_engine_integration_20260627.md" + ], + "modified_files": [ + "sloppy.py (add --enable-test-engine CLI flag)", + "src/app_controller.py (add test_engine_enabled field)", + "src/gui_2.py (enable engine in App.run + _register_imgui_tests method)", + "src/api_hooks.py (4 new /api/test_engine/* endpoints)", + "src/api_hook_client.py (4 new client methods)", + "tests/conftest.py (pass --enable-test-engine in live_gui fixture)", + "conductor/tracks.md (add row)", + "conductor/chronology.md (prepend row)" + ], + "deleted_files": [] + }, + "estimated_effort": { + "method": "scope (per workflow.md Tier 1 Track Initialization Rules. NO day estimates.)", + "phase_1": "4 tasks: 1 failing test + 1 CLI flag + 1 engine enable + 1 manual verification", + "phase_2": "4 tasks: 1 failing tests + 4 endpoints + 4 client methods + green verification", + "phase_3": "2 tasks: 1 conftest update + 1 full smoke test verification", + "phase_4": "3 tasks: 1 end-of-track report + 1 state update + 1 user sign-off" + }, + "verification_criteria": [ + "G1: sloppy.py accepts --enable-test-engine; when set, runner_params.use_imgui_test_engine = True + callbacks.register_tests assigned", + "G2: App._register_imgui_tests exists + registers at least 1 smoke test via imgui.test_engine.register_test", + "G3: HookServer has 4 new /api/test_engine/* endpoints (queue, status, results, abort)", + "G4: ApiHookClient has 4 new methods (queue_test, get_test_status, get_test_results, wait_for_test_results)", + "G5: live_gui fixture passes --enable-test-engine in subprocess args", + "G6: tests/test_test_engine_smoke.py has >=3 tests; all pass (engine enabled + queue+run smoke + results shape)", + "G7: docs/reports/TRACK_COMPLETION_test_engine_integration_20260627.md exists; documents threading model verification + Track 2 handoff", + "VC_parallel_safe": "ZERO file overlap with tier2/post_module_taxonomy_de_cruft_20260627 (touching sloppy.py, gui_2.py:641-700, api_hooks.py, api_hook_client.py, conftest.py — none of which Tier 2 touches) or enforcement_gap_closure_20260627 (touching scripts/audit_*, python.md — zero overlap)" + ], + "regressions_and_pre_existing_failures": [], + "pre_existing_failures_remaining": [], + "deferred_to_followup_tracks": [ + { + "title": "Track 2: test_engine_docking_tests", + "description": "Migrate docking/focus/panel tests (test_workspace_profiles_restoration, test_auto_switch_sim, etc.) to use ctx.dock_into, ctx.window_focus, ctx.window_resize. The bridge built in this track enables it.", + "track_status": "planned (Track 2 of 3)" + }, + { + "title": "Track 3: test_engine_capture_regression", + "description": "Visual regression via ctx.capture_screenshot_window + baseline PNG diff. The capture API is available but not wired in this track.", + "track_status": "planned (Track 3 of 3)" + }, + { + "title": "Headless test execution", + "description": "The test engine requires a live GLFW window. Headless mode (no window) is a future research item; the engine's scenario thread drives the actual render loop.", + "track_status": "not yet initialized; research item" + }, + { + "title": "Interactive test engine panel", + "description": "show_test_engine_windows(engine, True) opens the engine's debug UI. Not shown by default; can be added as a debug toggle in a follow-up.", + "track_status": "not yet initialized" + } + ], + "risk_register": [ + { + "id": "R1", + "description": "GIL-transfer crash: the test engine's scenario thread calls Python test_func from a different thread; if the GIL transfer mechanism in hello_imgui/immapp doesn't work with the app's existing thread layout, the app crashes", + "likelihood": "medium", + "impact": "hard blocker; the entire test engine approach is invalid if the threading model doesn't work", + "mitigation": "Phase 1 Task 1.4 is a manual verification checkpoint that catches this before any further work. If it crashes, STOP and report to user. The demo_testengine.py proves the mechanism works for simple apps; the risk is specific to this app's thread layout (AppController, SyncEventQueue, etc.)" + }, + { + "id": "R2", + "description": "Label path mismatch: the smoke test's ctx.set_ref('###manual slop') + ctx.item_click('**/Session') may not match the actual label tree", + "likelihood": "high", + "impact": "smoke test fails with 'item not found'; not a crash, just a wrong path", + "mitigation": "Use imgui.show_id_stack_tool_window() or ctx.window_info() to find the correct labels during implementation. The label tree is deterministic (same build, same layout). Once found, the path is stable." + }, + { + "id": "R3", + "description": "Engine overhead degrades live_gui test performance", + "likelihood": "low", + "impact": "live_gui tests take longer; batch run exceeds timeout", + "mitigation": "The engine is idle when no tests are queued (sub-ms per-frame overhead). The existing fps_idling settings are unchanged. If measurable, the --enable-test-engine flag can be made conditional (only passed when running test_test_engine_* files)." + }, + { + "id": "R4", + "description": "test_func accesses App state from the scenario thread, causing a race with the GUI render thread", + "likelihood": "medium", + "impact": "intermittent test failures or state corruption", + "mitigation": "The spec FR2 + plan Task 1.3 explicitly document: test_func must NOT directly mutate App/AppController state; it must use ctx.* primitives (which post simulated input to the GUI thread). Reading via ctx.item_info / ctx.window_info is safe (C++ accessors). CHECK() runs on the scenario thread but only writes to the engine's C++ result log (thread-safe)." + } + ], + "campaign": { + "name": "Test Engine Campaign (3 tracks)", + "tracks": [ + "test_engine_integration_20260627 (THIS TRACK; bridge + smoke test)", + "test_engine_docking_tests (Track 2; migrate docking/focus/panel tests)", + "test_engine_capture_regression (Track 3; visual regression via screenshot capture)" + ], + "campaign_rationale": "The test engine enables high-fidelity simulation of docking, focus, panel visibility, drag-and-drop, and keyboard input that the current Hook API cannot express. The campaign is split into 3 tracks to isolate risk: Track 1 proves the threading model + bridge work; Track 2 migrates the high-value docking tests; Track 3 adds visual regression. Each track is independently shippable." + } +} \ No newline at end of file diff --git a/conductor/tracks/test_engine_integration_20260627/plan.md b/conductor/tracks/test_engine_integration_20260627/plan.md new file mode 100644 index 00000000..8727a368 --- /dev/null +++ b/conductor/tracks/test_engine_integration_20260627/plan.md @@ -0,0 +1,163 @@ +# Plan: ImGui Test Engine Integration (Bridge via API Hooks) + +Track: `test_engine_integration_20260627` +Branch: master (parallel-safe; touches `sloppy.py`, `src/gui_2.py`, `src/app_controller.py`, `src/api_hooks.py`, `src/api_hook_client.py`, `tests/conftest.py`, new `tests/test_test_engine_smoke.py` — zero overlap with the running tier2 taxonomy branch or the enforcement_gap_closure track) +Spec: `conductor/tracks/test_engine_integration_20260627/spec.md` + +All Python edits use 1-space indentation. No comments in body. CRLF preserved. + +--- + +## Phase 1: Enable the Test Engine in the App + +Focus: Add `--enable-test-engine` CLI flag, set `runner_params.use_imgui_test_engine`, add the `register_tests` callback with a placeholder smoke test. + +- [ ] Task 1.1: Write failing test for `--enable-test-engine` flag + engine activation + - **WHERE:** `tests/test_test_engine_smoke.py` (NEW file) + - **WHAT:** Test 1: `test_engine_enabled` — start `live_gui` (which will pass `--enable-test-engine`), verify the engine is active by calling `client.get_test_status()` (new method, implemented in Phase 3) and asserting `queue_empty == True` (engine is running, no tests queued). This test will FAIL before Phase 1 + Phase 3 land (the endpoint doesn't exist yet). + - **HOW:** Use the `live_gui` fixture. Call `client.get_test_status()`. Assert the response has a `queue_empty` field. (The method is added in Phase 3; the test is written first per TDD.) + - **SAFETY:** No `live_gui` state mutation; just a GET request. + - **COMMIT:** `test(smoke): add failing test for test engine activation` + - **GIT NOTE:** Red-phase test for the `--enable-test-engine` flag + engine activation. + +- [ ] Task 1.2: Add `--enable-test-engine` CLI flag to `sloppy.py` + `AppController` + - **WHERE:** `sloppy.py:35` (add arg), `src/app_controller.py:1042` (add `test_engine_enabled` field) + - **WHAT:** + 1. `sloppy.py`: add `parser.add_argument("--enable-test-engine", action="store_true", help="Enable Dear ImGui Test Engine for automated UI testing")` after the `--enable-test-hooks` line. + 2. `src/app_controller.py:1042`: add `self.test_engine_enabled: bool = ("--enable-test-engine" in sys.argv)` after the `test_hooks_enabled` line. + - **HOW:** Use `manual-slop_edit_file` MCP tool. 1-space indent. + - **SAFETY:** The flag is opt-in; normal runs are unaffected. + - **COMMIT:** `feat(cli): add --enable-test-engine flag` + - **GIT NOTE:** CLI flag for test engine; mirrors the --enable-test-hooks pattern at app_controller.py:1042. + +- [ ] Task 1.3: Enable the engine in `App.run()` + add `_register_imgui_tests` callback + - **WHERE:** `src/gui_2.py:641` (after `RunnerParams()` construction) + `src/gui_2.py:~700` (new `_register_imgui_tests` method) + - **WHAT:** + 1. In `App.run()` between line 641 (`self.runner_params = _hi.RunnerParams()`) and line 684 (`callbacks.show_gui = ...`), add: + ```python + 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 + ``` + 2. Add `_register_imgui_tests(self)` method on `App` (after `_post_init`, ~line 700): + ```python + def _register_imgui_tests(self) -> None: + from imgui_bundle import hello_imgui + from imgui_bundle.imgui import test_engine + engine = hello_imgui.get_imgui_test_engine() + test = test_engine.register_test(engine, "Smoke Tests", "Tab Switch") + def smoke_func(ctx) -> None: + from imgui_bundle.imgui.test_engine_checks import CHECK + ctx.set_ref("###manual slop") + ctx.item_click("**/Session") + CHECK(True) + test.test_func = smoke_func + ``` + The exact `set_ref` + `item_click` targets are determined during implementation by inspecting the running GUI's label tree. The smoke test should click a harmless tab (e.g., switch to "Session" tab) and `CHECK(True)` as a placeholder assertion. The real assertion (verify the tab actually switched) is added once the label path is confirmed. + - **HOW:** Use `manual-slop_edit_file` / `manual-slop_py_update_definition` MCP tool. 1-space indent. + - **SAFETY:** Guarded by `test_engine_enabled`; normal runs skip this entirely. The `register_tests` callback is only called by `hello_imgui` when `use_imgui_test_engine = True`. + - **COMMIT:** `feat(gui): enable test engine + register smoke test via callbacks.register_tests` + - **GIT NOTE:** Activates the test engine when --enable-test-engine is set; registers a placeholder smoke test. + +- [ ] Task 1.4: Verify the engine activates (manual) + - **WHAT:** Run `uv run python sloppy.py --enable-test-hooks --enable-test-engine` locally. Verify the app starts without crashing (the GIL-transfer mechanism works). Verify `hello_imgui.get_imgui_test_engine()` returns a non-None engine. This is a manual checkpoint before proceeding to Phase 2. + - **COMMIT:** (no commit; manual verification checkpoint) + - **GIT NOTE:** Manual verification that the engine + GIL transfer works with the app's existing thread layout. + +--- + +## Phase 2: Build the API Hooks Bridge + +Focus: Add the 4 `/api/test_engine/*` endpoints to `HookServer` + the 4 methods to `ApiHookClient`. + +- [ ] Task 2.1: Write failing tests for the 4 new `ApiHookClient` methods + - **WHERE:** `tests/test_test_engine_smoke.py` (append to the file from Task 1.1) + - **WHAT:** 2 more tests: + - `test_queue_and_run_smoke_test`: queue the smoke test via `client.queue_test("Smoke Tests", "Tab Switch")`, poll via `client.wait_for_test_results(timeout=30)`, assert `results["count_success"] >= 1` and `results["count_tested"] >= 1`. + - `test_engine_results_shape`: call `client.get_test_results()`, assert the response dict has keys `count_tested`, `count_success`, `count_in_queue`. + - **HOW:** Use `live_gui` fixture. These tests fail until Phase 2 + Phase 3 land (the client methods + endpoints don't exist yet). + - **SAFETY:** The smoke test queues a harmless tab-switch; no destructive state change. + - **COMMIT:** `test(smoke): add failing tests for queue_test + wait_for_test_results + get_test_results` + - **GIT NOTE:** Red-phase tests for the 4 new ApiHookClient methods. + +- [ ] Task 2.2: Add the 4 `/api/test_engine/*` endpoints to `HookServer` + - **WHERE:** `src/api_hooks.py` — `do_GET` (line 157) + `do_POST` (line 490) + - **WHAT:** Add 4 new `elif` branches: + 1. `do_GET`: `elif self.path == "/api/test_engine/status":` — lazy-import `hello_imgui` + `test_engine`; get engine via `hello_imgui.get_imgui_test_engine()`; call `test_engine.is_test_queue_empty(engine)`; respond `{"queue_empty": bool}`. + 2. `do_GET`: `elif self.path == "/api/test_engine/results":` — get engine; create `TestEngineResultSummary()`; call `test_engine.get_result_summary(engine, out_results)`; respond `{"count_tested": N, "count_success": N, "count_in_queue": N}`. + 3. `do_POST`: `elif self.path == "/api/test_engine/queue":` — body `{"group": "...", "name": "..."}`; get engine; find test via `test_engine.find_test_by_name(engine, group, name)`; if found, `test_engine.queue_test(engine, test)`; respond `{"status": "queued"}` or `{"error": "test not found"}` (404). + 4. `do_POST`: `elif self.path == "/api/test_engine/abort":` — get engine; `test_engine.abort_current_test(engine)`; respond `{"status": "aborted"}`. + - **HOW:** Follow the existing endpoint pattern (lines 499-505 for POST, lines 231-241 for GET). Use `_get_app_attr(app, "controller")` to check `test_engine_enabled`; if not enabled, respond 503. Use `json.dumps(...)` for the response body. 1-space indent. + - **SAFETY:** The endpoints run on the HTTP handler thread. `hello_imgui.get_imgui_test_engine()` is a C++ accessor (thread-safe). `queue_test` / `is_test_queue_empty` / `get_result_summary` are thread-safe C++ engine operations (the engine is designed for cross-thread test scheduling). `abort_current_test` is also thread-safe. + - **COMMIT:** `feat(api_hooks): add /api/test_engine/* bridge endpoints` + - **GIT NOTE:** 4 new endpoints: queue, status, results, abort; bridge the test process to the engine via HTTP. + +- [ ] Task 2.3: Add the 4 new methods to `ApiHookClient` + - **WHERE:** `src/api_hook_client.py` (after the existing methods, ~line 500) + - **WHAT:** 4 new methods: + 1. `queue_test(self, group: str, name: str) -> dict` — POST `/api/test_engine/queue` with `{"group": group, "name": name}`; return the response dict. + 2. `get_test_status(self) -> dict` — GET `/api/test_engine/status`; return `{"queue_empty": bool}`. + 3. `get_test_results(self) -> dict` — GET `/api/test_engine/results`; return `{"count_tested": N, "count_success": N, "count_in_queue": N}`. + 4. `wait_for_test_results(self, timeout: float = 30.0) -> dict` — poll `get_test_status()` every 0.5s until `queue_empty == True` or timeout; then return `get_test_results()`. On timeout, return the last results (with a `timed_out: True` field). + - **HOW:** Follow the existing method pattern (e.g., `get_status` at line 105, `push_event` at line 156). Use `requests.get/post` + retry. 1-space indent. + - **SAFETY:** Pure HTTP client; no thread safety concerns. + - **COMMIT:** `feat(api_hook_client): add queue_test + get_test_status + get_test_results + wait_for_test_results` + - **GIT NOTE:** 4 new client methods mirroring the 4 new endpoints; wait_for_test_results replaces time.sleep+get_value polling. + +- [ ] Task 2.4: Run Phase 2 tests (Green phase) + - **WHAT:** `uv run pytest tests/test_test_engine_smoke.py -v --timeout=60`. All 3 tests must pass. If the smoke test (test_queue_and_run_smoke_test) fails, the most likely cause is the `set_ref` / `item_click` label path being wrong — debug by using `imgui.show_id_stack_tool_window()` or `ctx.window_info("manual slop")` to find the correct label. If the GIL transfer fails, the app will crash — that's a hard blocker; report to user. + - **COMMIT:** `conductor(state): Phase 2 green-phase verification` (or skip if no code changes) + - **GIT NOTE:** Green-phase verification for the 4 new endpoints + 4 new client methods. + +--- + +## Phase 3: Live_gui Fixture + Full Smoke Test + +Focus: Pass `--enable-test-engine` in the `live_gui` fixture + verify the full bridge works end-to-end. + +- [ ] Task 3.1: Update `live_gui` fixture to pass `--enable-test-engine` + - **WHERE:** `tests/conftest.py:792` + - **WHAT:** Change `gui_args = ["uv", "run", "python", "-u", gui_script, "--enable-test-hooks"]` to include `"--enable-test-engine"`: + ```python + gui_args = ["uv", "run", "python", "-u", gui_script, "--enable-test-hooks", "--enable-test-engine"] + ``` + - **HOW:** `manual-slop_edit_file` MCP tool. 1-space indent. + - **SAFETY:** The engine is idle when no tests are queued. Existing `live_gui` tests that don't use the test engine are unaffected (the engine adds sub-ms per-frame overhead). + - **COMMIT:** `test(conftest): pass --enable-test-engine in live_gui fixture` + - **GIT NOTE:** Engine activates on every live_gui run; idle when no tests queued. + +- [ ] Task 3.2: Run the full smoke test suite (Green phase) + - **WHAT:** `uv run pytest tests/test_test_engine_smoke.py -v --timeout=60`. All 3 tests pass. Then run a small batch of existing `live_gui` tests to verify no regression: `uv run pytest tests/test_workspace_profiles_restoration.py tests/test_undo_redo_lifecycle.py -v --timeout=120`. + - **COMMIT:** `conductor(state): Phase 3 green-phase verification` + - **GIT NOTE:** Full bridge verified: pytest → HTTP → HookServer → engine → scenario thread → ctx.item_click → GUI thread → CHECK → results → HTTP → pytest assert. + +--- + +## Phase 4: End-of-Track Report + State Update + +- [ ] Task 4.1: Write end-of-track report + - **WHERE:** `docs/reports/TRACK_COMPLETION_test_engine_integration_20260627.md` (NEW file) + - **WHAT:** Report following the precedent: + - TL;DR + - Phase summary (each phase + commits + status) + - Verification Criteria status (mapped to spec G1-G7) + - Threading model verification (did the GIL transfer work? any crashes? any state-access issues from the scenario thread?) + - The 4 new endpoints + 4 new client methods documented + - The smoke test result + - Handoff to Track 2 (docking test migration) — what's now possible that wasn't before + - Known limitations (engine requires a live window; not headless; the interactive panel is not shown) + - **COMMIT:** `docs(reports): TRACK_COMPLETION_test_engine_integration_20260627` + - **GIT NOTE:** End-of-track report; documents the bridge + threading model verification + Track 2 handoff. + +- [ ] Task 4.2: Update `conductor/tracks.md` + `conductor/chronology.md` + `state.toml` + - **WHAT:** + 1. `state.toml`: mark all phases "completed" with checkpoint SHA; `status = "completed"`. + 2. `conductor/tracks.md`: add row for this track (status "shipped"). + 3. `conductor/chronology.md`: prepend row for `2026-06-27 | test_engine_integration_20260627 | shipped | ...`. + - **COMMIT:** `conductor(state): test_engine_integration_20260627 SHIPPED + TRACK_COMPLETION` + - **GIT NOTE:** Track state + chronology + tracks.md closed out. + +- [ ] Task 4.3: Conductor - User Manual Verification + - **WHAT:** Present the results: the smoke test pass, the threading model verification, the 4 new endpoints, the 4 new client methods. PAUSE for user sign-off. + - **COMMIT:** (no commit; user-confirmation gate) + - **GIT NOTE:** User sign-off record. \ No newline at end of file diff --git a/conductor/tracks/test_engine_integration_20260627/spec.md b/conductor/tracks/test_engine_integration_20260627/spec.md new file mode 100644 index 00000000..f1e9f508 --- /dev/null +++ b/conductor/tracks/test_engine_integration_20260627/spec.md @@ -0,0 +1,187 @@ +# 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_bundle` v1.92.5** (pinned in `pyproject.toml:7`) ships the test engine compiled into the nanobind binary. Verified: `from imgui_bundle import imgui; imgui.test_engine.TestEngine` is a live class; `imgui.test_engine.register_test`, `imgui.test_engine.queue_test`, `imgui.test_engine.get_result_summary`, `imgui.test_engine.TestContext` with `dock_into`, `window_focus`, `item_click`, `capture_screenshot_window`, etc. are all present (verified via `dir()` enumeration — ~95 `TestContext` methods + ~35 module-level functions). The `.pyi` stub at `.venv/Lib/site-packages/imgui_bundle/imgui/test_engine.pyi` documents 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. When `True`, `hello_imgui`/`immapp` compiles 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=ON` was 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 live `TestEngine` instance after `use_imgui_test_engine = True`. Verified callable. + +- **`RunnerCallbacks.register_tests: VoidFunction`** (`.venv/Lib/site-packages/imgui_bundle/hello_imgui.pyi:1809`) — the callback that `hello_imgui` invokes at startup to let the app register tests via `imgui.test_engine.register_test(engine, group, name)`. The demo at `.venv/Lib/site-packages/imgui_bundle/demos_python/demos_immapp/demo_testengine.py` shows 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_init` are set + - `src/gui_2.py:1486`: `immapp.run(app.runner_params, ...)` — the main loop entry point + - The GIL-transfer mechanism is built into `immapp.run` when `use_imgui_test_engine = True`; no additional threading code is needed on the Python side. + +- **`HookServer`** (`src/api_hooks.py:857`) — the HTTP server on `127.0.0.1:8999`, started when `--enable-test-hooks` is passed. The `do_GET` method (line 157) and `do_POST` method (line 490) use a flat `if/elif self.path == "/api/..."` dispatch. The server holds `self.app` (the `App` instance) and accesses it via `_get_app_attr(app, ...)` helpers. The `_pending_gui_tasks` queue (`app_controller.py:900`) + `_pending_gui_tasks_lock` (`app_controller.py:822`) + `_process_pending_gui_tasks()` (`app_controller.py:1844`, called per-frame from `gui_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 all `live_gui` tests. + +- **`live_gui` fixture** (`tests/conftest.py:641`) — session-scoped; spawns `sloppy.py --enable-test-hooks --config=` as a subprocess; polls `http://127.0.0.1:8999/status` until ready; yields a `_LiveGuiHandle` with `.client` (an `ApiHookClient`), `.process`, `.workspace`. The fixture's subprocess args are at `conftest.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`. The `else` branch at line 75 (the normal GUI mode) calls `from src.gui_2 import main; main()`. + +- **`AppController.test_hooks_enabled`** (`src/app_controller.py:1042`) — set via `"--enable-test-hooks" in sys.argv` or `SLOP_TEST_HOOKS=1` env 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_engine` is never set to `True`. No `callbacks.register_tests` callback exists. The engine's scenario thread + GIL-transfer mechanism are dormant. + +- **GAP-2: No `/api/test_engine/*` bridge endpoints.** The `HookServer` has 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 `ApiHookClient` methods for test engine operations.** The client has `click`, `set_value`, `push_event`, `wait_for_event` — but no `queue_test`, `wait_for_test_results`, `get_test_results`. + +- **GAP-4: `live_gui` fixture doesn't pass `--enable-test-engine`.** The subprocess at `conftest.py:792` only 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.py` accepts `--enable-test-engine` CLI flag; when set, `App.run()` sets `runner_params.use_imgui_test_engine = True` + assigns `runner_params.callbacks.register_tests` to a method that registers tests. + +- **G2.** `App` has a `_register_imgui_tests(self)` method (called by `hello_imgui` at startup via the `register_tests` callback) that registers at least one smoke test ("Smoke Tests", "Click Increment Button") via `imgui.test_engine.register_test(engine, group, name)`. The smoke test's `test_func(ctx)` calls `ctx.set_ref("...")` + `ctx.item_click("**/...")` + `CHECK(...)`. + +- **G3.** `HookServer` (in `src/api_hooks.py`) has 4 new endpoints: + - `POST /api/test_engine/queue` — body `{"group": "...", "name": "..."}`; finds the test by group+name via `imgui.test_engine.find_test_by_name(engine, group, name)`; calls `queue_test(engine, test)`; responds `{"status": "queued"}`. + - `GET /api/test_engine/status` — calls `is_test_queue_empty(engine)`; responds `{"queue_empty": true/false}`. + - `GET /api/test_engine/results` — calls `get_result_summary(engine, out_results)`; responds `{"count_tested": N, "count_success": N, "count_in_queue": N}`. + - `POST /api/test_engine/abort` — calls `abort_current_test(engine)`; responds `{"status": "aborted"}`. + +- **G4.** `ApiHookClient` (in `src/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` — polls `get_test_status()` until `queue_empty == True` or timeout; then returns `get_test_results()`. + +- **G5.** The `live_gui` fixture passes `--enable-test-engine` in addition to `--enable-test-hooks` in the subprocess args (`conftest.py:792`). The engine activates on every `live_gui` test run. + +- **G6.** A smoke test in `tests/test_test_engine_smoke.py` that: + 1. Uses the `live_gui` fixture. + 2. Queues the smoke test via `client.queue_test("Smoke Tests", "Click Increment Button")`. + 3. Polls via `client.wait_for_test_results(timeout=30)`. + 4. Asserts `results["count_success"] >= 1` and `results["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. + +- **G7.** End-of-track report at `docs/reports/TRACK_COMPLETION_test_engine_integration_20260627.md` documenting: 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`: add `parser.add_argument("--enable-test-engine", action="store_true", help="Enable the Dear ImGui Test Engine for automated UI testing")` alongside the existing `--enable-test-hooks` flag (line 35). +- `src/app_controller.py`: add `self.test_engine_enabled: bool = ("--enable-test-engine" in sys.argv)` near line 1042 (same pattern as `test_hooks_enabled`). +- `src/gui_2.py` `App.run()` (line 619): between the `RunnerParams()` construction (line 641) and the `callbacks.show_gui = ...` assignments (line 684), add: + ```python + 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 + ``` + This is guarded by the flag so normal runs are unaffected. + +### FR2: `App._register_imgui_tests(self)` method + +- New method on `App` in `src/gui_2.py` (near the other callback registrations, ~line 700): + ```python + def _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_func + ``` + The exact button + state to click + verify is determined during implementation by inspecting the running GUI's item tree (use `ctx.window_info` / `imgui.show_id_stack_tool_window` to 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.py` `do_POST` (line 490): add 2 new `elif` branches for `POST /api/test_engine/queue` and `POST /api/test_engine/abort`. +- In `src/api_hooks.py` `do_GET` (line 157): add 2 new `elif` branches for `GET /api/test_engine/status` and `GET /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, but `get_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`: change `gui_args` to include `"--enable-test-engine"` when the fixture spawns the subprocess. The flag flows through to `AppController.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_func` runs on the engine scenario thread. It must NOT directly mutate `App` or `AppController` state — it must use `ctx.*` primitives (which post simulated input to the GUI thread). Reading state via `hello_imgui.get_imgui_test_engine()` or engine queries (`ctx.item_info`, `ctx.window_info`) is safe. The `CHECK()` assertion runs on the scenario thread but only writes to the engine's result log (thread-safe C++ structure). +- **No `live_gui` regression:** the `--enable-test-engine` flag must not affect normal GUI behavior when `live_gui` tests are NOT using the engine. The engine's scenario thread is idle when no tests are queued. The `show_test_engine_windows` panel is NOT shown by default (only via explicit call). +- **Performance:** the engine adds a per-frame overhead when active. The `fps_idling` settings in `runner_params` remain unchanged. The engine's overhead is sub-millisecond per frame when no tests are running. + +## Architecture Reference + +- **`docs/guide_testing.md`** — the `live_gui` fixture, the structural testing contract, the Puppeteer pattern. +- **`docs/guide_api_hooks.md`** — the Hook API surface, the `/api/ask` protocol, the `ApiHookClient` method reference. +- **`docs/guide_gui_2.md`** — the `App` class lifecycle, the `runner_params` construction, the `callbacks` system. +- **`.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: `TestContext` methods (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`** — the `CHECK(result: bool)` assertion primitive. +- **`conductor/workflow.md`** "Live_gui Test Fragility" + "Async Setters Need Poll-For-State" — the existing patterns for `live_gui` tests; the test engine's `wait_for_test_results` replaces `time.sleep` + `get_value` polling with a single engine-side poll. + +## Out of Scope + +- **Migrating existing `live_gui` tests to the test engine.** That's Track 2 (`test_engine_docking_tests_`). 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_`). The `ctx.capture_screenshot_window` API 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_gui` fixture already runs in CI via the batched runner. The `--enable-test-engine` flag is additive. No CI config changes needed. +- **Touching `src/models.py` or any taxonomy files.** Zero overlap with the running `tier2/post_module_taxonomy_de_cruft_20260627` branch or the `enforcement_gap_closure_20260627` track. \ No newline at end of file diff --git a/conductor/tracks/test_engine_integration_20260627/state.toml b/conductor/tracks/test_engine_integration_20260627/state.toml new file mode 100644 index 00000000..23faa5b5 --- /dev/null +++ b/conductor/tracks/test_engine_integration_20260627/state.toml @@ -0,0 +1,64 @@ +# Track state for test_engine_integration_20260627 +# Initialized by Tier 1 Orchestrator on 2026-06-27. +# Implementation delegated to Tier 2 (autonomous) or Tier 3 worker dispatch. +# This is Track 1 of 3 in the Test Engine Campaign. + +[meta] +track_id = "test_engine_integration_20260627" +name = "ImGui Test Engine Integration (Bridge via API Hooks)" +status = "active" +current_phase = 0 +last_updated = "2026-06-27" + +[blocked_by] +# None. Parallel-safe against tier2/post_module_taxonomy_de_cruft_20260627 +# (zero file overlap: this track touches sloppy.py, gui_2.py:641-700, +# api_hooks.py, api_hook_client.py, conftest.py — none of which Tier 2 touches) +# and enforcement_gap_closure_20260627 (scripts/audit_*, python.md — zero overlap). + +[blocks] +test_engine_docking_tests = "planned (Track 2 of 3 campaign)" +test_engine_capture_regression = "planned (Track 3 of 3 campaign)" + +[phases] +phase_1 = { status = "pending", checkpointsha = "", name = "Enable the Test Engine in the App (CLI flag + runner_params + register_tests callback)" } +phase_2 = { status = "pending", checkpointsha = "", name = "Build the API Hooks Bridge (4 endpoints + 4 client methods)" } +phase_3 = { status = "pending", checkpointsha = "", name = "Live_gui Fixture + Full Smoke Test" } +phase_4 = { status = "pending", checkpointsha = "", name = "End-of-Track Report + State Update + User Sign-off" } + +[tasks] +# Phase 1: enable the engine +t1_1 = { status = "pending", commit_sha = "", description = "Write failing test for --enable-test-engine flag + engine activation (Red phase)" } +t1_2 = { status = "pending", commit_sha = "", description = "Add --enable-test-engine CLI flag to sloppy.py + test_engine_enabled field to AppController" } +t1_3 = { status = "pending", commit_sha = "", description = "Enable engine in App.run() (runner_params.use_imgui_test_engine = True + callbacks.register_tests = self._register_imgui_tests) + add _register_imgui_tests method with smoke test" } +t1_4 = { status = "pending", commit_sha = "", description = "Manual verification: run sloppy.py --enable-test-engine locally; confirm engine activates + no GIL-transfer crash" } +# Phase 2: build the bridge +t2_1 = { status = "pending", commit_sha = "", description = "Write failing tests for queue_test + wait_for_test_results + get_test_results (Red phase)" } +t2_2 = { status = "pending", commit_sha = "", description = "Add 4 /api/test_engine/* endpoints to HookServer (queue, status, results, abort)" } +t2_3 = { status = "pending", commit_sha = "", description = "Add 4 new methods to ApiHookClient (queue_test, get_test_status, get_test_results, wait_for_test_results)" } +t2_4 = { status = "pending", commit_sha = "", description = "Run Phase 2 tests (Green phase); verify all 3 smoke tests pass" } +# Phase 3: live_gui fixture + full smoke test +t3_1 = { status = "pending", commit_sha = "", description = "Update live_gui fixture (conftest.py:792) to pass --enable-test-engine" } +t3_2 = { status = "pending", commit_sha = "", description = "Run full smoke test + regression batch (Green phase)" } +# Phase 4: end-of-track +t4_1 = { status = "pending", commit_sha = "", description = "Write docs/reports/TRACK_COMPLETION_test_engine_integration_20260627.md" } +t4_2 = { status = "pending", commit_sha = "", description = "Update conductor/tracks.md + chronology.md + state.toml -> status='completed'" } +t4_3 = { status = "pending", commit_sha = "", description = "Conductor - User Manual Verification (PAUSE for user sign-off)" } + +[verification] +phase_1_complete = false +phase_2_complete = false +phase_3_complete = false +phase_4_complete = false +engine_activates_without_crash = false +smoke_test_passes = false +no_live_gui_regression = false + +[campaign_context] +# This is Track 1 of 3. The campaign enables high-fidelity UI simulation via the +# Dear ImGui Test Engine, bridged through the existing API hooks layer. +campaign_name = "Test Engine Campaign" +track_1 = "test_engine_integration_20260627 (THIS; bridge + smoke test)" +track_2 = "test_engine_docking_tests (migrate docking/focus/panel tests)" +track_3 = "test_engine_capture_regression (visual regression via screenshot capture)" +key_risk = "R1: GIL-transfer crash if the app's thread layout doesn't work with the engine's scenario thread (mitigated by Phase 1 Task 1.4 manual checkpoint)" \ No newline at end of file