@@ -117,9 +117,14 @@ def __init__(self) -> None:
117117 self ._pty_handles : dict [str , _FakePtyHandle ] = {}
118118 self .create_pty_session_error : BaseException | None = None
119119 self .symlinks : dict [str , str ] = {}
120+ self .workspace_roots : set [str ] = set ()
121+ self .require_workspace_root_for_cd = False
120122
121123 async def exec (self , cmd : str , ** kwargs : object ) -> _FakeExecResult :
122124 self .exec_calls .append ((cmd , dict (kwargs )))
125+ parts = shlex .split (cmd )
126+ if len (parts ) >= 4 and parts [:3 ] == ["mkdir" , "-p" , "--" ]:
127+ self .workspace_roots .add (parts [3 ])
123128 if "sleep 0.5" in cmd :
124129 await asyncio .sleep (0.5 )
125130 result = self .next_result
@@ -150,6 +155,21 @@ async def execute_session_command(
150155 ) -> object :
151156 self .execute_session_command_calls .append ((session_id , request , dict (kwargs )))
152157 command = cast (str , getattr (request , "command" , "" ))
158+ parts = shlex .split (command )
159+ if (
160+ self .require_workspace_root_for_cd
161+ and len (parts ) >= 3
162+ and parts [0 ] == "cd"
163+ and parts [2 ] == "&&"
164+ and parts [1 ] not in self .workspace_roots
165+ ):
166+ return types .SimpleNamespace (
167+ cmd_id = "cmd-123" ,
168+ exit_code = 1 ,
169+ stdout = "" ,
170+ stderr = f"cd: no such file or directory: { parts [1 ]} " ,
171+ output = f"cd: no such file or directory: { parts [1 ]} " ,
172+ )
153173 resolved = resolve_fake_workspace_path (
154174 command ,
155175 symlinks = self .symlinks ,
@@ -511,6 +531,58 @@ async def test_create_uses_daytona_safe_default_workspace_root(
511531
512532 assert session .state .manifest .root == daytona_module .DEFAULT_DAYTONA_WORKSPACE_ROOT
513533
534+ @pytest .mark .asyncio
535+ async def test_start_prepares_workspace_root_before_runtime_helpers (
536+ self ,
537+ monkeypatch : pytest .MonkeyPatch ,
538+ ) -> None :
539+ """Verify Daytona creates the root before exec uses it as cwd."""
540+
541+ daytona_module = _load_daytona_module (monkeypatch )
542+
543+ async with daytona_module .DaytonaSandboxClient () as client :
544+ session = await client .create (options = daytona_module .DaytonaSandboxClientOptions ())
545+ sandbox = _FakeAsyncDaytona .current_sandbox
546+ assert sandbox is not None
547+ sandbox .process .require_workspace_root_for_cd = True
548+
549+ await session .start ()
550+
551+ root = daytona_module .DEFAULT_DAYTONA_WORKSPACE_ROOT
552+ assert root in sandbox .process .workspace_roots
553+ assert sandbox .process .exec_calls [0 ][0 ] == f"mkdir -p -- { root } "
554+ assert sandbox .process .execute_session_command_calls
555+ _session_id , request , _kwargs = sandbox .process .execute_session_command_calls [0 ]
556+ assert cast (str , cast (Any , request ).command ).startswith (f"cd { root } && " )
557+ assert session .state .workspace_root_ready is True
558+
559+ @pytest .mark .asyncio
560+ async def test_start_wraps_workspace_root_prepare_failure (
561+ self ,
562+ monkeypatch : pytest .MonkeyPatch ,
563+ ) -> None :
564+ """Verify Daytona surfaces root preparation failures as start errors."""
565+
566+ daytona_module = _load_daytona_module (monkeypatch )
567+
568+ async with daytona_module .DaytonaSandboxClient () as client :
569+ session = await client .create (options = daytona_module .DaytonaSandboxClientOptions ())
570+ sandbox = _FakeAsyncDaytona .current_sandbox
571+ assert sandbox is not None
572+ sandbox .process .next_result = _FakeExecResult (exit_code = 2 , result = "mkdir failed" )
573+
574+ with pytest .raises (daytona_module .WorkspaceStartError ) as exc_info :
575+ await session .start ()
576+
577+ assert exc_info .value .context == {
578+ "path" : daytona_module .DEFAULT_DAYTONA_WORKSPACE_ROOT ,
579+ "reason" : "workspace_root_nonzero_exit" ,
580+ "exit_code" : 2 ,
581+ "output" : "mkdir failed" ,
582+ }
583+ assert sandbox .process .execute_session_command_calls == []
584+ assert session .state .workspace_root_ready is False
585+
514586 @pytest .mark .asyncio
515587 async def test_create_passes_only_option_env_vars_to_daytona (
516588 self ,
0 commit comments