test: cover ACP backend workflow execution

This commit is contained in:
Bryan Helmkamp 2026-05-11 11:38:57 -04:00
parent abd3f79f7c
commit f64c672bc5
No known key found for this signature in database
3 changed files with 154 additions and 0 deletions

View file

@ -0,0 +1,114 @@
#![expect(
clippy::disallowed_methods,
reason = "integration test initializes an isolated git repository with the system git binary"
)]
use fabro_auth::{AuthCredential, AuthDetails};
use fabro_config::Storage;
use fabro_model::Provider;
use fabro_test::test_context;
use fabro_types::EventBody;
use fabro_vault::{SecretType, Vault};
use super::{find_run_dir, fixture, has_event, read_conclusion, run_events, run_state};
#[test]
fn acp_backend_workflow() {
let context = test_context!();
seed_openai_vault(&context.storage_dir);
let fake_agent = fixture("fake_acp_agent.py");
let workflow = context.temp_dir.join("acp_backend.fabro");
context.write_temp(
"acp_backend.fabro",
&format!(
r#"digraph ACP {{
graph [goal="Exercise ACP backend"]
start [shape=Mdiamond]
work [type="agent", backend="acp", provider="openai", model="fake-acp", prompt="write hello.txt", acp_command="python3 {}"]
exit [shape=Msquare]
start -> work
work -> exit
}}"#,
fake_agent.display()
),
);
init_git_repo(&context.temp_dir);
context
.run_cmd()
.args(["--auto-approve", "--sandbox", "local"])
.arg(&workflow)
.assert()
.success();
let run_dir = find_run_dir(&context);
let conclusion = read_conclusion(&run_dir);
assert_eq!(conclusion["status"].as_str(), Some("succeeded"));
let events = run_events(&run_dir);
assert!(has_event(&run_dir, "agent.acp.started"));
assert!(has_event(&run_dir, "agent.acp.completed"));
let completed = events
.iter()
.find_map(|event| match &event.event.body {
EventBody::StageCompleted(props) if event.event.node_id.as_deref() == Some("work") => {
Some(props)
}
_ => None,
})
.expect("work stage should complete");
assert_eq!(completed.response.as_deref(), Some("hello from acp"));
assert!(
completed
.files_touched
.iter()
.any(|file| file == "hello.txt"),
"files_touched should include hello.txt: {:?}",
completed.files_touched
);
let state = serde_json::to_value(run_state(&run_dir)).expect("run state should serialize");
let stages = state["stages"]
.as_object()
.expect("run state should contain stages");
assert!(
stages.values().any(|stage| {
stage["provider_used"]["mode"] == "acp"
&& stage["provider_used"]["provider"] == "openai"
}),
"run projection should include ACP provider metadata: {stages:?}"
);
}
fn seed_openai_vault(storage_dir: &std::path::Path) {
let mut vault =
Vault::load(Storage::new(storage_dir).secrets_path()).expect("test vault should load");
vault
.set(
"openai",
&serde_json::to_string(&AuthCredential {
provider: Provider::OpenAi,
details: AuthDetails::ApiKey {
key: "test-openai-key".to_string(),
},
})
.expect("OpenAI test credential should serialize"),
SecretType::Credential,
None,
)
.expect("OpenAI credential should store in test vault");
}
fn init_git_repo(dir: &std::path::Path) {
let output = std::process::Command::new("git")
.args(["init", "-q"])
.current_dir(dir)
.output()
.expect("git init should run");
assert!(
output.status.success(),
"git init failed\nstdout:\n{}\nstderr:\n{}",
String::from_utf8_lossy(&output.stdout),
String::from_utf8_lossy(&output.stderr)
);
}

View file

@ -0,0 +1,39 @@
import json
import os
import sys
SESSION_ID = "sess-1"
def send(message):
print(json.dumps(message), flush=True)
def respond(message, result):
send({"jsonrpc": "2.0", "id": message["id"], "result": result})
for line in sys.stdin:
message = json.loads(line)
method = message.get("method")
if method == "initialize":
respond(message, {"protocolVersion": 1, "agentCapabilities": {}})
elif method == "session/new":
respond(message, {"sessionId": SESSION_ID})
elif method == "session/prompt":
with open("hello.txt", "w", encoding="utf-8") as file:
file.write("hello from acp\n")
for text in ["hello ", "from acp"]:
send({
"jsonrpc": "2.0",
"method": "session/update",
"params": {
"sessionId": SESSION_ID,
"update": {
"sessionUpdate": "agent_message_chunk",
"content": {"type": "text", "text": text},
},
},
})
respond(message, {"stopReason": os.environ.get("ACP_STOP_REASON", "end_turn")})
break

View file

@ -3,6 +3,7 @@
reason = "This test module prefers explicit type paths over extra imports."
)]
mod acp;
mod agent_linear;
mod command_agent_mixed;
mod command_pipeline;