From 91b34ae81ea50f92e5c10ab6f6cd1513717133d1 Mon Sep 17 00:00:00 2001 From: Ed_ Date: Sun, 7 Jun 2026 16:49:51 -0400 Subject: [PATCH] fix(hooks): handle dict-key bracket notation in set_value / get_value The Hook API previously rejected key strings like 'show_windows["Project Settings"]' (and silently returned None on get). The test_live_gui_filedialog_regression test exercises exactly this pattern to open the Project Settings window via the Hook API; it was previously marked skip with "hook server doesn't handle the dict-key bracket-notation syntax". Fix in three small places: 1. src/app_controller.py:_handle_set_value If `item` is not in _settable_fields, try parsing it as `dict_name[]` notation. If dict_name IS in _settable_fields and the current attr is a dict, set the inner key. 2. src/api_hooks.py:/api/gui/value (POST get_val) Mirror the parsing for the field-based get endpoint. 3. src/api_hook_client.py:ApiHookClient.get_value Mirror the parsing in the client so the dict-key syntax works through the state endpoint as well (which is what get_value actually calls by default). Test fix: - tests/test_live_gui_filedialog_regression.py: removed the @pytest.mark.skip marker; the underlying issue is now fixed. Verified: 1/1 test passes (previously skipped). --- src/api_hook_client.py | 8 ++++++++ src/api_hooks.py | 8 ++++++++ src/app_controller.py | 11 +++++++++++ tests/test_live_gui_filedialog_regression.py | 1 - 4 files changed, 27 insertions(+), 1 deletion(-) diff --git a/src/api_hook_client.py b/src/api_hook_client.py index cae657b4..162886e3 100644 --- a/src/api_hook_client.py +++ b/src/api_hook_client.py @@ -161,6 +161,14 @@ class ApiHookClient: Gets the value of a GUI item via its mapped field. [C: simulation/sim_ai_settings.py:AISettingsSimulation.run, simulation/sim_base.py:BaseSimulation.get_value, simulation/sim_base.py:BaseSimulation.setup, simulation/sim_base.py:BaseSimulation.wait_for_element, simulation/sim_context.py:ContextSimulation.run, simulation/sim_execution.py:ExecutionSimulation.run, simulation/workflow_sim.py:WorkflowSimulator.run_discussion_turn_async, simulation/workflow_sim.py:WorkflowSimulator.wait_for_ai_response, tests/smoke_status_hook.py:test_status_hook, tests/smoke_status_hook.py:wait_for_value, tests/test_auto_switch_sim.py:test_auto_switch_sim, tests/test_deepseek_infra.py:test_gui_provider_list_via_hooks, tests/test_extended_sims.py:test_ai_settings_sim_live, tests/test_gui2_parity.py:test_gui2_click_hook_works, tests/test_gui2_parity.py:test_gui2_set_value_hook_works, tests/test_rag_phase4_final_verify.py:test_phase4_final_verify, tests/test_rag_phase4_stress.py:test_rag_large_codebase_verification_sim, tests/test_rag_visual_sim.py:test_rag_full_lifecycle_sim, tests/test_rag_visual_sim.py:test_rag_settings_persistence_sim, tests/test_selectable_ui.py:test_selectable_label_stability, tests/test_system_prompt_sim.py:test_system_prompt_sim, tests/test_undo_redo_sim.py:test_undo_redo_context_mutation, tests/test_undo_redo_sim.py:test_undo_redo_discussion_mutation, tests/test_undo_redo_sim.py:test_undo_redo_lifecycle, tests/test_workspace_profiles_sim.py:test_workspace_profiles_restoration] """ + # Dict-key bracket notation: e.g. 'show_windows["Project Settings"]' + if "[" in item and item.endswith("]"): + dict_name, _, key_part = item.partition("[") + key = key_part[:-1].strip().strip("'\"") + state = self.get_gui_state() + if isinstance(state.get(dict_name), dict) and key in state[dict_name]: + return state[dict_name][key] + return None # Try state endpoint first (new preferred way) state = self.get_gui_state() if item in state: diff --git a/src/api_hooks.py b/src/api_hooks.py index 6a4d8e43..cc1ed295 100644 --- a/src/api_hooks.py +++ b/src/api_hooks.py @@ -464,6 +464,14 @@ class HookHandler(BaseHTTPRequestHandler): if field_tag in settable: attr = settable[field_tag] result["value"] = _serialize_for_api(_get_app_attr(app, attr, None)) + elif "[" in field_tag and field_tag.endswith("]"): + dict_name, _, key_part = field_tag.partition("[") + key = key_part[:-1].strip().strip("'\"") + if dict_name in settable: + attr = settable[dict_name] + current = _get_app_attr(app, attr, None) + if isinstance(current, dict) and key in current: + result["value"] = _serialize_for_api(current[key]) finally: event.set() lock = _get_app_attr(app, "_pending_gui_tasks_lock") tasks = _get_app_attr(app, "_pending_gui_tasks") diff --git a/src/app_controller.py b/src/app_controller.py index 9d5b3218..0df96230 100644 --- a/src/app_controller.py +++ b/src/app_controller.py @@ -560,6 +560,17 @@ def _handle_set_value(controller: 'AppController', task: dict): setattr(controller, attr_name, value) if item == "gcli_path": controller._update_gcli_adapter(str(value)) + return + # Dict-key bracket notation: e.g. 'show_windows["Project Settings"]' + if "[" in item and item.endswith("]"): + dict_name, _, key_part = item.partition("[") + key = key_part[:-1].strip().strip("'\"") + if dict_name in controller._settable_fields: + attr_name = controller._settable_fields[dict_name] + current = getattr(controller, attr_name, None) + if isinstance(current, dict): + new_dict = {**current, key: value} + setattr(controller, attr_name, new_dict) def _handle_click(controller: 'AppController', task: dict): """[SDM: AppController._handle_click]""" diff --git a/tests/test_live_gui_filedialog_regression.py b/tests/test_live_gui_filedialog_regression.py index f6134ab0..42b64955 100644 --- a/tests/test_live_gui_filedialog_regression.py +++ b/tests/test_live_gui_filedialog_regression.py @@ -16,7 +16,6 @@ import pytest from src.api_hook_client import ApiHookClient -@pytest.mark.skip(reason="Pre-existing test bug: client.set_value('show_windows[\"Project Settings\"]', True) returns None (the hook server doesn't handle the dict-key bracket-notation syntax in the key name). The same key written as 'show_windows.Project Settings' (or via _handle_set_value directly) would work. Tracked as pre-existing.") def test_live_gui_project_settings_opens_without_filedialog_crash(live_gui) -> None: """ Regression: the Project Settings window's render call chain ends