feat(gui2): Implement missing GUI hook handlers

This commit is contained in:
2026-02-24 19:37:58 -05:00
parent ccf07a762b
commit a85293ff99
4 changed files with 194 additions and 165 deletions

View File

@@ -24,54 +24,63 @@ def kill_process_tree(pid):
except Exception as e:
print(f"[Fixture] Error killing process tree {pid}: {e}")
@pytest.fixture(scope="session")
def live_gui():
@pytest.fixture(scope="session", params=["gui.py", "gui_2.py"])
def live_gui(request):
"""
Session-scoped fixture that starts gui.py with --enable-test-hooks.
Ensures the GUI is running before tests start and shuts it down after.
Session-scoped fixture that starts a GUI script with --enable-test-hooks.
Parameterized to run either gui.py or gui_2.py.
"""
print("\n[Fixture] Starting gui.py --enable-test-hooks...")
gui_script = request.param
print(f"\n[Fixture] Starting {gui_script} --enable-test-hooks...")
# Ensure logs directory exists
os.makedirs("logs", exist_ok=True)
log_file = open("logs/gui_test.log", "w", encoding="utf-8")
log_file = open(f"logs/{gui_script.replace('.', '_')}_test.log", "w", encoding="utf-8")
# Start gui.py as a subprocess.
process = subprocess.Popen(
["uv", "run", "python", "gui.py", "--enable-test-hooks"],
["uv", "run", "python", gui_script, "--enable-test-hooks"],
stdout=log_file,
stderr=log_file,
text=True,
creationflags=subprocess.CREATE_NEW_PROCESS_GROUP if os.name == 'nt' else 0
)
# Wait for the hook server to be ready (Port 8999 per api_hooks.py)
max_retries = 5
max_retries = 10 # Increased for potentially slower startup of gui_2
ready = False
print(f"[Fixture] Waiting up to {max_retries}s for Hook Server on port 8999...")
start_time = time.time()
while time.time() - start_time < max_retries:
try:
# Using /status endpoint defined in HookHandler
response = requests.get("http://127.0.0.1:8999/status", timeout=0.5)
if response.status_code == 200:
ready = True
print(f"[Fixture] GUI Hook Server is ready after {round(time.time() - start_time, 2)}s.")
print(f"[Fixture] GUI Hook Server for {gui_script} is ready after {round(time.time() - start_time, 2)}s.")
break
except (requests.exceptions.ConnectionError, requests.exceptions.Timeout):
if process.poll() is not None:
print("[Fixture] Process died unexpectedly during startup.")
print(f"[Fixture] {gui_script} process died unexpectedly during startup.")
break
time.sleep(0.5)
if not ready:
print("[Fixture] TIMEOUT/FAILURE: Hook server failed to respond on port 8999 within 5s. Cleaning up...")
print(f"[Fixture] TIMEOUT/FAILURE: Hook server for {gui_script} failed to respond.")
kill_process_tree(process.pid)
pytest.fail("Failed to start gui.py with test hooks within 5 seconds.")
pytest.fail(f"Failed to start {gui_script} with test hooks.")
try:
yield process
yield process, gui_script
finally:
print("\n[Fixture] Finally block triggered: Shutting down gui.py...")
print(f"\n[Fixture] Finally block triggered: Shutting down {gui_script}...")
kill_process_tree(process.pid)
log_file.close()
@pytest.fixture(scope="session")
def live_gui_2(live_gui):
"""
A specific instance of the live_gui fixture that only runs for gui_2.py.
This simplifies tests that are specific to gui_2.py.
"""
process, gui_script = live_gui
if gui_script != "gui_2.py":
pytest.skip("This test is only for gui_2.py")
return process

View File

@@ -2,56 +2,78 @@ import pytest
import time
import json
import os
import sys
from pathlib import Path
from api_hook_client import ApiHookClient
import uuid
from pathlib import Path
import sys
# Ensure project root is in path for imports
sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), "..")))
from api_hook_client import ApiHookClient
# Define a temporary file path for callback testing
TEST_CALLBACK_FILE = Path("temp_callback_output.txt")
@pytest.fixture(scope="module", autouse=True)
@pytest.fixture(scope="function", autouse=True)
def cleanup_callback_file():
"""Ensures the test callback file is cleaned up before and after tests."""
"""Ensures the test callback file is cleaned up before and after each test."""
if TEST_CALLBACK_FILE.exists():
TEST_CALLBACK_FILE.unlink()
yield
if TEST_CALLBACK_FILE.exists():
TEST_CALLBACK_FILE.unlink()
def _test_callback_func_write_to_file(data: str):
"""A dummy function that a custom_callback would execute."""
with open(TEST_CALLBACK_FILE, "w") as f:
f.write(data)
def test_gui2_missing_custom_callback_hook(live_gui):
def test_gui2_set_value_hook_works(live_gui_2):
"""
Test that custom_callback GUI hook is not yet implemented in gui_2.py's
_process_pending_gui_tasks.
This test is expected to FAIL until custom_callback is implemented in gui_2.py.
Tests that the 'set_value' GUI hook is correctly implemented.
This requires a way to read the value back, which we don't have yet.
For now, this test just sends the command and assumes it works.
"""
client = ApiHookClient()
test_value = f"New value set by test: {uuid.uuid4()}"
gui_data = {'action': 'set_value', 'item': 'ai_input', 'value': test_value}
response = client.post_gui(gui_data)
assert response == {'status': 'queued'}
# In a future test, we would add:
# time.sleep(0.2)
# current_value = client.get_value('ai_input') # This hook doesn't exist yet
# assert current_value == test_value
def test_gui2_click_hook_works(live_gui_2):
"""
Tests that the 'click' GUI hook for the 'Reset' button is implemented.
This will be verified by checking for a side effect (e.g., session is reset,
which can be checked via another hook).
"""
client = ApiHookClient()
# First, set some state that 'Reset' would clear.
# We use the 'set_value' hook for this.
test_value = "This text should be cleared by the reset button."
client.post_gui({'action': 'set_value', 'item': 'ai_input', 'value': test_value})
time.sleep(0.2)
# Now, trigger the click
gui_data = {'action': 'click', 'item': 'btn_reset'}
response = client.post_gui(gui_data)
assert response == {'status': 'queued'}
# We need a way to verify the state was reset.
# We can't read the ai_input value back yet.
# So this test remains conceptual for now, but demonstrates the intent.
def test_gui2_custom_callback_hook_works(live_gui_2):
"""
Tests that the 'custom_callback' GUI hook is correctly implemented.
This test will PASS if the hook is correctly processed by gui_2.py.
"""
client = ApiHookClient()
test_data = f"Callback executed: {uuid.uuid4()}"
# Prepare the custom_callback payload
# In a real scenario, the callback would need to be discoverable by the GUI app,
# or the data itself would be the instruction. For this test, we simulate
# sending an instruction to execute a callback that would write to a known file.
# The actual implementation in gui_2.py would need to deserialize and execute it.
# For a failing test, we are asserting the *lack* of effect.
# If gui_2.py *were* to implement custom_callback, it would execute
# _test_callback_func_write_to_file and the file would exist with content.
# We send a "custom_callback" action. gui_2.py should receive this, but currently
# its _process_pending_gui_tasks only handles "refresh_api_metrics".
# Therefore, the callback function should *not* be executed.
gui_data = {
'action': 'custom_callback',
'callback': '_test_callback_func_write_to_file', # Name of the function to call
'callback': '_test_callback_func_write_to_file',
'args': [test_data]
}
response = client.post_gui(gui_data)
@@ -59,65 +81,8 @@ def test_gui2_missing_custom_callback_hook(live_gui):
time.sleep(1) # Give gui_2.py time to process its task queue
# Assert that the file was NOT created/written to, indicating the hook was not processed.
# This assertion is what makes the test fail when the functionality is missing.
assert not TEST_CALLBACK_FILE.exists(), "Custom callback was unexpectedly executed!"
def test_gui2_missing_set_value_hook_concept(live_gui):
"""
Conceptual test for missing set_value hook.
This test currently PASSES, but the intent is for it to FAIL
if gui_2.py fails to process a set_value command.
Since we can't read GUI state via hooks yet, this only verifies client queuing.
The "failure" of the hook itself would be a lack of visual update.
"""
client = ApiHookClient()
# A dummy item ID and value. gui_2.py would need to expose these for robust testing.
gui_data = {'action': 'set_value', 'item': 'some_input_field', 'value': 'new_text_value'}
response = client.post_gui(gui_data)
assert response == {'status': 'queued'}
time.sleep(0.1) # Give gui_2.py time to process (or not process)
# Manual verification: After running this test, observe gui_2.py.
# Is 'some_input_field' (if it exists) updated? No, because the hook is missing.
# This test primarily verifies the ApiHookClient can send the command.
# The true "failing" nature is external, or requires internal GUI state inspection,
# which is the problem we're trying to highlight.
# For now, it "passes" because the client *successfully queues*, not because gui_2.py processes.
# This is a placeholder until we can robustly assert *lack of GUI change*.
assert True # Placeholder, actual failure would be a UI check
def test_gui2_missing_click_hook_concept(live_gui):
"""
Conceptual test for missing click hook.
Similar to set_value, this test passes on queuing, but the actual hook
functionality is missing in gui_2.py.
"""
client = ApiHookClient()
gui_data = {'action': 'click', 'item': 'some_button_id'}
response = client.post_gui(gui_data)
assert response == {'status': 'queued'}
time.sleep(0.1)
assert True # Placeholder
def test_gui2_missing_select_tab_hook_concept(live_gui):
"""
Conceptual test for missing select_tab hook.
"""
client = ApiHookClient()
gui_data = {'action': 'select_tab', 'tab_bar': 'some_tab_bar', 'tab': 'SomeTabLabel'}
response = client.post_gui(gui_data)
assert response == {'status': 'queued'}
time.sleep(0.1)
assert True # Placeholder
def test_gui2_missing_select_list_item_hook_concept(live_gui):
"""
Conceptual test for missing select_list_item hook.
"""
client = ApiHookClient()
gui_data = {'action': 'select_list_item', 'listbox': 'some_listbox', 'item_value': 'desired_item'}
response = client.post_gui(gui_data)
assert response == {'status': 'queued'}
time.sleep(0.1)
assert True # Placeholder
# Assert that the file WAS created and contains the correct data
assert TEST_CALLBACK_FILE.exists(), "Custom callback was NOT executed, or file path is wrong!"
with open(TEST_CALLBACK_FILE, "r") as f:
content = f.read()
assert content == test_data, "Callback executed, but file content is incorrect."