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.
This commit is contained in:
+31
-26
@@ -374,33 +374,38 @@ class ApiHookClient:
|
||||
return result
|
||||
|
||||
def wait_for_project_switch(self, expected_path: str = None, timeout: float = 30.0, poll_interval: float = 0.2) -> dict[str, Any]:
|
||||
"""
|
||||
Blocks until the project switch completes (or fails). Returns the final
|
||||
status dict. If expected_path is provided, also waits until the reported
|
||||
path matches it (or its basename matches).
|
||||
Fails fast (within timeout seconds) with a clear error if:
|
||||
- the controller reports an error during the switch
|
||||
- the path doesn't reach expected_path within the timeout
|
||||
- the controller is hung (in_progress stays True)
|
||||
[C: tests/test_live_workflow.py:test_full_live_workflow]
|
||||
"""
|
||||
import os
|
||||
start = time.time()
|
||||
last_status = {"in_progress": True, "path": None, "error": None}
|
||||
while time.time() - start < timeout:
|
||||
last_status = self.get_project_switch_status()
|
||||
if last_status.get("error"):
|
||||
return last_status
|
||||
if not last_status.get("in_progress"):
|
||||
if expected_path is None:
|
||||
"""
|
||||
Blocks until the project switch completes (or fails). Returns the
|
||||
final status dict. If expected_path is provided, also waits until the
|
||||
reported path matches it (or its basename matches).
|
||||
Fails fast (within timeout seconds) with a clear error if:
|
||||
- the controller reports an error during the switch
|
||||
- the path doesn't reach expected_path within the timeout
|
||||
- the controller is hung (in_progress stays True)
|
||||
[C: tests/test_live_workflow.py:test_full_live_workflow]
|
||||
"""
|
||||
import os
|
||||
start = time.time()
|
||||
last_status = {"in_progress": True, "path": None, "error": None}
|
||||
expected_basename = os.path.basename(expected_path) if expected_path else None
|
||||
while time.time() - start < timeout:
|
||||
last_status = self.get_project_switch_status()
|
||||
if last_status.get("error"):
|
||||
return last_status
|
||||
if last_status.get("path") == expected_path:
|
||||
return last_status
|
||||
if last_status.get("path") and os.path.basename(str(last_status.get("path"))) == os.path.basename(expected_path):
|
||||
return last_status
|
||||
time.sleep(poll_interval)
|
||||
last_status["timeout"] = True
|
||||
return last_status
|
||||
if not last_status.get("in_progress"):
|
||||
current_path = last_status.get("path") or ""
|
||||
current_basename = os.path.basename(str(current_path))
|
||||
path_matches = (expected_path is None
|
||||
or current_path == expected_path
|
||||
or (expected_basename and current_basename == expected_basename))
|
||||
if path_matches:
|
||||
return last_status
|
||||
time.sleep(poll_interval)
|
||||
last_status["timeout"] = True
|
||||
return last_status
|
||||
|
||||
def post_project(self, project_data: dict) -> dict[str, Any]:
|
||||
return last_status
|
||||
|
||||
def post_project(self, project_data: dict) -> dict[str, Any]:
|
||||
"""
|
||||
|
||||
@@ -3276,6 +3276,13 @@ class AppController:
|
||||
Path(self.active_project_path).stem if self.active_project_path else "unnamed"
|
||||
)
|
||||
self.project_paths = []
|
||||
# Clear project-switch state machine so a hung switch from a prior test
|
||||
# does not block the next session. (is_project_stale() must return False
|
||||
# for the next click to actually submit a new switch.)
|
||||
with self._project_switch_lock:
|
||||
self._project_switch_in_progress = False
|
||||
self._project_switch_pending_path = None
|
||||
self._project_switch_error = None
|
||||
self.ai_status = "session reset"
|
||||
self.ai_response = ""
|
||||
self.ui_ai_input = ""
|
||||
|
||||
Reference in New Issue
Block a user