@@ -678,3 +678,116 @@ def test_wrapper_script_exists_and_executable(self, tmp_path):
678678 content = wrapper .read_text ()
679679 assert "python" in content .lower ()
680680 assert "-m studyctl.cli" in content
681+
682+
683+ # ---------------------------------------------------------------------------
684+ # Test: E2E Experience Verification
685+ # ---------------------------------------------------------------------------
686+
687+
688+ class TestExperienceVerification :
689+ """Verify user-facing experience, not just plumbing.
690+
691+ These tests go beyond checking IPC files to verify what the user
692+ actually sees in the terminal and what data flows across sessions.
693+ """
694+
695+ def test_sidebar_pane_renders_topics (self , tmp_path ):
696+ """Topics logged by the agent appear in the sidebar PANE, not just IPC files."""
697+ agent = _make_mock_agent (tmp_path )
698+ info = _start_session (agent )
699+
700+ _wait_for (
701+ lambda : TOPICS_FILE .exists () and "First-class" in TOPICS_FILE .read_text (),
702+ desc = "topics logged to IPC" ,
703+ )
704+ time .sleep (7 )
705+
706+ content = _capture_pane (info ["sidebar_pane" ])
707+ assert any (marker in content for marker in ["Closures" , "First-class" , "W:" , "L:" ]), (
708+ f"Sidebar pane should render topic data but got:\n { content } "
709+ )
710+
711+ def test_cleanup_notes_flow_into_resume_persona (self , tmp_path ):
712+ """Full chain: topics -> cleanup -> DB notes -> resume persona."""
713+ agent = _make_mock_agent (tmp_path )
714+ info = _start_session (agent )
715+ original_name = info ["session_name" ]
716+
717+ _wait_for (
718+ lambda : TOPICS_FILE .exists () and "First-class" in TOPICS_FILE .read_text (),
719+ desc = "topics logged before end" ,
720+ )
721+ _studyctl ("study" , "--end" )
722+ _wait_for (
723+ lambda : not _session_exists (original_name ),
724+ timeout = 10 ,
725+ desc = "original session killed" ,
726+ )
727+
728+ agent2 = _make_mock_agent (tmp_path , name = "mock-agent-chain.sh" )
729+ _studyctl (
730+ "study" ,
731+ "--resume" ,
732+ env_overrides = {"STUDYCTL_TEST_AGENT_CMD" : f"bash { agent2 } {{persona_file}}" },
733+ )
734+ _wait_for (STATE_FILE .exists , desc = "resumed state file" )
735+ state = _read_state ()
736+
737+ persona_path = state .get ("persona_file" )
738+ assert persona_path , "Resumed session should have a persona file"
739+ persona = Path (persona_path )
740+ assert persona .exists (), f"Persona file not found at { persona_path } "
741+
742+ persona_content = persona .read_text ()
743+ has_topic_ref = "Closures" in persona_content or "First-class" in persona_content
744+ has_resume_section = "Resuming" in persona_content or "Previous" in persona_content
745+ assert has_topic_ref or has_resume_section , (
746+ f"Resume persona should reference session 1 topics.\n "
747+ f"Persona content (first 800 chars):\n { persona_content [:800 ]} "
748+ )
749+
750+ def test_resume_flag_strictly_present (self , tmp_path ):
751+ """Resume must reuse session directory -- no OR fallback allowed."""
752+ agent = _make_fast_agent (tmp_path )
753+ info = _start_session (agent )
754+ original_dir = info ["session_dir" ]
755+ original_name = info ["session_name" ]
756+
757+ _wait_for (
758+ lambda : not _session_exists (original_name ) or _read_state ().get ("mode" ) == "ended" ,
759+ timeout = 20 ,
760+ desc = "session ended" ,
761+ )
762+
763+ agent2 = _make_mock_agent (tmp_path , name = "mock-resume-strict.sh" )
764+ _studyctl (
765+ "study" ,
766+ "--resume" ,
767+ env_overrides = {"STUDYCTL_TEST_AGENT_CMD" : f"bash { agent2 } {{persona_file}}" },
768+ )
769+ _wait_for (STATE_FILE .exists , desc = "resumed state file" )
770+ state = _read_state ()
771+
772+ assert state .get ("session_dir" ) == original_dir , (
773+ f"Resume should reuse { original_dir } , got { state .get ('session_dir' )} "
774+ )
775+
776+ def test_sidebar_shows_elapsed_time (self , tmp_path ):
777+ """Sidebar should display a non-zero elapsed time after a few seconds."""
778+ import re
779+
780+ agent = _make_mock_agent (tmp_path )
781+ info = _start_session (agent )
782+
783+ _wait_for (
784+ lambda : len (_capture_pane (info ["sidebar_pane" ]).strip ()) > 0 ,
785+ timeout = 10 ,
786+ desc = "sidebar to render" ,
787+ )
788+ time .sleep (5 )
789+
790+ content = _capture_pane (info ["sidebar_pane" ])
791+ has_time = bool (re .search (r"\d+:\d{2}" , content ))
792+ has_elapsed = "elapsed" in content .lower () or "timer" in content .lower ()
793+ assert has_time or has_elapsed , f"Sidebar should show elapsed time but got:\n { content } "
0 commit comments