Skip to content

Commit e54a688

Browse files
author
lijiuyang.5137
committed
Emit idle state after stream turns
1 parent cbeb8a2 commit e54a688

3 files changed

Lines changed: 100 additions & 14 deletions

File tree

src/runner.rs

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1349,13 +1349,7 @@ fn emit_idle_session_state_if_requested(
13491349
state: &mut TranscriptState,
13501350
output_format: OutputFormat,
13511351
) -> Result<()> {
1352-
if output_format != OutputFormat::StreamJson
1353-
|| state.saw_idle_session_state
1354-
|| std::env::var("CLAUDE_CODE_EMIT_SESSION_STATE_EVENTS")
1355-
.ok()
1356-
.as_deref()
1357-
!= Some("1")
1358-
{
1352+
if output_format != OutputFormat::StreamJson || state.saw_idle_session_state {
13591353
return Ok(());
13601354
}
13611355
let value = json!({

tests/cctty_cli.rs

Lines changed: 87 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -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+
10361055
fn 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]
15061586
fn stream_json_permission_prompt_stdio_bridges_can_use_tool_request() {
15071587
let fixture = FakeClaude::new();

tests/support/mod.rs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -612,6 +612,18 @@ while True:
612612
after += 1
613613
buf = buf[after:]
614614
continue
615+
if "OMIT_FAKE_RESULT" in prompt:
616+
with transcript.open("a", encoding="utf-8") as f:
617+
f.write(json.dumps({"type":"system","subtype":"init","session_id":session_id}) + "\n")
618+
f.write(json.dumps({"type":"user","message":{"role":"user","content":prompt}}) + "\n")
619+
f.write(json.dumps({"type":"assistant","message":{"model":"fake-model","content":[{"type":"text","text":response}]}}) + "\n")
620+
sys.stdout.write("Context permissions /mcp\n")
621+
sys.stdout.flush()
622+
after = end + len(b"\x1b[201~")
623+
while after < len(buf) and buf[after:after + 1] in (b"\r", b"\n"):
624+
after += 1
625+
buf = buf[after:]
626+
continue
615627
with transcript.open("a", encoding="utf-8") as f:
616628
f.write(json.dumps({"type":"system","subtype":"init","session_id":session_id}) + "\n")
617629
f.write(json.dumps({"type":"user","message":{"role":"user","content":prompt}}) + "\n")

0 commit comments

Comments
 (0)