9afc93bce2
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.
111 lines
4.5 KiB
Python
111 lines
4.5 KiB
Python
"""Red-phase test: _handle_reset_session must clear project state.
|
|
|
|
Background: the live_gui session-scoped fixture is shared across all 48 live
|
|
tests. Prior tests can leave stale `self.project` and `self.project_paths`
|
|
on the controller, which leaks into `test_full_live_workflow` and causes
|
|
it to fail with "Project not switched".
|
|
|
|
The fix: `_handle_reset_session` should reset `self.project` (to a fresh
|
|
default dict) and `self.project_paths` (empty list).
|
|
|
|
Note: `self.active_project_path` is INTENTIONALLY NOT cleared, because
|
|
`_do_project_switch` calls `_flush_to_project()` which writes to
|
|
`self.active_project_path`. An empty path would raise OSError and
|
|
create an infinite re-switch loop. (See the regression discovered
|
|
in test_context_sim_live on 2026-06-08.)
|
|
|
|
This test uses a real AppController() (per the test_view_presets pattern),
|
|
pollutes the state, then calls _handle_reset_session and asserts.
|
|
"""
|
|
import pytest
|
|
|
|
from src.app_controller import AppController
|
|
from src import project_manager
|
|
|
|
|
|
@pytest.fixture
|
|
def controller(tmp_path):
|
|
"""Build a real AppController with stale project state."""
|
|
proj_path = tmp_path / "stale_project.toml"
|
|
proj_path.write_text("[project]\nname = 'StaleProject'\n")
|
|
ctrl = AppController()
|
|
# Pollute with stale state mimicking what a prior live_gui test leaves behind
|
|
ctrl.project = {
|
|
"project": {"name": "StaleProject", "active_discussion": "main"},
|
|
"files": {"paths": [str(proj_path)]},
|
|
"discussion": {"discussions": {"main": {"history": ["stale msg"]}}},
|
|
}
|
|
ctrl.active_project_path = str(proj_path)
|
|
ctrl.project_paths = [str(proj_path)]
|
|
yield ctrl
|
|
|
|
|
|
def test_handle_reset_session_keeps_active_project_path(controller):
|
|
"""Active project path is intentionally NOT cleared.
|
|
|
|
`_do_project_switch` writes to it via `_flush_to_project`; clearing it
|
|
causes OSError on the next project switch. (Regression: test_context_sim_live
|
|
on 2026-06-08.)
|
|
"""
|
|
assert controller.active_project_path.endswith("stale_project.toml") # precondition
|
|
controller._handle_reset_session()
|
|
assert controller.active_project_path.endswith("stale_project.toml"), (
|
|
f"_handle_reset_session should NOT clear active_project_path "
|
|
f"(got {controller.active_project_path!r})"
|
|
)
|
|
|
|
|
|
def test_handle_reset_session_clears_project_paths(controller):
|
|
"""project_paths list must be cleared (or reset to defaults)."""
|
|
assert len(controller.project_paths) == 1 # precondition
|
|
controller._handle_reset_session()
|
|
assert controller.project_paths != [str(controller.active_project_path)], (
|
|
f"_handle_reset_session did not clear project_paths "
|
|
f"(still {controller.project_paths!r})"
|
|
)
|
|
|
|
|
|
def test_handle_reset_session_resets_project_to_valid_default(controller):
|
|
"""self.project must be a valid (non-stale) project dict after reset."""
|
|
assert controller.project["project"]["name"] == "StaleProject" # precondition
|
|
controller._handle_reset_session()
|
|
name = controller.project.get("project", {}).get("name", "")
|
|
assert name != "StaleProject", (
|
|
f"_handle_reset_session did not reset self.project (still {name!r})"
|
|
)
|
|
# And it must still be a usable project dict
|
|
assert isinstance(controller.project, dict)
|
|
assert "project" in controller.project
|
|
|
|
|
|
def test_handle_reset_session_clears_project_switch_state(controller):
|
|
"""The project-switch state machine must be reset so a hung switch
|
|
from a prior test does not block the next session.
|
|
|
|
`is_project_stale()` must return False after reset, otherwise the next
|
|
`btn_project_new_automated` click is queued behind the hung switch
|
|
and `is_project_stale()` keeps returning True, blocking AI ops
|
|
(`_handle_generate_send` returns 'project switch in progress; AI ops disabled').
|
|
"""
|
|
# Simulate a prior hung switch
|
|
controller._project_switch_in_progress = True
|
|
controller._project_switch_pending_path = "/some/old/path.toml"
|
|
controller._project_switch_error = "stale error from hung switch"
|
|
assert controller.is_project_stale() # precondition
|
|
controller._handle_reset_session()
|
|
assert controller._project_switch_in_progress is False, (
|
|
f"_project_switch_in_progress not cleared: {controller._project_switch_in_progress}"
|
|
)
|
|
assert controller._project_switch_pending_path is None, (
|
|
f"_project_switch_pending_path not cleared: {controller._project_switch_pending_path}"
|
|
)
|
|
assert controller._project_switch_error is None, (
|
|
f"_project_switch_error not cleared: {controller._project_switch_error}"
|
|
)
|
|
assert not controller.is_project_stale(), (
|
|
f"is_project_stale() still True after reset: "
|
|
f"in_progress={controller._project_switch_in_progress}, "
|
|
f"pending={controller._project_switch_pending_path}"
|
|
)
|
|
|