#![expect( clippy::disallowed_methods, reason = "integration test initializes an isolated git repository with the system git binary" )] use fabro_acp::test_support::fake_acp_agent_script; use fabro_config::Storage; use fabro_test::test_context; use fabro_types::EventBody; use fabro_vault::{SecretType, Vault}; use super::{find_run_dir, has_event, read_conclusion, run_events, run_id_for, run_state}; use crate::cmd::support::output_stdout; #[test] fn acp_backend_workflow() { let mut context = test_context!(); context.write_home( ".fabro/settings.toml", "[server.auth]\nmethods = [\"dev-token\"]\n", ); context.isolated_server(); let fake_agent = write_fake_acp_agent(&context); let acp_config = fake_acp_config_attr(&fake_agent); 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", prompt="write hello.txt", acp.config={acp_config}] exit [shape=Msquare] start -> work work -> exit }}"# ), ); init_git_repo(&context.temp_dir); context .run_cmd() .args(["--auto-approve", "--environment", "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"] == "acp" && stage["provider_used"]["model"] == "fake" && stage["provider_used"].get("reasoning_effort").is_none() && stage["provider_used"].get("speed").is_none() }), "run projection should include ACP model usage metadata: {stages:?}" ); } #[test] fn acp_backend_does_not_inject_registered_provider_credentials() { let mut context = test_context!(); context.write_home( ".fabro/settings.toml", "[server.auth]\nmethods = [\"dev-token\"]\n", ); context.isolated_server(); seed_anthropic_vault(&context.storage_dir); let fake_agent = write_fake_acp_agent(&context); let env_record = context.temp_dir.join("acp-env.json"); let acp_config = fake_acp_config_attr_recording_env(&fake_agent, &env_record); let workflow = context.temp_dir.join("acp_provider_env.fabro"); context.write_temp( "acp_provider_env.fabro", format!( r#"digraph ACP {{ graph [goal="Exercise ACP backend with stored provider credentials"] start [shape=Mdiamond] work [type="agent", backend="acp", prompt="write hello.txt", acp.config={acp_config}] exit [shape=Msquare] start -> work work -> exit }}"# ), ); init_git_repo(&context.temp_dir); context .run_cmd() .env_remove("ANTHROPIC_API_KEY") .env_remove("OPENAI_API_KEY") .env_remove("GEMINI_API_KEY") .args(["--auto-approve", "--environment", "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 recorded_env: serde_json::Value = serde_json::from_str( &std::fs::read_to_string(&env_record) .expect("fake ACP agent should record its environment"), ) .expect("fake ACP environment record should be valid JSON"); assert_eq!(recorded_env, serde_json::json!({})); } #[test] fn acp_artifacts_are_listed_when_touched_file_mtime_precedes_attempt_start() { let mut context = test_context!(); context.write_home( ".fabro/settings.toml", "[server.auth]\nmethods = [\"dev-token\"]\n", ); context.isolated_server(); let fake_agent = write_fake_acp_agent(&context); let acp_config = fake_acp_config_attr_with_env(&fake_agent, vec![ serde_json::json!({ "name": "ACP_WRITE_PATH", "value": "verification-artifacts/report.md", }), serde_json::json!({ "name": "ACP_WRITE_CONTENT", "value": "verified\n", }), serde_json::json!({ "name": "ACP_WRITE_MTIME_EPOCH", "value": "946684800", }), ]); context.write_temp( "acp_artifact_collection.fabro", format!( r#"digraph ACPArtifacts {{ graph [goal="Capture verification artifacts"] start [shape=Mdiamond] verify [type="agent", backend="acp", prompt="write verification artifact", acp.config={acp_config}] exit [shape=Msquare] start -> verify verify -> exit }}"# ), ); context.write_temp( "workflow.toml", r#"_version = 1 [workflow] graph = "acp_artifact_collection.fabro" [run] goal = "Capture verification artifacts" [run.checkpoint] exclude_globs = ["verification-artifacts/**"] [run.artifacts] include = ["verification-artifacts/**"] "#, ); init_git_repo(&context.temp_dir); context .run_cmd() .args(["--auto-approve", "--environment", "local"]) .arg(context.temp_dir.join("workflow.toml")) .assert() .success(); let run_dir = find_run_dir(&context); let run_id = run_id_for(&run_dir); let events = run_events(&run_dir); let completed = events .iter() .find_map(|event| match &event.event.body { EventBody::StageCompleted(props) if event.event.node_id.as_deref() == Some("verify") => { Some(props) } _ => None, }) .expect("verify stage should complete"); assert!( completed .files_touched .iter() .any(|file| file == "verification-artifacts/report.md"), "files_touched should include the artifact path: {:?}", completed.files_touched ); let output = context .command() .args(["--json", "artifact", "list", &run_id]) .output() .expect("artifact list should execute"); assert!( output.status.success(), "artifact list failed\nstdout:\n{}\nstderr:\n{}", String::from_utf8_lossy(&output.stdout), String::from_utf8_lossy(&output.stderr) ); let artifacts: Vec = serde_json::from_str(&output_stdout(&output)).expect("artifact list JSON should parse"); assert!( artifacts.iter().any(|artifact| { artifact["node_slug"] == "verify" && artifact["relative_path"] == "verification-artifacts/report.md" }), "artifact list should include the touched verification artifact: {artifacts:?}" ); let completed_run = events .iter() .find_map(|event| match &event.event.body { EventBody::RunCompleted(props) => Some(props), _ => None, }) .expect("run.completed should be emitted"); assert!( completed_run.artifact_count > 0, "run.completed should report captured artifacts" ); } fn write_fake_acp_agent(context: &fabro_test::TestContext) -> std::path::PathBuf { context.write_temp("fake_acp_agent.py", fake_acp_agent_script()); context.temp_dir.join("fake_acp_agent.py") } fn fake_acp_config_attr(script_path: &std::path::Path) -> String { fake_acp_config_attr_with_env(script_path, Vec::new()) } fn fake_acp_config_attr_recording_env( script_path: &std::path::Path, env_record: &std::path::Path, ) -> String { fake_acp_config_attr_with_env(script_path, vec![ serde_json::json!({"name": "ACP_ENV_RECORD", "value": env_record.to_string_lossy()}), serde_json::json!({ "name": "ACP_ENV_RECORD_KEYS", "value": "ANTHROPIC_API_KEY,OPENAI_API_KEY,GEMINI_API_KEY", }), ]) } fn fake_acp_config_attr_with_env( script_path: &std::path::Path, extra_env: Vec, ) -> String { let mut env = vec![serde_json::json!({"name": "ACP_MODE", "value": "write_file"})]; env.extend(extra_env); let config = serde_json::json!({ "type": "stdio", "name": "fake", "command": "python3", "args": [script_path.to_string_lossy()], "env": env, }) .to_string(); format!("{config:?}") } fn seed_anthropic_vault(storage_dir: &std::path::Path) { let mut vault = Vault::load(Storage::new(storage_dir).secrets_path()).expect("test vault should load"); vault .set( "ANTHROPIC_API_KEY", "vault-anthropic-key", SecretType::Token, None, ) .expect("Anthropic 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) ); }