From f64c672bc5cfcc714d92a21191dd9b0dc215ff46 Mon Sep 17 00:00:00 2001 From: Bryan Helmkamp Date: Mon, 11 May 2026 11:38:57 -0400 Subject: [PATCH] test: cover ACP backend workflow execution --- lib/crates/fabro-cli/tests/it/workflow/acp.rs | 114 ++++++++++++++++++ .../it/workflow/fixtures/fake_acp_agent.py | 39 ++++++ lib/crates/fabro-cli/tests/it/workflow/mod.rs | 1 + 3 files changed, 154 insertions(+) create mode 100644 lib/crates/fabro-cli/tests/it/workflow/acp.rs create mode 100644 lib/crates/fabro-cli/tests/it/workflow/fixtures/fake_acp_agent.py diff --git a/lib/crates/fabro-cli/tests/it/workflow/acp.rs b/lib/crates/fabro-cli/tests/it/workflow/acp.rs new file mode 100644 index 000000000..09ca27c63 --- /dev/null +++ b/lib/crates/fabro-cli/tests/it/workflow/acp.rs @@ -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) + ); +} diff --git a/lib/crates/fabro-cli/tests/it/workflow/fixtures/fake_acp_agent.py b/lib/crates/fabro-cli/tests/it/workflow/fixtures/fake_acp_agent.py new file mode 100644 index 000000000..231f5d046 --- /dev/null +++ b/lib/crates/fabro-cli/tests/it/workflow/fixtures/fake_acp_agent.py @@ -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 diff --git a/lib/crates/fabro-cli/tests/it/workflow/mod.rs b/lib/crates/fabro-cli/tests/it/workflow/mod.rs index 1d5c5a4e0..023d4f825 100644 --- a/lib/crates/fabro-cli/tests/it/workflow/mod.rs +++ b/lib/crates/fabro-cli/tests/it/workflow/mod.rs @@ -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;