@@ -404,6 +404,44 @@ def run(_executable, script, **_kwargs):
404404 assert observations == [["resolve" , "finish" ] if ownership == "agent" else []]
405405
406406
407+ @pytest .mark .parametrize ("code,expected" , [
408+ ("EGO_TASK_SPACE_USER_IN_CONTROL" , "source_reader_not_agent_owned" ),
409+ ("EGO_TASK_SPACE_INACTIVE" , "source_reader_not_agent_owned" ),
410+ ("EGO_OPERATION_FAILED" , "browser_read_failed" ),
411+ ])
412+ @pytest .mark .parametrize ("stage" , ["resolve" , "navigate" , "extract" ])
413+ @pytest .mark .parametrize ("image" , [False , True ])
414+ def test_native_control_race_survives_discarded_cli_output (
415+ configured , monkeypatch , code , expected , stage , image ,
416+ ):
417+ # Inventory still says Agent, but a later SDK boundary denies control.
418+ # Run the production script in a real child. Model Ego's hard-stop sink,
419+ # which discards all result markers and does not print error_code.
420+ real_run = subprocess .run
421+ trace = []
422+ def run (args , ** kwargs ):
423+ harness = (
424+ "const code=" + json .dumps (code ) + ";const stage=" + json .dumps (stage ) + ";"
425+ "function denied(){const e=Error('native wording changed');e.error_code=code;throw e;}"
426+ "async function listTaskSpaces(){return [{id:7,ownership:'agent'}];}"
427+ "async function taskSpace(){if(stage==='resolve')denied();return {page(){return {"
428+ "async goto(){if(stage==='navigate')denied();},async waitForFunction(){},"
429+ "async evaluate(){denied();},async screenshot(){throw Error('capture forbidden');}};}};}"
430+ "(async()=>{" + args [3 ] + "})().catch(()=>{process.exitCode=1;});"
431+ )
432+ child = real_run (["node" , "-e" , harness ], ** kwargs )
433+ trace .append (child .returncode )
434+ # Neither code nor an extraction sentinel survives the real SDK sink.
435+ return subprocess .CompletedProcess (args , child .returncode , "" , "SDK hard stop guidance" )
436+ monkeypatch .setattr (reader .subprocess , "run" , run )
437+ if image :
438+ result = json .loads (reader .read_public_image (URL , 0 )[0 ].text )
439+ else :
440+ result = reader .read_public_url (URL )
441+ assert result ["ok" ] is False and result ["error" ] == expected
442+ assert trace == [1 ] # No retry, new space, claim, takeover, or capture.
443+
444+
407445def test_creation_and_read_share_one_timeout_budget (configured , monkeypatch ):
408446 auto_config (monkeypatch )
409447 clock = iter ([0 , 5 , 29 ])
0 commit comments