Private
Public Access
0
0
Files
manual_slop/tests/test_live_workflow.py
T
ed 9afc93bce2 fix(app_controller): clear project-switch state in _handle_reset_session
When a prior test in the tier-3-live_gui batch leaves a _do_project_switch
background thread running, the next test's btn_project_new_automated click
sees _project_switch_in_progress=True (from the prior thread) and queues
the new path via _project_switch_pending_path. The queued switch is never
actually submitted to the io_pool, so is_project_stale() stays True and
AI ops (_handle_generate_send) bail with 'project switch in progress;
AI ops disabled'.

Fix: _handle_reset_session now also clears _project_switch_in_progress,
_project_switch_pending_path, and _project_switch_error (under the
existing _project_switch_lock). This way, even if the prior background
thread is still running, the controller reports an idle state and the
new switch can be submitted normally.

Also:
- src/api_hook_client.py: reverted wait_for_project_switch to require
  in_progress=False (was relaxed to return on queued path, which misled
  the caller into thinking the switch was done)
- tests/test_handle_reset_session_clears_project.py: new test
  test_handle_reset_session_clears_project_switch_state asserts
  is_project_stale() returns False after reset
- tests/test_api_hook_client_wait_for_project_switch.py: updated
  test_wait_for_project_switch_does_not_return_on_queued (in_progress
  + matching path should keep waiting, not return early)
- tests/test_live_workflow.py: added pre-wait for any in-flight switch
  before doing btn_reset (so the test waits up to 60s for the prior
  switch to complete if needed)
- conductor/todos/TODO_test_full_live_workflow.md: updated Task 4 with
  the deeper hang analysis and recommended fix

Known follow-up: test_full_live_workflow still hangs in tier-3 batch
even with this fix, because the new _do_project_switch itself is hung
in the io_pool (likely saturation from prior sims' AI discussion turn
workers). Deeper investigation required.
2026-06-08 15:19:30 -04:00

185 lines
6.3 KiB
Python

"""
ANTI-SIMPLIFICATION: These tests verify the end-to-end full live workflow.
They MUST NOT be simplified. They depend on exact execution states and timing
through the actual GUI and ApiHookClient interface.
"""
import pytest
import time
import sys
import os
# Ensure project root is in path
sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), "..")))
from src.api_hook_client import ApiHookClient
def wait_for_value(client, field, expected, timeout=10):
"""
Helper to poll the GUI state until a field matches the expected value.
"""
start = time.time()
while time.time() - start < timeout:
state = client.get_gui_state()
val = state.get(field)
if val == expected:
return True
time.sleep(0.5)
return False
@pytest.mark.integration
def test_full_live_workflow(live_gui) -> None:
"""
Integration test that drives the GUI through a full workflow.
ANTI-SIMPLIFICATION: Asserts exact AI behavior, thinking state tracking,
and response logging in discussion history.
"""
client = ApiHookClient()
assert client.wait_for_server(timeout=10)
client.post_session(session_entries=[])
# 0. Wait for any in-flight project switch to complete before starting.
# The session-scoped live_gui fixture shares the controller across all
# 48 live tests. Prior tests (especially test_extended_sims) may leave
# a project switch hanging in the io_pool. If we proceed without waiting,
# our new switch will be queued behind the hung one and is_project_stale()
# will return True, blocking AI ops.
pre_status = client.get_project_switch_status()
if pre_status.get("in_progress"):
print(f"\n[TEST] Waiting for prior project switch to complete: {pre_status}")
idle_status = client.wait_for_project_switch(timeout=60.0)
assert not idle_status.get("timeout"), (
f"Prior project switch did not complete in 60s. Aborting. "
f"Last status: {idle_status}"
)
print(f"[TEST] Prior switch done: {idle_status}")
# 1. Reset
print("\n[TEST] Clicking Reset...")
client.click("btn_reset")
time.sleep(1)
# 2. Project Setup
temp_project_path = os.path.abspath("tests/artifacts/temp_project.toml")
if os.path.exists(temp_project_path):
try: os.remove(temp_project_path)
except: pass
print(f"[TEST] Creating new project at {temp_project_path}...")
client.click("btn_project_new_automated", user_data=temp_project_path)
# Defensive: fail fast if the click was dropped or the handler crashed
# before writing the project file.
import time as _time
_start = _time.time()
while _time.time() - _start < 5.0:
if os.path.exists(temp_project_path):
break
_time.sleep(0.1)
assert os.path.exists(temp_project_path), (
f"temp_project.toml not created within 5s of click. "
f"Click may have been dropped or _cb_new_project_automated crashed."
)
# Wait for project switch to complete (deterministic, condition-based).
# Replaces the prior 10x1s blind poll of derived state.
status = client.wait_for_project_switch(expected_path=temp_project_path, timeout=30.0)
assert not status.get("timeout"), (
f"Project switch did not complete in 30s. Last status: {status}"
)
assert not status.get("error"), (
f"Project switch failed with error: {status.get('error')}"
)
test_git = os.path.abspath(".")
print(f"[TEST] Setting project_git_dir to {test_git}...")
client.set_value("project_git_dir", test_git)
assert wait_for_value(client, "project_git_dir", test_git)
client.click("btn_project_save")
time.sleep(1)
# Enable auto-add so the response ends up in history
client.set_value("auto_add_history", True)
client.set_value("current_provider", "gemini")
# USE gemini-2.0-flash-lite (Actual current model)
client.set_value("current_model", "gemini-2.5-flash-lite")
time.sleep(1)
# 3. Discussion Turn
print("[TEST] Sending AI request...")
client.set_value("ai_input", "Hello! This is an automated test. Just say 'Acknowledged'.")
client.click("btn_gen_send")
# Verify thinking indicator appears or ai_status changes
print("[TEST] Polling for thinking indicator...")
success = False
for i in range(20):
mma = client.get_mma_status()
ai_status = mma.get('ai_status')
print(f" Poll {i}: ai_status='{ai_status}'")
if ai_status == 'error':
state = client.get_gui_state()
pytest.fail(f"AI Status went to error during thinking poll. Response: {state.get('ai_response')}")
if ai_status == 'sending...' or ai_status == 'streaming...':
print(f" AI is sending/streaming at poll {i}")
success = True
# Don't break, keep watching for a bit
indicator = client.get_indicator_state("thinking_indicator")
if indicator.get('shown'):
print(f" Thinking indicator seen at poll {i}")
success = True
break
time.sleep(0.5)
# 4. Wait for response in session
success = False
print("[TEST] Waiting for AI response in session history...")
for i in range(60):
session = client.get_session()
entries = session.get('session', {}).get('entries', [])
# Check for AI role. The entries are objects with a 'role' key.
found_ai = any(str(e.get('role', '')).upper() == 'AI' for e in entries)
if found_ai:
success = True
print(f" AI response found in history after {i}s")
break
mma = client.get_mma_status()
if mma.get('ai_status') == 'error':
state = client.get_gui_state()
pytest.fail(f"AI Status went to error during response wait. Response: {state.get('ai_response')}")
time.sleep(1)
# FALLBACK: if not in entries yet, check if ai_response is populated and status is done
if not success:
mma = client.get_mma_status()
if mma.get('ai_status') == 'done' or mma.get('ai_status') == 'idle':
state = client.get_gui_state()
if state.get('ai_response'):
print("[TEST] AI response found in ai_response field (fallback)")
success = True
assert success, f"AI failed to respond. Entries: {client.get_session()}, Status: {client.get_mma_status()}"
# 5. Switch Discussion
print("[TEST] Creating new discussion 'AutoDisc'...")
client.set_value("disc_new_name_input", "AutoDisc")
client.click("btn_disc_create")
time.sleep(1.0)
print("[TEST] Switching to 'AutoDisc'...")
client.select_list_item("disc_listbox", "AutoDisc")
time.sleep(1.0)
# Verify session is empty in new discussion
session = client.get_session()
entries = session.get('session', {}).get('entries', [])
print(f" New discussion history length: {len(entries)}")
assert len(entries) == 0
print("[TEST] Workflow completed successfully.")