@@ -80,7 +80,7 @@ fn stream_json_text_prompt_uses_tty_transcript() {
8080
8181 assert_eq ! (
8282 json_types( & lines) ,
83- [ "system" , "user" , "assistant" , "result" ]
83+ [ "system" , "user" , "assistant" , "result" , "system" ]
8484 ) ;
8585 assert_eq ! ( lines[ 0 ] [ "session_id" ] , session_id) ;
8686 assert_eq ! ( lines[ 1 ] [ "message" ] [ "content" ] , "Say OK" ) ;
@@ -655,8 +655,7 @@ fn stream_json_control_permission_mode_restart_recovers_from_stale_session_lock(
655655 )
656656 . unwrap ( ) ;
657657 stdin. flush ( ) . unwrap ( ) ;
658- let plan_response = read_json_line ( & mut reader, & mut line) ;
659- assert_eq ! ( plan_response[ "type" ] , "control_response" ) ;
658+ let plan_response = wait_for_control_response_line ( & mut reader, & mut line) ;
660659 assert_eq ! ( plan_response[ "response" ] [ "request_id" ] , "plan-after-prompt" ) ;
661660
662661 writeln ! (
@@ -973,8 +972,7 @@ fn stream_json_sdk_mcp_survives_control_restart_between_turns() {
973972 )
974973 . unwrap ( ) ;
975974 stdin. flush ( ) . unwrap ( ) ;
976- let model_response = read_json_line ( & mut reader, & mut line) ;
977- assert_eq ! ( model_response[ "type" ] , "control_response" ) ;
975+ let model_response = wait_for_control_response_line ( & mut reader, & mut line) ;
978976 assert_eq ! (
979977 model_response[ "response" ] [ "request_id" ] ,
980978 "set-model-after-mcp"
@@ -1033,6 +1031,27 @@ fn wait_for_result_line<R: BufRead>(reader: &mut R, line: &mut String) -> Value
10331031 }
10341032}
10351033
1034+ fn wait_for_control_response_line < R : BufRead > ( reader : & mut R , line : & mut String ) -> Value {
1035+ loop {
1036+ let value = read_json_line ( reader, line) ;
1037+ if value. get ( "type" ) . and_then ( Value :: as_str) == Some ( "control_response" ) {
1038+ return value;
1039+ }
1040+ }
1041+ }
1042+
1043+ fn wait_for_idle_session_state_line < R : BufRead > ( reader : & mut R , line : & mut String ) -> Value {
1044+ loop {
1045+ let value = read_json_line ( reader, line) ;
1046+ if value. get ( "type" ) . and_then ( Value :: as_str) == Some ( "system" )
1047+ && value. get ( "subtype" ) . and_then ( Value :: as_str) == Some ( "session_state_changed" )
1048+ && value. get ( "state" ) . and_then ( Value :: as_str) == Some ( "idle" )
1049+ {
1050+ return value;
1051+ }
1052+ }
1053+ }
1054+
10361055fn drive_fake_sdk_mcp_prompt < W : Write , R : BufRead > (
10371056 stdin : & mut W ,
10381057 reader : & mut R ,
@@ -1443,15 +1462,14 @@ fn assert_stream_json_recovers_from_bad_resume_startup(fake_failure: &str) {
14431462}
14441463
14451464#[ test]
1446- fn stream_json_emits_idle_session_state_when_sdk_requests_it ( ) {
1465+ fn stream_json_emits_idle_session_state_after_each_turn ( ) {
14471466 let fixture = FakeClaude :: new ( ) ;
14481467 let workspace = tempfile:: tempdir ( ) . unwrap ( ) ;
14491468 let config_dir = tempfile:: tempdir ( ) . unwrap ( ) ;
14501469 let session_id = "00000000-0000-0000-0000-000000000010" ;
14511470 let mut child = std:: process:: Command :: new ( env ! ( "CARGO_BIN_EXE_cctty" ) )
14521471 . env ( "CCTTY_CLAUDE_PATH" , fixture. path ( ) )
14531472 . env ( "CLAUDE_CONFIG_DIR" , config_dir. path ( ) )
1454- . env ( "CLAUDE_CODE_EMIT_SESSION_STATE_EVENTS" , "1" )
14551473 . current_dir ( workspace. path ( ) )
14561474 . args ( [
14571475 "--output-format" ,
@@ -1502,6 +1520,68 @@ fn stream_json_emits_idle_session_state_when_sdk_requests_it() {
15021520 ) ;
15031521}
15041522
1523+ #[ test]
1524+ fn stream_json_synthetic_result_emits_idle_and_accepts_followup ( ) {
1525+ let fixture = FakeClaude :: new ( ) ;
1526+ let workspace = tempfile:: tempdir ( ) . unwrap ( ) ;
1527+ let config_dir = tempfile:: tempdir ( ) . unwrap ( ) ;
1528+ let session_id = "00000000-0000-0000-0000-000000000023" ;
1529+ let mut child = std:: process:: Command :: new ( env ! ( "CARGO_BIN_EXE_cctty" ) )
1530+ . env ( "CCTTY_CLAUDE_PATH" , fixture. path ( ) )
1531+ . env ( "CLAUDE_CONFIG_DIR" , config_dir. path ( ) )
1532+ . current_dir ( workspace. path ( ) )
1533+ . args ( [
1534+ "--output-format" ,
1535+ "stream-json" ,
1536+ "--input-format" ,
1537+ "stream-json" ,
1538+ "--session-id" ,
1539+ session_id,
1540+ ] )
1541+ . stdin ( Stdio :: piped ( ) )
1542+ . stdout ( Stdio :: piped ( ) )
1543+ . stderr ( Stdio :: piped ( ) )
1544+ . spawn ( )
1545+ . unwrap ( ) ;
1546+ let mut stdin = child. stdin . take ( ) . unwrap ( ) ;
1547+ let stdout = child. stdout . take ( ) . unwrap ( ) ;
1548+ let mut reader = BufReader :: new ( stdout) ;
1549+ let mut line = String :: new ( ) ;
1550+
1551+ writeln ! (
1552+ stdin,
1553+ r#"{{"type":"user","message":{{"role":"user","content":"OMIT_FAKE_RESULT first"}}}}"#
1554+ )
1555+ . unwrap ( ) ;
1556+ stdin. flush ( ) . unwrap ( ) ;
1557+ let first_result = wait_for_result_line ( & mut reader, & mut line) ;
1558+ assert_eq ! (
1559+ first_result[ "result" ] ,
1560+ "FAKE_RESPONSE: OMIT_FAKE_RESULT first"
1561+ ) ;
1562+ let idle = wait_for_idle_session_state_line ( & mut reader, & mut line) ;
1563+ assert_eq ! ( idle[ "session_id" ] , session_id) ;
1564+
1565+ writeln ! (
1566+ stdin,
1567+ r#"{{"type":"user","message":{{"role":"user","content":"Second prompt"}}}}"#
1568+ )
1569+ . unwrap ( ) ;
1570+ stdin. flush ( ) . unwrap ( ) ;
1571+ let second_result = wait_for_result_line ( & mut reader, & mut line) ;
1572+ assert_eq ! ( second_result[ "result" ] , "FAKE_RESPONSE: Second prompt" ) ;
1573+ drop ( stdin) ;
1574+
1575+ let status = child
1576+ . wait_timeout ( Duration :: from_secs ( 10 ) )
1577+ . unwrap ( )
1578+ . unwrap_or_else ( || {
1579+ let _ = child. kill ( ) ;
1580+ panic ! ( "cctty did not exit after stdin closed" ) ;
1581+ } ) ;
1582+ assert ! ( status. success( ) ) ;
1583+ }
1584+
15051585#[ test]
15061586fn stream_json_permission_prompt_stdio_bridges_can_use_tool_request ( ) {
15071587 let fixture = FakeClaude :: new ( ) ;
0 commit comments