crates/engine/tests/acp_client_conformance.rs
Rama main · réplica de lectura del repositorio original.
#[path = "support/acp_conformance.rs"]
mod conformance;
use codewhale_engine::fleet::acp::{AcpClient, ControlWrite, PermissionPolicy};
use codewhale_engine::fleet::host::{
FleetHostAdapter, FleetWorkerCommand, FleetWorkerStartRequest, FleetWorkerTransport,
LocalProcessFleetHostAdapter,
};
use conformance::{Allowed, Conformance, Side};
use std::path::{Path, PathBuf};
use std::time::Duration;
const KNOWN: &[Allowed] = &[
Allowed {
side: Side::Client,
frame: "session/set_model request",
problem: "`session/set_model` is not a client request in official v1",
reason: "compatibility, kept: sent only to an agent that offers the pre-v1 `models` object and no `model` config option",
},
Allowed {
side: Side::Agent,
frame: "session/new response",
problem: "field `.models` is not in the official type",
reason: "the fixture's legacy leg offers the pre-v1 `models` object on purpose, to make the client send session/set_model",
},
Allowed {
side: Side::Agent,
frame: "initialize response",
problem: "field `.agentCapabilities.sessionCapabilities.fork` is not in the official type",
reason: "options fixture advertises session/fork, which is unstable_session_fork, not stable v1",
},
Allowed {
side: Side::Agent,
frame: "session/prompt response",
problem: "field `.usage` is not in the official type",
reason: "options fixture reports PromptResponse.usage, which is unstable_end_turn_token_usage, not stable v1",
},
];
const STEP: Duration = Duration::from_secs(20);
fn fixture(name: &str) -> String {
PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("tests/fixtures")
.join(name)
.display()
.to_string()
}
fn captured(
agent: &str,
workspace: &Path,
log: &Path,
drive: impl FnOnce(&mut AcpClient),
) -> Vec<(Side, serde_json::Value)> {
let mut host = LocalProcessFleetHostAdapter::new(workspace);
let mut request = FleetWorkerStartRequest::new(
"w-acp-conformance",
FleetWorkerCommand::new(
"node",
[
fixture("acp-tee.mjs"),
log.display().to_string(),
fixture(agent),
],
),
);
request.transport = FleetWorkerTransport::JsonRpc;
request.cwd = Some(workspace.to_path_buf());
host.start_worker(request).unwrap();
let mut client = AcpClient::new(host.take_worker_io("w-acp-conformance").unwrap());
drive(&mut client);
host.stop_worker("w-acp-conformance").unwrap();
std::fs::read_to_string(log)
.unwrap()
.lines()
.map(|line| {
let (tag, frame) = line.split_once(' ').unwrap();
let side = if tag == "C" {
Side::Client
} else {
Side::Agent
};
(side, serde_json::from_str(frame).unwrap())
})
.collect()
}
#[test]
fn the_acp_client_and_its_fixture_agents_speak_official_v1_shapes() {
let has_node = std::process::Command::new("node")
.arg("--version")
.output()
.is_ok_and(|output| output.status.success());
if !has_node {
eprintln!("skipped: node is not installed");
return;
}
let capture = tempfile::tempdir().unwrap();
let workspace = tempfile::tempdir().unwrap();
let mut frames = captured(
"acp-agent.mjs",
workspace.path(),
&capture.path().join("agent.log"),
|client| {
let identity = client.initialize(STEP).unwrap();
let session = client
.new_session(&identity, workspace.path(), &[], STEP)
.unwrap()
.session_id;
client
.set_config_option(&session, "model", "fixture-model", STEP)
.unwrap();
for policy in [
PermissionPolicy::AllowOnce,
PermissionPolicy::RejectOnce,
PermissionPolicy::Cancel,
] {
client.prompt(&session, "conform", policy, STEP).unwrap();
}
client.cancel(&session).unwrap();
client
.reopen_session(&identity, &session, workspace.path(), &[], STEP)
.unwrap();
},
);
let options_workspace = tempfile::tempdir().unwrap();
frames.extend(captured(
"acp-options-agent.mjs",
options_workspace.path(),
&capture.path().join("options.log"),
|client| {
let identity = client.initialize(STEP).unwrap();
let created = client
.new_session(&identity, options_workspace.path(), &[], STEP)
.unwrap();
let mut applied = 0;
for control in &created.offer.controls {
if control.write.is_none() || control.unavailable.is_some() {
continue;
}
if let Some(choice) = control.choices.last() {
client
.apply_control(&created.session_id, control, &choice.value, STEP)
.unwrap();
applied += 1;
}
}
assert!(
applied >= 2,
"options fixture offered {applied} writable controls"
);
client
.prompt(
&created.session_id,
"conform",
PermissionPolicy::AllowOnce,
STEP,
)
.unwrap();
},
));
let legacy_workspace = tempfile::tempdir().unwrap();
std::fs::write(
legacy_workspace.path().join("fixture-plan.json"),
r#"{"legacyModels":true}"#,
)
.unwrap();
frames.extend(captured(
"acp-agent.mjs",
legacy_workspace.path(),
&capture.path().join("legacy.log"),
|client| {
let identity = client.initialize(STEP).unwrap();
let created = client
.new_session(&identity, legacy_workspace.path(), &[], STEP)
.unwrap();
let model = created.offer.control("model").unwrap();
assert_eq!(model.write, Some(ControlWrite::SetModel));
client
.apply_control(&created.session_id, model, "legacy-b", STEP)
.unwrap();
},
));
let mut observed = Conformance::checking(&[Side::Client, Side::Agent]);
for (side, frame) in &frames {
observed.observe(*side, frame);
}
assert!(
observed.frames() > 30,
"only {} frames observed",
observed.frames()
);
observed.assert_only(KNOWN);
}