mirror of
https://github.com/fabro-sh/fabro.git
synced 2026-09-06 08:18:58 +00:00
test: cover ACP backend workflow execution
This commit is contained in:
parent
abd3f79f7c
commit
f64c672bc5
3 changed files with 154 additions and 0 deletions
114
lib/crates/fabro-cli/tests/it/workflow/acp.rs
Normal file
114
lib/crates/fabro-cli/tests/it/workflow/acp.rs
Normal 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)
|
||||
);
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
@ -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;
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue