@@ -34,6 +34,7 @@ import {
3434 type Tracker ,
3535 type SCM ,
3636 type RuntimeHandle ,
37+ type Session ,
3738} from "../types.js" ;
3839
3940let tmpDir : string ;
@@ -1387,6 +1388,89 @@ describe("list", () => {
13871388 expect ( sessions [ 0 ] . activity ) . toBe ( "active" ) ;
13881389 } ) ;
13891390
1391+ it . each ( [ "claude-code" , "codex" , "aider" , "opencode" ] ) (
1392+ "uses tmuxName fallback handle for %s activity detection when runtimeHandle is missing" ,
1393+ async ( agentName : string ) => {
1394+ const expectedTmuxName = "hash-app-1" ;
1395+ const selectedAgent : Agent = {
1396+ ...mockAgent ,
1397+ name : agentName ,
1398+ getActivityState : vi . fn ( ) . mockImplementation ( async ( session : Session ) => {
1399+ return {
1400+ state : session . runtimeHandle ?. id === expectedTmuxName ? "active" : "exited" ,
1401+ } ;
1402+ } ) ,
1403+ } ;
1404+ const registryWithNamedAgents : PluginRegistry = {
1405+ ...mockRegistry ,
1406+ get : vi . fn ( ) . mockImplementation ( ( slot : string , name : string ) => {
1407+ if ( slot === "runtime" ) return mockRuntime ;
1408+ if ( slot === "agent" && name === agentName ) return selectedAgent ;
1409+ if ( slot === "workspace" ) return mockWorkspace ;
1410+ return null ;
1411+ } ) ,
1412+ } ;
1413+
1414+ writeMetadata ( sessionsDir , "app-1" , {
1415+ worktree : "/tmp" ,
1416+ branch : "a" ,
1417+ status : "working" ,
1418+ project : "my-app" ,
1419+ agent : agentName ,
1420+ tmuxName : expectedTmuxName ,
1421+ ...( agentName === "opencode" ? { opencodeSessionId : "ses_existing_mapping" } : { } ) ,
1422+ } ) ;
1423+
1424+ const sm = createSessionManager ( { config, registry : registryWithNamedAgents } ) ;
1425+ const sessions = await sm . list ( "my-app" ) ;
1426+
1427+ expect ( sessions ) . toHaveLength ( 1 ) ;
1428+ expect ( sessions [ 0 ] . runtimeHandle ?. id ) . toBe ( expectedTmuxName ) ;
1429+ expect ( sessions [ 0 ] . activity ) . toBe ( "active" ) ;
1430+ expect ( selectedAgent . getActivityState ) . toHaveBeenCalled ( ) ;
1431+ } ,
1432+ ) ;
1433+
1434+ it ( "uses tmuxName fallback handle for runtime liveness checks when runtimeHandle is missing" , async ( ) => {
1435+ const expectedTmuxName = "hash-app-1" ;
1436+ const deadRuntime : Runtime = {
1437+ ...mockRuntime ,
1438+ isAlive : vi
1439+ . fn ( )
1440+ . mockImplementation ( async ( handle : RuntimeHandle ) => handle . id !== expectedTmuxName ) ,
1441+ } ;
1442+ const agentWithSpy : Agent = {
1443+ ...mockAgent ,
1444+ getActivityState : vi . fn ( ) . mockResolvedValue ( { state : "active" } ) ,
1445+ } ;
1446+ const registryWithDeadRuntime : PluginRegistry = {
1447+ ...mockRegistry ,
1448+ get : vi . fn ( ) . mockImplementation ( ( slot : string ) => {
1449+ if ( slot === "runtime" ) return deadRuntime ;
1450+ if ( slot === "agent" ) return agentWithSpy ;
1451+ if ( slot === "workspace" ) return mockWorkspace ;
1452+ return null ;
1453+ } ) ,
1454+ } ;
1455+
1456+ writeMetadata ( sessionsDir , "app-1" , {
1457+ worktree : "/tmp" ,
1458+ branch : "a" ,
1459+ status : "working" ,
1460+ project : "my-app" ,
1461+ tmuxName : expectedTmuxName ,
1462+ } ) ;
1463+
1464+ const sm = createSessionManager ( { config, registry : registryWithDeadRuntime } ) ;
1465+ const sessions = await sm . list ( "my-app" ) ;
1466+
1467+ expect ( sessions ) . toHaveLength ( 1 ) ;
1468+ expect ( sessions [ 0 ] . runtimeHandle ?. id ) . toBe ( expectedTmuxName ) ;
1469+ expect ( sessions [ 0 ] . status ) . toBe ( "killed" ) ;
1470+ expect ( sessions [ 0 ] . activity ) . toBe ( "exited" ) ;
1471+ expect ( agentWithSpy . getActivityState ) . not . toHaveBeenCalled ( ) ;
1472+ } ) ;
1473+
13901474 it ( "keeps existing activity when getActivityState throws" , async ( ) => {
13911475 const agentWithError : Agent = {
13921476 ...mockAgent ,
0 commit comments