@@ -729,6 +729,62 @@ describe("routes", () => {
729729 expect ( res . status ) . toBe ( 200 ) ;
730730 } ) ;
731731
732+ it ( "POST /api/agents/:agentId/sessions/:sessionId/reopen returns 404 for nonexistent session" , async ( ) => {
733+ const nonexistentSessionId = randomUUID ( ) ;
734+ const res = await apiRequest ( "POST" , `/api/agents/${ agentId } /sessions/${ nonexistentSessionId } /reopen` , { } , apiKey ) ;
735+ expect ( res . status ) . toBe ( 404 ) ;
736+ } ) ;
737+
738+ it ( "POST /api/agents/:agentId/sessions/:sessionId/reopen is idempotent when session is already active" , async ( ) => {
739+ // Create a fresh session that starts active (status='active', closed_at=NULL)
740+ const freshSessionId = randomUUID ( ) ;
741+ const freshKeypair = await crypto . subtle . generateKey ( { name : "Ed25519" } as any , true , [ "sign" , "verify" ] ) ;
742+ const freshPubJwk = await crypto . subtle . exportKey ( "jwk" , ( freshKeypair as any ) . publicKey ) ;
743+ await apiRequest ( "POST" , `/api/agents/${ agentId } /sessions` , { session_id : freshSessionId , session_public_key : freshPubJwk . x ! } , apiKey ) ;
744+
745+ // Inject a sentinel closed_at while keeping status='active'. This state is not reachable
746+ // via the public API — it exists solely to discriminate the no-op path from an erroneous
747+ // UPDATE: if reopen runs the UPDATE it would set closed_at to NULL, failing the assertion;
748+ // if it correctly skips the UPDATE the sentinel value survives unchanged.
749+ const sentinelClosedAt = "2000-01-01T00:00:00.000Z" ;
750+ await env . DB . prepare ( "UPDATE agent_sessions SET closed_at = ? WHERE id = ?" ) . bind ( sentinelClosedAt , freshSessionId ) . run ( ) ;
751+
752+ const res = await apiRequest ( "POST" , `/api/agents/${ agentId } /sessions/${ freshSessionId } /reopen` , { } , apiKey ) ;
753+ expect ( res . status ) . toBe ( 200 ) ;
754+
755+ const row = await env . DB . prepare ( "SELECT status, closed_at FROM agent_sessions WHERE id = ?" )
756+ . bind ( freshSessionId )
757+ . first < { status : string ; closed_at : string | null } > ( ) ;
758+ expect ( row ?. status ) . toBe ( "active" ) ;
759+ // The sentinel must survive — proves the UPDATE branch was skipped entirely
760+ expect ( row ?. closed_at ) . toBe ( sentinelClosedAt ) ;
761+ } ) ;
762+
763+ it ( "POST /api/agents/:agentId/sessions/:sessionId/reopen clears closed_at after close" , async ( ) => {
764+ // Create a session, close it, then reopen and verify closed_at is cleared
765+ const freshSessionId = randomUUID ( ) ;
766+ const freshKeypair = await crypto . subtle . generateKey ( { name : "Ed25519" } as any , true , [ "sign" , "verify" ] ) ;
767+ const freshPubJwk = await crypto . subtle . exportKey ( "jwk" , ( freshKeypair as any ) . publicKey ) ;
768+ await apiRequest ( "POST" , `/api/agents/${ agentId } /sessions` , { session_id : freshSessionId , session_public_key : freshPubJwk . x ! } , apiKey ) ;
769+
770+ await apiRequest ( "DELETE" , `/api/agents/${ agentId } /sessions/${ freshSessionId } ` , undefined , apiKey ) ;
771+
772+ const closedRow = await env . DB . prepare ( "SELECT status, closed_at FROM agent_sessions WHERE id = ?" )
773+ . bind ( freshSessionId )
774+ . first < { status : string ; closed_at : string | null } > ( ) ;
775+ expect ( closedRow ?. status ) . toBe ( "closed" ) ;
776+ expect ( closedRow ?. closed_at ) . not . toBeNull ( ) ;
777+
778+ const res = await apiRequest ( "POST" , `/api/agents/${ agentId } /sessions/${ freshSessionId } /reopen` , { } , apiKey ) ;
779+ expect ( res . status ) . toBe ( 200 ) ;
780+
781+ const reopenedRow = await env . DB . prepare ( "SELECT status, closed_at FROM agent_sessions WHERE id = ?" )
782+ . bind ( freshSessionId )
783+ . first < { status : string ; closed_at : string | null } > ( ) ;
784+ expect ( reopenedRow ?. status ) . toBe ( "active" ) ;
785+ expect ( reopenedRow ?. closed_at ) . toBeNull ( ) ;
786+ } ) ;
787+
732788 // ─── Agent PATCH/DELETE ───
733789
734790 it ( "PATCH /api/agents/:id returns 404 for nonexistent agent" , async ( ) => {
0 commit comments