@@ -591,6 +591,18 @@ fn write_fake_claude(dir: &Path) -> std::path::PathBuf {
591591 fs:: set_permissions ( & script_path, fs:: Permissions :: from_mode ( 0o755 ) ) . unwrap ( ) ;
592592 script_path
593593}
594+ fn write_fake_codex ( dir : & Path ) -> std:: path:: PathBuf {
595+ use std:: os:: unix:: fs:: PermissionsExt ;
596+
597+ let script_path = dir. join ( "codex" ) ;
598+ fs:: write (
599+ & script_path,
600+ "#!/bin/sh\n set -eu\n : > \" $FAKE_CODEX_ARGV_CAPTURE\" \n for arg in \" $@\" ; do\n printf '%s\\ 0' \" $arg\" >> \" $FAKE_CODEX_ARGV_CAPTURE\" \n done\n cat > \" $FAKE_CODEX_STDIN_CAPTURE\" \n " ,
601+ )
602+ . unwrap ( ) ;
603+ fs:: set_permissions ( & script_path, fs:: Permissions :: from_mode ( 0o755 ) ) . unwrap ( ) ;
604+ script_path
605+ }
594606fn write_producing_fake_claude ( dir : & Path ) -> std:: path:: PathBuf {
595607 use std:: os:: unix:: fs:: PermissionsExt ;
596608
@@ -1788,15 +1800,27 @@ fn step_without_dry_run_absolutizes_relative_config_override_and_path_entry() {
17881800 serde_json:: Value :: String ( project_dir. to_string_lossy( ) . into_owned( ) )
17891801 ) ;
17901802}
1803+ fn read_nul_separated_args ( path : & Path ) -> Vec < String > {
1804+ fs:: read ( path)
1805+ . unwrap ( )
1806+ . split ( |byte| * byte == 0 )
1807+ . filter ( |part| !part. is_empty ( ) )
1808+ . map ( |part| String :: from_utf8 ( part. to_vec ( ) ) . unwrap ( ) )
1809+ . collect ( )
1810+ }
1811+
1812+ fn adapter_path ( name : & str ) -> PathBuf {
1813+ Path :: new ( env ! ( "CARGO_MANIFEST_DIR" ) ) . join ( format ! ( "../adapters/{name}" ) )
1814+ }
1815+
17911816#[ test]
1792- fn claude_wrapper_wraps_runa_mcp_config_under_mcp_servers ( ) {
1817+ fn claude_code_adapter_wraps_runa_mcp_config_under_mcp_servers ( ) {
17931818 let dir = tempfile:: tempdir ( ) . unwrap ( ) ;
17941819 let bin_dir = dir. path ( ) . join ( "bin" ) ;
17951820 fs:: create_dir ( & bin_dir) . unwrap ( ) ;
17961821 let fake_claude = write_fake_claude ( & bin_dir) ;
17971822 let capture_path = dir. path ( ) . join ( "captured-claude-config.json" ) ;
1798- let wrapper_path =
1799- Path :: new ( env ! ( "CARGO_MANIFEST_DIR" ) ) . join ( "../examples/agent-claude-code.sh" ) ;
1823+ let wrapper_path = adapter_path ( "agent-claude-code.sh" ) ;
18001824
18011825 let output = Command :: new ( & wrapper_path)
18021826 . arg ( "--print" )
@@ -1838,6 +1862,147 @@ fn claude_wrapper_wraps_runa_mcp_config_under_mcp_servers() {
18381862 } )
18391863 ) ;
18401864}
1865+
1866+ #[ test]
1867+ fn claude_code_adapter_requires_runa_mcp_config ( ) {
1868+ let output = Command :: new ( adapter_path ( "agent-claude-code.sh" ) )
1869+ . env_remove ( "RUNA_MCP_CONFIG" )
1870+ . output ( )
1871+ . unwrap ( ) ;
1872+
1873+ assert ! ( !output. status. success( ) , "{output:?}" ) ;
1874+ let stderr = String :: from_utf8_lossy ( & output. stderr ) ;
1875+ assert ! (
1876+ stderr. contains( "agent-claude-code requires RUNA_MCP_CONFIG" ) ,
1877+ "stderr: {stderr}"
1878+ ) ;
1879+ }
1880+
1881+ #[ test]
1882+ fn codex_adapter_requires_runa_mcp_config ( ) {
1883+ let output = Command :: new ( adapter_path ( "agent-codex.sh" ) )
1884+ . env_remove ( "RUNA_MCP_CONFIG" )
1885+ . output ( )
1886+ . unwrap ( ) ;
1887+
1888+ assert ! ( !output. status. success( ) , "{output:?}" ) ;
1889+ let stderr = String :: from_utf8_lossy ( & output. stderr ) ;
1890+ assert ! (
1891+ stderr. contains( "agent-codex requires RUNA_MCP_CONFIG" ) ,
1892+ "stderr: {stderr}"
1893+ ) ;
1894+ }
1895+
1896+ #[ test]
1897+ fn codex_adapter_requires_jq_for_json_translation ( ) {
1898+ let dir = tempfile:: tempdir ( ) . unwrap ( ) ;
1899+ let empty_path = dir. path ( ) . join ( "empty-path" ) ;
1900+ fs:: create_dir ( & empty_path) . unwrap ( ) ;
1901+
1902+ let output = Command :: new ( adapter_path ( "agent-codex.sh" ) )
1903+ . env (
1904+ "RUNA_MCP_CONFIG" ,
1905+ r#"{"command":"/tmp/runa-mcp","args":[],"env":{}}"# ,
1906+ )
1907+ . env ( "PATH" , & empty_path)
1908+ . output ( )
1909+ . unwrap ( ) ;
1910+
1911+ assert ! ( !output. status. success( ) , "{output:?}" ) ;
1912+ let stderr = String :: from_utf8_lossy ( & output. stderr ) ;
1913+ assert ! (
1914+ stderr. contains( "agent-codex requires jq to parse RUNA_MCP_CONFIG" ) ,
1915+ "stderr: {stderr}"
1916+ ) ;
1917+ }
1918+
1919+ #[ test]
1920+ fn codex_adapter_translates_runa_mcp_config_to_mcp_server_overrides ( ) {
1921+ let dir = tempfile:: tempdir ( ) . unwrap ( ) ;
1922+ let bin_dir = dir. path ( ) . join ( "bin" ) ;
1923+ fs:: create_dir ( & bin_dir) . unwrap ( ) ;
1924+ let fake_codex = write_fake_codex ( & bin_dir) ;
1925+ let argv_capture = dir. path ( ) . join ( "codex.argv" ) ;
1926+ let stdin_capture = dir. path ( ) . join ( "codex.stdin" ) ;
1927+ let path = format ! (
1928+ "{}:{}" ,
1929+ bin_dir. display( ) ,
1930+ std:: env:: var( "PATH" ) . unwrap_or_default( )
1931+ ) ;
1932+ let prompt = "advance one runa session tick\n with stdin only" ;
1933+
1934+ let output = Command :: new ( adapter_path ( "agent-codex.sh" ) )
1935+ . arg ( "--model" )
1936+ . arg ( "gpt-test" )
1937+ . env (
1938+ "RUNA_MCP_CONFIG" ,
1939+ r#"{"command":"/tmp/runa mcp","args":["--session","--work-unit","wu-a","--sentinel=kept"],"env":{"RUNA_CONFIG":"/tmp/config.toml","RUNA_WORKING_DIR":"/tmp/project dir","EMPTY_VALUE":"","RUNA_FORGE_OWNER":"tesserine"}}"# ,
1940+ )
1941+ . env ( "PATH" , & path)
1942+ . env ( "FAKE_CODEX_ARGV_CAPTURE" , & argv_capture)
1943+ . env ( "FAKE_CODEX_STDIN_CAPTURE" , & stdin_capture)
1944+ . stdin ( std:: process:: Stdio :: piped ( ) )
1945+ . spawn ( )
1946+ . and_then ( |mut child| {
1947+ use std:: io:: Write ;
1948+
1949+ child. stdin . as_mut ( ) . unwrap ( ) . write_all ( prompt. as_bytes ( ) ) ?;
1950+ child. wait_with_output ( )
1951+ } )
1952+ . unwrap ( ) ;
1953+
1954+ assert ! (
1955+ output. status. success( ) ,
1956+ "stderr: {}" ,
1957+ String :: from_utf8_lossy( & output. stderr)
1958+ ) ;
1959+ assert_eq ! ( fake_codex, bin_dir. join( "codex" ) ) ;
1960+
1961+ let argv = read_nul_separated_args ( & argv_capture) ;
1962+ assert_eq ! ( argv[ 0 ] , "exec" ) ;
1963+ assert_eq ! ( argv[ 1 ] , "-c" ) ;
1964+ assert ! ( argv[ 2 ] . starts_with( "mcp_servers.runa.command=" ) ) ;
1965+ assert_eq ! ( argv[ 3 ] , "-c" ) ;
1966+ assert ! ( argv[ 4 ] . starts_with( "mcp_servers.runa.args=" ) ) ;
1967+ assert_eq ! ( argv[ 5 ] , "-c" ) ;
1968+ assert ! ( argv[ 6 ] . starts_with( "mcp_servers.runa.env=" ) ) ;
1969+ assert_eq ! ( argv[ 7 ..] , [ "--model" , "gpt-test" ] ) ;
1970+
1971+ let command_toml = argv[ 2 ] . strip_prefix ( "mcp_servers.runa.command=" ) . unwrap ( ) ;
1972+ let args_toml = argv[ 4 ] . strip_prefix ( "mcp_servers.runa.args=" ) . unwrap ( ) ;
1973+ let env_toml = argv[ 6 ] . strip_prefix ( "mcp_servers.runa.env=" ) . unwrap ( ) ;
1974+ let command_value: toml:: Value = toml:: from_str ( & format ! ( "value = {command_toml}" ) ) . unwrap ( ) ;
1975+ let args_value: toml:: Value = toml:: from_str ( & format ! ( "value = {args_toml}" ) ) . unwrap ( ) ;
1976+ let env_value: toml:: Value = toml:: from_str ( & format ! ( "value = {env_toml}" ) ) . unwrap ( ) ;
1977+
1978+ assert_eq ! ( command_value[ "value" ] . as_str( ) , Some ( "/tmp/runa mcp" ) ) ;
1979+ assert_eq ! (
1980+ args_value[ "value" ] . as_array( ) . unwrap( ) ,
1981+ & vec![
1982+ toml:: Value :: String ( "--session" . to_string( ) ) ,
1983+ toml:: Value :: String ( "--work-unit" . to_string( ) ) ,
1984+ toml:: Value :: String ( "wu-a" . to_string( ) ) ,
1985+ toml:: Value :: String ( "--sentinel=kept" . to_string( ) ) ,
1986+ ]
1987+ ) ;
1988+ let env_table = env_value[ "value" ] . as_table ( ) . unwrap ( ) ;
1989+ assert_eq ! (
1990+ env_table. get( "RUNA_CONFIG" ) . unwrap( ) . as_str( ) ,
1991+ Some ( "/tmp/config.toml" )
1992+ ) ;
1993+ assert_eq ! (
1994+ env_table. get( "RUNA_WORKING_DIR" ) . unwrap( ) . as_str( ) ,
1995+ Some ( "/tmp/project dir" )
1996+ ) ;
1997+ assert_eq ! ( env_table. get( "EMPTY_VALUE" ) . unwrap( ) . as_str( ) , Some ( "" ) ) ;
1998+ assert_eq ! (
1999+ env_table. get( "RUNA_FORGE_OWNER" ) . unwrap( ) . as_str( ) ,
2000+ Some ( "tesserine" )
2001+ ) ;
2002+ assert_eq ! ( env_table. len( ) , 4 ) ;
2003+ assert_eq ! ( fs:: read_to_string( & stdin_capture) . unwrap( ) , prompt) ;
2004+ assert ! ( !argv. iter( ) . any( |arg| arg. contains( prompt) ) , "{argv:?}" ) ;
2005+ }
18412006#[ test]
18422007fn step_without_dry_run_reports_missing_runa_mcp_after_sibling_and_path_lookup ( ) {
18432008 let dir = tempfile:: tempdir ( ) . unwrap ( ) ;
0 commit comments