@@ -191,6 +191,49 @@ def test_opencode_session_end_without_an_id_keeps_the_pane_session(self) -> None
191191 self .assertEqual (printed .stdout .strip (), f"opencode --session '{ OPENCODE_SESSION_ID } '" )
192192 self .assertEqual (sent (rec ), [], "--print must not type anything" )
193193
194+ def write_hook_sessions (self , * records : tuple [str , str ]) -> None :
195+ now = time .time ()
196+ self .box .hook_state .write_text (json .dumps ({"version" : 1 , "sessions" : {session_id : {
197+ "sessionId" : session_id , "workspaceId" : WORKSPACE_ID , "surfaceId" : surface_id ,
198+ "cwd" : str (self .box .project ), "startedAt" : now , "updatedAt" : now ,
199+ } for session_id , surface_id in records }}), encoding = "utf-8" )
200+
201+ def test_panes_in_the_same_directory_keep_each_others_ended_sessions (self ) -> None :
202+ # Pane A's session ends, then pane B's in the same directory; A must still resume its own.
203+ other_session = "9a8b7c6d-5e4f-4a3b-8c2d-1e0f9a8b7c6d"
204+ self .write_hook_sessions ((SESSION_ID , SURFACE_ID ), (other_session , OTHER_SURFACE_ID ))
205+ with self .recorder () as rec :
206+ for session_id in (SESSION_ID , other_session ):
207+ payload = json .dumps ({"session_id" : session_id , "cwd" : str (self .box .project )})
208+ ended = self .box .cli (rec , ["claude-hook" , "session-end" ], input_text = payload )
209+ self .assertEqual (ended .returncode , 0 , merged_output (ended ))
210+ printed = self .box .cli (rec , ["resume" , "--print" , "--surface" , SURFACE_ID ])
211+ kept = json .loads (self .box .last_sessions .read_text (encoding = "utf-8" ))["sessions" ]
212+ self .assertEqual (
213+ sorted ((e ["surfaceId" ], e ["sessionId" ]) for e in kept ),
214+ sorted ([(SURFACE_ID , SESSION_ID ), (OTHER_SURFACE_ID , other_session )]),
215+ kept ,
216+ )
217+ self .assertEqual (printed .returncode , 0 , merged_output (printed ))
218+ self .assertEqual (printed .stdout .strip (), f"claude --resume '{ SESSION_ID } '" )
219+
220+ def test_ending_an_unrecorded_session_leaves_the_panes_live_session_alone (self ) -> None :
221+ # Claude /clear starts a new session in the pane before the old one's SessionEnd runs;
222+ # an old id with no live record must not consume the new session's record.
223+ live_session = "1c2d3e4f-5a6b-4c7d-8e9f-0a1b2c3d4e5f"
224+ self .write_hook_sessions ((live_session , SURFACE_ID ))
225+ payload = json .dumps ({"session_id" : SESSION_ID , "cwd" : str (self .box .project )})
226+ with self .recorder () as rec :
227+ ended = self .box .cli (rec , ["claude-hook" , "session-end" ], input_text = payload )
228+ self .assertEqual (ended .returncode , 0 , merged_output (ended ))
229+ live = json .loads (self .box .hook_state .read_text (encoding = "utf-8" ))["sessions" ]
230+ self .assertIn (live_session , live , "the newer live session must keep its record" )
231+ exits = [
232+ f for f in rec .frames
233+ if f .get ("method" ) == "agent.event" and f .get ("params" , {}).get ("event_type" ) == "session.exited"
234+ ]
235+ self .assertEqual (exits , [], "the live session's agent state must not be cleared" )
236+
194237 def test_refuses_while_an_agent_runs_in_the_pane (self ) -> None :
195238 self .box .write_last_sessions (self .box .entry ())
196239 for state in ("working" , "idle" ):
0 commit comments