@@ -397,6 +397,8 @@ test.describe('workspace transport baseline', () => {
397397 const page = await context . newPage ( ) ;
398398 const ids = { deviceId : 'device-runtime-reconnect' , clientId : 'client-runtime-reconnect' } ;
399399 const replayClaudeSessionId = `claude-reconnect-${ Date . now ( ) } ` ;
400+ const workspacePath = await createExternalTempWorkspace ( 'coder-studio-transport-reconnect-' ) ;
401+ let workspace : WorkspaceHandle | null = null ;
400402
401403 try {
402404 await prepareTransportPage ( page ) ;
@@ -405,7 +407,7 @@ test.describe('workspace transport baseline', () => {
405407 agentCommand : `node ${ AGENT_CLAUDE_LIFECYCLE_SCRIPT } --running-delay-ms 150 --stopped-delay-ms 3000 ${ replayClaudeSessionId } ` ,
406408 } ) ;
407409 await seedWorkspaceControllerIds ( page , ids ) ;
408- const workspace = await openWorkspace ( page , ids ) ;
410+ workspace = await openWorkspace ( page , ids , workspacePath ) ;
409411 await waitForBackendSocket ( page ) ;
410412 const controller = await currentWorkspaceController ( page , workspace . workspaceId , ids ) ;
411413 const session = await invokeRpc < { id : number } > ( page , 'create_session' , {
@@ -485,7 +487,11 @@ test.describe('workspace transport baseline', () => {
485487 timeout : 4000 ,
486488 } ) . toBe ( 'idle' ) ;
487489 } finally {
488- await context . close ( ) ;
490+ if ( workspace && ! page . isClosed ( ) ) {
491+ await closeWorkspaceBestEffort ( page , workspace . workspaceId , ids ) ;
492+ }
493+ await Promise . allSettled ( [ context . close ( ) ] ) ;
494+ await removeExternalWorkspace ( workspacePath ) ;
489495 }
490496 } ) ;
491497
@@ -495,6 +501,8 @@ test.describe('workspace transport baseline', () => {
495501 const page = await context . newPage ( ) ;
496502 const ids = { deviceId : 'device-lifecycle' , clientId : 'client-lifecycle' } ;
497503 const replayClaudeSessionId = `claude-replay-${ Date . now ( ) } ` ;
504+ const workspacePath = await createExternalTempWorkspace ( 'coder-studio-transport-lifecycle-' ) ;
505+ let workspace : WorkspaceHandle | null = null ;
498506
499507 try {
500508 await prepareTransportPage ( page ) ;
@@ -503,7 +511,7 @@ test.describe('workspace transport baseline', () => {
503511 agentCommand : `node ${ AGENT_CLAUDE_LIFECYCLE_SCRIPT } --running-delay-ms ${ AGENT_CLAUDE_REPLAY_DELAY_MS } ${ replayClaudeSessionId } ` ,
504512 } ) ;
505513 await seedWorkspaceControllerIds ( page , ids ) ;
506- const workspace = await openWorkspace ( page , ids ) ;
514+ workspace = await openWorkspace ( page , ids , workspacePath ) ;
507515 await waitForBackendSocket ( page ) ;
508516 const controller = await currentWorkspaceController ( page , workspace . workspaceId , ids ) ;
509517 const session = await invokeRpc < { id : number } > ( page , 'create_session' , {
@@ -581,7 +589,11 @@ test.describe('workspace transport baseline', () => {
581589 page . locator ( `.agent-pane-card[data-session-id="${ session . id } "]` ) . first ( ) . getAttribute ( 'data-session-status' )
582590 ) . toBe ( 'idle' ) ;
583591 } finally {
584- await context . close ( ) ;
592+ if ( workspace && ! page . isClosed ( ) ) {
593+ await closeWorkspaceBestEffort ( page , workspace . workspaceId , ids ) ;
594+ }
595+ await Promise . allSettled ( [ context . close ( ) ] ) ;
596+ await removeExternalWorkspace ( workspacePath ) ;
585597 }
586598 } ) ;
587599
@@ -593,6 +605,8 @@ test.describe('workspace transport baseline', () => {
593605 const observer = await observerContext . newPage ( ) ;
594606 const controllerIds = { deviceId : 'device-a' , clientId : 'client-a' } ;
595607 const observerIds = { deviceId : 'device-b' , clientId : 'client-b' } ;
608+ const workspacePath = await createExternalTempWorkspace ( 'coder-studio-transport-takeover-follow-' ) ;
609+ let workspace : WorkspaceHandle | null = null ;
596610
597611 try {
598612 await prepareTransportPage ( controller ) ;
@@ -601,7 +615,7 @@ test.describe('workspace transport baseline', () => {
601615 await installTransportProbe ( observer ) ;
602616 await seedWorkspaceControllerIds ( controller , controllerIds ) ;
603617 await seedWorkspaceControllerIds ( observer , observerIds ) ;
604- const workspace = await openWorkspace ( controller , controllerIds ) ;
618+ workspace = await openWorkspace ( controller , controllerIds , workspacePath ) ;
605619 await waitForBackendSocket ( controller ) ;
606620
607621 await observer . goto ( `/workspace/${ workspace . workspaceId } ` ) ;
@@ -629,10 +643,14 @@ test.describe('workspace transport baseline', () => {
629643 timeout : 15000 ,
630644 } ) ;
631645 } finally {
646+ if ( workspace && ! observer . isClosed ( ) ) {
647+ await closeWorkspaceBestEffort ( observer , workspace . workspaceId , observerIds ) ;
648+ }
632649 await Promise . allSettled ( [
633650 observerContext . close ( ) ,
634651 controllerContext . close ( ) ,
635652 ] ) ;
653+ await removeExternalWorkspace ( workspacePath ) ;
636654 }
637655 } ) ;
638656
@@ -644,6 +662,8 @@ test.describe('workspace transport baseline', () => {
644662 const observer = await observerContext . newPage ( ) ;
645663 const controllerIds = { deviceId : 'device-click-a' , clientId : 'client-click-a' } ;
646664 const observerIds = { deviceId : 'device-click-b' , clientId : 'client-click-b' } ;
665+ const workspacePath = await createExternalTempWorkspace ( 'coder-studio-transport-takeover-request-' ) ;
666+ let workspace : WorkspaceHandle | null = null ;
647667
648668 try {
649669 await prepareTransportPage ( controller ) ;
@@ -652,7 +672,7 @@ test.describe('workspace transport baseline', () => {
652672 await installTransportProbe ( observer ) ;
653673 await seedWorkspaceControllerIds ( controller , controllerIds ) ;
654674 await seedWorkspaceControllerIds ( observer , observerIds ) ;
655- const workspace = await openWorkspace ( controller , controllerIds ) ;
675+ workspace = await openWorkspace ( controller , controllerIds , workspacePath ) ;
656676 await waitForBackendSocket ( controller ) ;
657677
658678 await observer . goto ( `/workspace/${ workspace . workspaceId } ` ) ;
@@ -674,10 +694,14 @@ test.describe('workspace transport baseline', () => {
674694 timeout : 15000 ,
675695 } ) ;
676696 } finally {
697+ if ( workspace && ! observer . isClosed ( ) ) {
698+ await closeWorkspaceBestEffort ( observer , workspace . workspaceId , observerIds ) ;
699+ }
677700 await Promise . allSettled ( [
678701 observerContext . close ( ) ,
679702 controllerContext . close ( ) ,
680703 ] ) ;
704+ await removeExternalWorkspace ( workspacePath ) ;
681705 }
682706 } ) ;
683707
@@ -689,6 +713,8 @@ test.describe('workspace transport baseline', () => {
689713 const observer = await observerContext . newPage ( ) ;
690714 const controllerIds = { deviceId : 'device-click-fail-a' , clientId : 'client-click-fail-a' } ;
691715 const observerIds = { deviceId : 'device-click-fail-b' , clientId : 'client-click-fail-b' } ;
716+ const workspacePath = await createExternalTempWorkspace ( 'coder-studio-transport-takeover-error-' ) ;
717+ let workspace : WorkspaceHandle | null = null ;
692718
693719 try {
694720 await prepareTransportPage ( controller ) ;
@@ -708,7 +734,7 @@ test.describe('workspace transport baseline', () => {
708734 } ) ;
709735 await seedWorkspaceControllerIds ( controller , controllerIds ) ;
710736 await seedWorkspaceControllerIds ( observer , observerIds ) ;
711- const workspace = await openWorkspace ( controller , controllerIds ) ;
737+ workspace = await openWorkspace ( controller , controllerIds , workspacePath ) ;
712738 await waitForBackendSocket ( controller ) ;
713739
714740 await observer . goto ( `/workspace/${ workspace . workspaceId } ` ) ;
@@ -728,22 +754,27 @@ test.describe('workspace transport baseline', () => {
728754 'This workspace is following the current controller.' ,
729755 ) ;
730756 } finally {
757+ if ( workspace && ! controller . isClosed ( ) ) {
758+ await closeWorkspaceBestEffort ( controller , workspace . workspaceId , controllerIds ) ;
759+ }
731760 await Promise . allSettled ( [
732761 observerContext . close ( ) ,
733762 controllerContext . close ( ) ,
734763 ] ) ;
764+ await removeExternalWorkspace ( workspacePath ) ;
735765 }
736766 } ) ;
737767
738768 test ( 'first draft prompt becomes the session title' , async ( { browser } ) => {
739769 const context = await browser . newContext ( ) ;
740770 const page = await context . newPage ( ) ;
741771 const titleWorkspacePath = await createExternalTempWorkspace ( 'coder-studio-transport-title-' ) ;
772+ let workspace : WorkspaceHandle | null = null ;
742773
743774 try {
744775 await prepareTransportPage ( page ) ;
745776 await installTransportProbe ( page ) ;
746- await openWorkspace ( page , DEFAULT_TRANSPORT_IDS , titleWorkspacePath ) ;
777+ workspace = await openWorkspace ( page , DEFAULT_TRANSPORT_IDS , titleWorkspacePath ) ;
747778 await waitForBackendSocket ( page ) ;
748779 await seedAppSettings ( page , {
749780 agentCommand : `node ${ AGENT_STDIN_ECHO_SCRIPT } ` ,
@@ -760,8 +791,11 @@ test.describe('workspace transport baseline', () => {
760791 timeout : 10000 ,
761792 } ) ;
762793 } finally {
763- await context . close ( ) ;
764- await fs . rm ( titleWorkspacePath , { recursive : true , force : true } ) ;
794+ if ( workspace && ! page . isClosed ( ) ) {
795+ await closeWorkspaceBestEffort ( page , workspace . workspaceId , DEFAULT_TRANSPORT_IDS ) ;
796+ }
797+ await Promise . allSettled ( [ context . close ( ) ] ) ;
798+ await removeExternalWorkspace ( titleWorkspacePath ) ;
765799 }
766800 } ) ;
767801
@@ -772,6 +806,8 @@ test.describe('workspace transport baseline', () => {
772806 const reopened = await reopenedContext . newPage ( ) ;
773807 const controllerIds = { deviceId : 'device-reopen' , clientId : 'client-a' } ;
774808 const reopenedIds = { deviceId : 'device-reopen' , clientId : 'client-b' } ;
809+ const workspacePath = await createExternalTempWorkspace ( 'coder-studio-transport-same-device-' ) ;
810+ let workspace : WorkspaceHandle | null = null ;
775811
776812 try {
777813 await prepareTransportPage ( controller ) ;
@@ -781,7 +817,7 @@ test.describe('workspace transport baseline', () => {
781817 await seedWorkspaceControllerIds ( controller , controllerIds ) ;
782818 await seedWorkspaceControllerIds ( reopened , reopenedIds ) ;
783819
784- const workspace = await openWorkspace ( controller , controllerIds ) ;
820+ workspace = await openWorkspace ( controller , controllerIds , workspacePath ) ;
785821 await waitForBackendSocket ( controller ) ;
786822
787823 await reopened . goto ( `/workspace/${ workspace . workspaceId } ` ) ;
@@ -820,10 +856,14 @@ test.describe('workspace transport baseline', () => {
820856 expect ( runtime . controller . controller_device_id ) . toBe ( reopenedIds . deviceId ) ;
821857 expect ( runtime . controller . controller_client_id ) . toBe ( reopenedIds . clientId ) ;
822858 } finally {
859+ if ( workspace && ! reopened . isClosed ( ) ) {
860+ await closeWorkspaceBestEffort ( reopened , workspace . workspaceId , reopenedIds ) ;
861+ }
823862 await Promise . allSettled ( [
824863 reopenedContext . close ( ) ,
825864 controllerContext . close ( ) ,
826865 ] ) ;
866+ await removeExternalWorkspace ( workspacePath ) ;
827867 }
828868 } ) ;
829869
@@ -834,6 +874,7 @@ test.describe('workspace transport baseline', () => {
834874 const ids = { deviceId : 'device-recovery' , clientId : 'client-recovery' } ;
835875 const resumeClaudeSessionId = `claude-e2e-resume-${ Date . now ( ) } ` ;
836876 const workspacePath = await createExternalTempWorkspace ( 'coder-studio-transport-recovery-' ) ;
877+ let workspace : WorkspaceHandle | null = null ;
837878
838879 try {
839880 await prepareTransportPage ( page ) ;
@@ -842,7 +883,7 @@ test.describe('workspace transport baseline', () => {
842883 agentCommand : `node ${ AGENT_CLAUDE_LIFECYCLE_SCRIPT } --running-delay-ms ${ AGENT_CLAUDE_RECOVERY_DELAY_MS } ` ,
843884 } ) ;
844885 await seedWorkspaceControllerIds ( page , ids ) ;
845- const workspace = await openWorkspace ( page , ids , workspacePath ) ;
886+ workspace = await openWorkspace ( page , ids , workspacePath ) ;
846887 await waitForBackendSocket ( page ) ;
847888 const controller = await currentWorkspaceController ( page , workspace . workspaceId , ids ) ;
848889 const session = await invokeRpc < { id : number } > ( page , 'create_session' , {
@@ -919,8 +960,11 @@ test.describe('workspace transport baseline', () => {
919960 message : `resumed session status after reload: ${ resumedStatus ?? 'null' } ` ,
920961 } ) . toBe ( true ) ;
921962 } finally {
922- await context . close ( ) ;
923- await fs . rm ( workspacePath , { recursive : true , force : true } ) ;
963+ if ( workspace && ! page . isClosed ( ) ) {
964+ await closeWorkspaceBestEffort ( page , workspace . workspaceId , ids ) ;
965+ }
966+ await Promise . allSettled ( [ context . close ( ) ] ) ;
967+ await removeExternalWorkspace ( workspacePath ) ;
924968 }
925969 } ) ;
926970} ) ;
@@ -1435,6 +1479,34 @@ async function closeAllOpenWorkspaces(page: Page) {
14351479 }
14361480}
14371481
1482+ async function closeWorkspaceBestEffort (
1483+ page : Page ,
1484+ workspaceId : string ,
1485+ ids : { deviceId : string ; clientId : string } = DEFAULT_TRANSPORT_IDS ,
1486+ ) {
1487+ try {
1488+ const controller = await currentWorkspaceController ( page , workspaceId , ids ) ;
1489+ await invokeRpc ( page , 'close_workspace' , controller ) ;
1490+ } catch {
1491+ // Best-effort cleanup for flaky transport cases.
1492+ }
1493+ }
1494+
1495+ async function removeExternalWorkspace ( workspacePath : string ) {
1496+ for ( let attempt = 0 ; attempt < 5 ; attempt += 1 ) {
1497+ try {
1498+ await fs . rm ( workspacePath , { recursive : true , force : true } ) ;
1499+ return ;
1500+ } catch ( error ) {
1501+ const code = ( error as NodeJS . ErrnoException ) . code ;
1502+ if ( ( code !== 'EBUSY' && code !== 'EPERM' ) || attempt === 4 ) {
1503+ throw error ;
1504+ }
1505+ await new Promise ( ( resolve ) => setTimeout ( resolve , 200 * ( attempt + 1 ) ) ) ;
1506+ }
1507+ }
1508+ }
1509+
14381510async function waitForPollCycle ( counts : PollCounts ) {
14391511 await expect
14401512 . poll ( ( ) => {
0 commit comments