use super::*;
type RawWire = JsonRpcConnection<ReadHalf<DuplexStream>, Sink>;
struct RawWired {
session: Session,
reader: RawWire,
writer: WriteHalf<DuplexStream>,
prompt_id: JsonRpcId,
}
async fn write_line(writer: &mut WriteHalf<DuplexStream>, line: &str) -> TestResult {
writer
.write_all(line.as_bytes())
.await
.map_err(|error| fail("the raw line must be writable", &error))?;
writer
.write_all(b"\n")
.await
.map_err(|error| fail("the frame terminator must be writable", &error))?;
writer
.flush()
.await
.map_err(|error| fail("the raw line must flush", &error))
}
fn id_json(id: &JsonRpcId) -> TestResult<String> {
serde_json::to_string(id).map_err(|error| fail("a request id must encode", &error))
}
async fn wire_up_raw(policy: PermissionPolicy) -> TestResult<RawWired> {
let (client_io, agent_io) = duplex(DUPLEX_CAPACITY);
let (client_read, client_write) = split(client_io);
let (agent_read, agent_write) = split(agent_io);
let connection = Arc::new(JsonRpcConnection::new(client_read, client_write));
let reader: RawWire = JsonRpcConnection::new(agent_read, tokio::io::sink());
let script = tokio::spawn(async move {
let mut writer = agent_write;
let initialize = expect_request(&reader, protocol::METHOD_INITIALIZE)
.await
.map_err(|error| fail("initialize must arrive", &error))?;
write_line(
&mut writer,
&format!(
r#"{{"jsonrpc":"2.0","id":{},"result":{{"protocolVersion":1,"agentCapabilities":{{}},"authMethods":[],"agentInfo":{{"name":"raw-agent","version":"0.0.0"}}}}}}"#,
id_json(&initialize.id)?
),
)
.await?;
let new_session = expect_request(&reader, protocol::METHOD_SESSION_NEW)
.await
.map_err(|error| fail("session/new must arrive", &error))?;
write_line(
&mut writer,
&format!(
r#"{{"jsonrpc":"2.0","id":{},"result":{{"sessionId":"{SESSION}"}}}}"#,
id_json(&new_session.id)?
),
)
.await?;
expect_request(&reader, protocol::METHOD_SESSION_PROMPT)
.await
.map_err(|error| fail("session/prompt must arrive", &error))?;
Ok::<_, String>((reader, writer))
});
let opened = harness(policy)
.connect(
connection.as_ref(),
PathBuf::from(WORKSPACE),
"do the thing".to_owned(),
None,
)
.await
.map_err(|error| fail("the handshake must succeed", &error))?;
let (reader, writer) = script
.await
.map_err(|error| fail("the agent script task must not panic", &error))??;
let prompt_id = opened.prompt_id.clone();
let session = AcpSession::start(SessionStart {
connection,
session_id: opened.session_id,
prompt_id: opened.prompt_id,
policy,
identity: identity(),
deferred: opened.deferred,
load_replay: opened.load_replay,
config_options: opened.config_options,
child: None,
exit_grace: EXIT_GRACE,
load_session: false,
process_group: None,
});
Ok(RawWired {
session,
reader,
writer,
prompt_id,
})
}
#[tokio::test]
async fn a_line_that_is_not_a_jsonrpc_frame_does_not_destroy_the_turn() -> TestResult {
let RawWired {
mut session,
reader,
mut writer,
prompt_id,
} = wire_up_raw(PermissionPolicy::Deny).await?;
write_line(
&mut writer,
&format!(
r#"{{"jsonrpc":"2.0","method":"session/update","params":{{"sessionId":"{SESSION}","update":{{"sessionUpdate":"agent_message_chunk","content":{{"type":"text","text":"42"}},"messageId":"m1"}}}}}}"#
),
)
.await?;
write_line(&mut writer, "Debugger listening on ws://127.0.0.1:9229").await?;
write_line(&mut writer, r#"{"hello":"world"}"#).await?;
write_line(
&mut writer,
&format!(
r#"{{"jsonrpc":"2.0","id":{},"result":{{"stopReason":"end_turn"}}}}"#,
id_json(&prompt_id)?
),
)
.await?;
let events = session.events();
let collected = within("draining the transcript", events.collect::<Vec<_>>()).await?;
assert!(
has_raw(&collected, "acp/unreadable_frame"),
"the rejected line must be recorded, not swallowed: {collected:?}"
);
let unreadable = collected
.iter()
.filter(|event| {
matches!(&event.kind, ActivityEventKind::Raw { source, .. }
if source == "acp/unreadable_frame")
})
.count();
assert_eq!(unreadable, 2, "both bad lines are recorded: {collected:?}");
assert!(
collected.iter().any(|event| matches!(
event.kind,
ActivityEventKind::Stop {
reason: StopKind::EndTurn
}
)),
"the turn still reaches its terminal boundary: {collected:?}"
);
let payload = within("taking the terminal result", session.wait_result())
.await?
.map_err(|error| fail("a bad line must not fail a completed turn", &error))?;
let decoded = payload
.to_json()
.map_err(|error| fail("the payload is JSON", &error))?;
assert_eq!(
decoded,
json!({ "text": "42", "final_message": "42", "stop_reason": "end_turn", "session_id": SESSION }),
"the answer received before the bad line survives it"
);
drop(reader);
drop(writer);
Ok(())
}
const PRIOR_SESSION: &str = "sess-prior";
async fn agent_opens_with_load(agent: &Wire) -> Result<(), HarnessError> {
agent_opens_with_load_expecting_mcp(agent, &json!([])).await
}
async fn agent_opens_with_load_expecting_mcp(
agent: &Wire,
expected_mcp: &Value,
) -> Result<(), HarnessError> {
let initialize = expect_request(agent, protocol::METHOD_INITIALIZE).await?;
agent
.send_response(&JsonRpcResponse::success(
initialize.id,
json!({
"protocolVersion": protocol::PROTOCOL_VERSION.as_u16(),
"agentCapabilities": { "loadSession": true },
"authMethods": [],
"agentInfo": { "name": "fake-acp-agent", "version": "0.0.0" }
}),
))
.await?;
let load = expect_request(agent, protocol::METHOD_SESSION_LOAD).await?;
let params = load
.params
.as_ref()
.ok_or_else(|| HarnessError::protocol("session/load carried no params"))?;
if params["sessionId"] != json!(PRIOR_SESSION) {
return Err(HarnessError::protocol(format!(
"session/load must name the job's prior session, got {}",
params["sessionId"]
)));
}
if params["cwd"] != json!(WORKSPACE) {
return Err(HarnessError::protocol(format!(
"session/load must send the configured absolute cwd, got {}",
params["cwd"]
)));
}
if params.get("mcpServers") != Some(expected_mcp) {
return Err(HarnessError::protocol(format!(
"session/load must send exactly the configured `mcpServers` ({expected_mcp}), got {:?}",
params.get("mcpServers")
)));
}
agent
.send_notification(&JsonRpcNotification::new(
protocol::METHOD_SESSION_UPDATE,
Some(json!({
"sessionId": PRIOR_SESSION,
"update": {
"sessionUpdate": "agent_message_chunk",
"content": { "type": "text", "text": "replayed history" },
},
})),
))
.await?;
agent
.send_response(&JsonRpcResponse::success(load.id, json!({})))
.await?;
let prompt = expect_request(agent, protocol::METHOD_SESSION_PROMPT).await?;
let prompt_params = prompt
.params
.as_ref()
.ok_or_else(|| HarnessError::protocol("session/prompt carried no params"))?;
if prompt_params["sessionId"] != json!(PRIOR_SESSION) {
return Err(HarnessError::protocol(format!(
"the prompt must be scoped to the LOADED session, got {}",
prompt_params["sessionId"]
)));
}
Ok(())
}
#[tokio::test]
async fn a_prior_session_and_a_load_capable_agent_resume_via_session_load() -> TestResult {
let (client_io, agent_io) = duplex(DUPLEX_CAPACITY);
let (client_read, client_write) = split(client_io);
let (agent_read, agent_write) = split(agent_io);
let connection = Arc::new(JsonRpcConnection::new(client_read, client_write));
let agent = Arc::new(JsonRpcConnection::new(agent_read, agent_write));
let script = {
let agent = Arc::clone(&agent);
tokio::spawn(async move { agent_opens_with_load(agent.as_ref()).await })
};
let opened = within(
"the resume handshake",
harness(PermissionPolicy::Deny).connect(
connection.as_ref(),
PathBuf::from(WORKSPACE),
"carry on".to_owned(),
Some(agent_client_protocol_schema::v1::SessionId::from(
PRIOR_SESSION.to_owned(),
)),
),
)
.await?
.map_err(|error| fail("the resume handshake must succeed", &error))?;
within("the agent script", script)
.await?
.map_err(|error| fail("the agent script task must not panic", &error))?
.map_err(|error| fail("the agent script must complete", &error))?;
assert_eq!(
opened.session_id.to_string(),
PRIOR_SESSION,
"the opened turn carries the loaded session's id"
);
assert!(
opened.deferred.iter().any(|note| {
note.method == protocol::METHOD_SESSION_UPDATE
&& note.params.as_ref().is_some_and(|params| {
params["update"]["content"]["text"] == json!("replayed history")
})
}),
"the replayed session/update is deferred for the pump: {:?}",
opened.deferred
);
Ok(())
}
#[tokio::test]
async fn asking_to_resume_an_agent_that_cannot_load_refuses_and_mints_nothing() -> TestResult {
let (client_io, agent_io) = duplex(DUPLEX_CAPACITY);
let (client_read, client_write) = split(client_io);
let (agent_read, agent_write) = split(agent_io);
let connection = Arc::new(JsonRpcConnection::new(client_read, client_write));
let agent = Arc::new(JsonRpcConnection::new(agent_read, agent_write));
let script = {
let agent = Arc::clone(&agent);
tokio::spawn(async move {
let initialize = expect_request(agent.as_ref(), protocol::METHOD_INITIALIZE).await?;
agent
.send_response(&JsonRpcResponse::success(
initialize.id,
json!({
"protocolVersion": protocol::PROTOCOL_VERSION.as_u16(),
"agentCapabilities": { "loadSession": false },
"authMethods": [],
"agentInfo": { "name": "fake-acp-agent", "version": "0.0.0" }
}),
))
.await?;
Ok::<(), HarnessError>(())
})
};
let result = within(
"the refused resume handshake",
harness(PermissionPolicy::Deny).connect(
connection.as_ref(),
PathBuf::from(WORKSPACE),
"carry on".to_owned(),
Some(agent_client_protocol_schema::v1::SessionId::from(
PRIOR_SESSION.to_owned(),
)),
),
)
.await?;
within("the agent script", script)
.await?
.map_err(|error| fail("the agent script task must not panic", &error))?
.map_err(|error| fail("the agent script must complete", &error))?;
let Err(error) = result else {
return Err("resume against a load-incapable agent must refuse, not open a turn".into());
};
assert!(
matches!(error, HarnessError::Configuration { .. }),
"the refusal is a deterministic Configuration error, got {error:?}"
);
let message = error.to_string();
assert!(
message.contains("loadSession"),
"the refusal names the missing capability: {message}"
);
assert!(
message.contains(PRIOR_SESSION),
"the refusal names the session the job asked for: {message}"
);
drop(connection);
let next = within("the agent's post-refusal read", agent.recv())
.await?
.map_err(|error| fail("the agent's read must not error", &error))?;
assert!(
next.is_none(),
"no frame follows the refusal — nothing was minted, got {next:?}"
);
Ok(())
}
#[tokio::test]
async fn replayed_history_is_recorded_as_replay_and_never_as_the_turns_own_words() -> TestResult {
let (client_io, agent_io) = duplex(DUPLEX_CAPACITY);
let (client_read, client_write) = split(client_io);
let (agent_read, agent_write) = split(agent_io);
let connection = Arc::new(JsonRpcConnection::new(client_read, client_write));
let agent = Arc::new(JsonRpcConnection::new(agent_read, agent_write));
let script = {
let agent = Arc::clone(&agent);
tokio::spawn(async move { agent_opens_with_load(agent.as_ref()).await })
};
let opened = within(
"the resume handshake",
harness(PermissionPolicy::Deny).connect(
connection.as_ref(),
PathBuf::from(WORKSPACE),
"carry on".to_owned(),
Some(agent_client_protocol_schema::v1::SessionId::from(
PRIOR_SESSION.to_owned(),
)),
),
)
.await?
.map_err(|error| fail("the resume handshake must succeed", &error))?;
within("the agent script", script)
.await?
.map_err(|error| fail("the agent script task must not panic", &error))?
.map_err(|error| fail("the agent script must complete", &error))?;
assert_eq!(
opened.load_replay.len(),
1,
"exactly the one replayed update is marked as load replay: {:?}",
opened.load_replay
);
let prompt_id = opened.prompt_id.clone();
let mut session = AcpSession::start(SessionStart {
connection,
session_id: opened.session_id,
prompt_id: opened.prompt_id,
policy: PermissionPolicy::Deny,
identity: identity(),
deferred: opened.deferred,
load_replay: opened.load_replay,
config_options: opened.config_options,
child: None,
exit_grace: EXIT_GRACE,
load_session: true,
process_group: None,
});
agent
.send_notification(&JsonRpcNotification::new(
protocol::METHOD_SESSION_UPDATE,
Some(json!({
"sessionId": PRIOR_SESSION,
"update": {
"sessionUpdate": "agent_message_chunk",
"content": { "type": "text", "text": "the actual answer" },
},
})),
))
.await
.map_err(|error| fail("the live chunk must be writable", &error))?;
agent
.send_response(&prompt_response(prompt_id, "end_turn"))
.await
.map_err(|error| fail("the terminal response must be writable", &error))?;
let events = session.events();
let collected = within("draining the transcript", events.collect::<Vec<_>>()).await?;
let replayed: Vec<_> = collected
.iter()
.filter(|event| {
matches!(
&event.kind,
ActivityEventKind::Raw { source, .. } if source == crate::pump::LOAD_REPLAY_SOURCE
)
})
.collect();
assert_eq!(
replayed.len(),
1,
"the replayed history is recorded exactly once under the replay source"
);
for event in &collected {
if let ActivityEventKind::Message { text, .. } = &event.kind {
assert!(
!text.contains("replayed history"),
"a replayed chunk reached the transcript as the turn's own message: {text:?}"
);
}
}
let answer = collected.iter().any(|event| {
matches!(&event.kind, ActivityEventKind::Message { text, .. } if text == "the actual answer")
});
assert!(
answer,
"the turn's own live chunk still folds to its message: {collected:?}"
);
drop(session);
drop(agent);
Ok(())
}