Fix RunIntent producer CI failures

This commit is contained in:
Scott Werner 2026-09-01 10:23:47 -04:00
parent c67c60eeba
commit 45741a3e6e
4 changed files with 98 additions and 40 deletions

View file

@ -18,8 +18,10 @@ use fabro_manifest::SuppliedWorkflowVersionPackager;
use fabro_server::run_tool_create::ServerRunCreateAdapter;
use fabro_store::{EventEnvelope, RunProjection, RunProjectionReducer};
use fabro_tool::fabro_client::ClientBackend;
use fabro_types::settings::run::{RunMode, RunNamespace};
use fabro_types::{ArtifactUpload, BlobHash, EventBody, FailureReason, Principal, RunEvent, RunId};
use fabro_types::settings::run::{EnvironmentProvider, RunMode, RunNamespace};
use fabro_types::{
ArtifactUpload, BlobHash, EventBody, FailureReason, Principal, RunEvent, RunId, RunTarget,
};
use fabro_vault::{SecretStore, Vault};
use fabro_workflow::artifact_upload::{ArtifactSink, StageArtifactUploader};
use fabro_workflow::event::{Emitter, RunEventSink};
@ -231,8 +233,8 @@ fn build_fabro_run_tool_services(
worker_token: &str,
client: fabro_client::Client,
current_run_id: RunId,
provider: fabro_types::settings::run::EnvironmentProvider,
inherited_target: Option<fabro_types::RunTarget>,
provider: EnvironmentProvider,
inherited_target: Option<RunTarget>,
source_directory: Option<&str>,
run_dir: &Path,
) -> Option<FabroRunToolServices> {
@ -254,8 +256,8 @@ fn build_fabro_run_tool_services(
}
fn worker_run_create_adapter(
provider: fabro_types::settings::run::EnvironmentProvider,
inherited_target: Option<fabro_types::RunTarget>,
provider: EnvironmentProvider,
inherited_target: Option<RunTarget>,
user_workflows_root: Option<PathBuf>,
) -> ServerRunCreateAdapter {
ServerRunCreateAdapter::worker(provider, inherited_target, user_workflows_root)

View file

@ -743,6 +743,7 @@ async fn mcp_create_and_search_manage_real_runs_with_cli_auth() {
serde_json::json!({
"runs": [{
"workflow": workflow,
"target": { "kind": "none" },
"dry_run": true,
"auto_approve": true,
"labels": { "source": "mcp-test" }
@ -781,7 +782,7 @@ async fn mcp_create_and_search_manage_real_runs_with_cli_auth() {
"labels": {
"source": "mcp-test"
},
"source_directory": "[SOURCE_DIRECTORY]",
"source_directory": null,
"repo_origin_url": null,
"goal_preview": "Run tests and report results",
"goal_truncated": false
@ -810,6 +811,7 @@ async fn mcp_run_tools_use_default_local_server_without_server_flag() {
serde_json::json!({
"runs": [{
"workflow": workflow,
"target": { "kind": "none" },
"dry_run": true,
"auto_approve": true,
"labels": { "source": "mcp-default-server-test" },
@ -1388,7 +1390,7 @@ async fn mcp_lifecycle_tools_manage_real_run() {
"labels": {
"source": "mcp-test"
},
"source_directory": "[SOURCE_DIRECTORY]",
"source_directory": null,
"repo_origin_url": null,
"goal": "Run tests and report results"
}
@ -1934,6 +1936,29 @@ async fn mcp_create_string_shorthand_deserializes_before_auth() {
RealAuthHarness::start_with_dev_token(fabro_test::GitHubAppState::default()).await;
let target_url = harness.api_target();
let workflow = context.install_fixture("simple.fabro");
context.git_init();
run_git(&context.temp_dir, &["config", "user.name", "Fabro Test"]);
run_git(&context.temp_dir, &[
"config",
"user.email",
"fabro@example.com",
]);
run_git(&context.temp_dir, &["add", "simple.fabro"]);
run_git(&context.temp_dir, &["commit", "--quiet", "-m", "fixture"]);
run_git(&context.temp_dir, &[
"remote",
"add",
"origin",
"https://github.com/fabro-sh/fabro.git",
]);
let missing_push = format!("file://{}/missing.git", context.temp_dir.display());
run_git(&context.temp_dir, &[
"remote",
"set-url",
"--push",
"origin",
&missing_push,
]);
let client = spawn_mcp_client(&context, &["--server", &target_url]).await;
let result = client
@ -2828,6 +2853,7 @@ async fn create_mcp_run(client: &McpClient, workflow: PathBuf, start: bool) -> S
serde_json::json!({
"runs": [{
"workflow": workflow,
"target": { "kind": "none" },
"dry_run": true,
"auto_approve": true,
"labels": { "source": "mcp-test" },
@ -2842,6 +2868,19 @@ async fn create_mcp_run(client: &McpClient, workflow: PathBuf, start: bool) -> S
.to_string()
}
fn run_git(cwd: &Path, args: &[&str]) {
let output = Command::new("git")
.args(args)
.current_dir(cwd)
.output()
.expect("git command should run");
assert!(
output.status.success(),
"git {args:?} failed: {}",
String::from_utf8_lossy(&output.stderr)
);
}
fn seed_oauth_auth(
home_dir: &Path,
target: &fabro_client::ServerTarget,

View file

@ -4,6 +4,7 @@ use std::time::Duration;
use anyhow::Result;
use fabro_manifest::SuppliedWorkflowVersionPackager;
use fabro_server::run_tool_create::ServerRunCreateAdapter;
use fabro_tool::fabro_client::ClientBackend;
use fabro_tool::{self as run_tools, FabroToolBackend};
use fabro_util::version::FABRO_VERSION;
@ -279,9 +280,7 @@ impl FabroMcpServer {
.map(|parent| parent.join("workflows"));
Arc::new(
ClientBackend::new(Arc::new(client)).with_run_create_adapter(Arc::new(
fabro_server::run_tool_create::ServerRunCreateAdapter::standalone(
user_workflows_root,
),
ServerRunCreateAdapter::standalone(user_workflows_root),
)).with_workflow_version_packager(Arc::new(SuppliedWorkflowVersionPackager)),
) as Arc<dyn FabroToolBackend>
})

View file

@ -13,6 +13,7 @@ use fabro_tool::{
};
use fabro_types::settings::run::EnvironmentProvider;
use fabro_types::{DirtyStatus, RunTarget};
use tokio::fs;
use tokio::io::AsyncWriteExt;
use crate::manifest_validation;
@ -95,7 +96,7 @@ impl ServerRunCreateAdapter {
);
}
let path = cwd.join(goal_file);
tokio::fs::read_to_string(&path)
fs::read_to_string(&path)
.await
.with_context(|| format!("failed to read goal file {}", path.display()))
.map(Some)
@ -254,14 +255,14 @@ impl LocalWorkflowSource {
for (path, content) in &source.files {
let destination = root.path().join(path.as_str());
if let Some(parent) = destination.parent() {
tokio::fs::create_dir_all(parent).await.with_context(|| {
fs::create_dir_all(parent).await.with_context(|| {
format!(
"failed to create inline workflow directory {}",
parent.display()
)
})?;
}
let mut file = tokio::fs::OpenOptions::new()
let mut file = fs::OpenOptions::new()
.write(true)
.create_new(true)
.open(&destination)
@ -341,7 +342,7 @@ mod tests {
use super::*;
fn validated_spec(value: serde_json::Value) -> ValidatedCreateRunSpec {
fn validated_spec(value: &serde_json::Value) -> ValidatedCreateRunSpec {
let params: FabroRunCreateParams = serde_json::from_value(json!({ "runs": [value] }))
.expect("create input should deserialize");
ValidatedCreateRuns::try_from(params)
@ -354,6 +355,10 @@ mod tests {
fabro_client::Client::new_no_proxy(base_url).expect("test client should build")
}
#[expect(
clippy::disallowed_methods,
reason = "test fixture setup uses the Git CLI against an isolated temporary repository"
)]
fn run_git(cwd: &Path, args: &[&str]) {
let output = Command::new("git")
.args(args)
@ -367,10 +372,10 @@ mod tests {
);
}
async fn dynamic_version_registration_mock<'a>(
server: &'a MockServer,
async fn dynamic_version_registration_mock(
server: &MockServer,
registered: Arc<Mutex<Vec<WorkflowVersion>>>,
) -> httpmock::Mock<'a> {
) -> httpmock::Mock<'_> {
server
.mock_async(move |when, then| {
when.method(POST).path("/api/v1/workflow-versions");
@ -396,7 +401,7 @@ mod tests {
let registration =
dynamic_version_registration_mock(&server, Arc::clone(&registered)).await;
let client = no_proxy_client(&server.url(""));
let spec = validated_spec(json!({
let spec = validated_spec(&json!({
"workflow": {
"kind": "inline",
"entrypoint": "root/workflow.fabro",
@ -468,7 +473,7 @@ mod tests {
tag: Some("v1.0.0".to_string()),
sha: Some("0123456789abcdef0123456789abcdef01234567".to_string()),
});
let spec = validated_spec(json!({
let spec = validated_spec(&json!({
"workflow": {
"kind": "stored",
"workflow_version_id": workflow_version_id
@ -494,25 +499,29 @@ mod tests {
let temp = tempfile::tempdir().unwrap();
let operation_cwd = temp.path().join("nested/operation");
let workflow_dir = temp.path().join(".fabro/workflows/demo");
std::fs::create_dir_all(&operation_cwd).unwrap();
std::fs::create_dir_all(&workflow_dir).unwrap();
std::fs::write(temp.path().join(".fabro/project.toml"), "_version = 1\n").unwrap();
std::fs::write(
fs::create_dir_all(&operation_cwd).await.unwrap();
fs::create_dir_all(&workflow_dir).await.unwrap();
fs::write(temp.path().join(".fabro/project.toml"), "_version = 1\n")
.await
.unwrap();
fs::write(
workflow_dir.join("workflow.toml"),
"_version = 1\n[workflow]\ngraph = \"workflow.fabro\"\n",
)
.await
.unwrap();
std::fs::write(
fs::write(
workflow_dir.join("workflow.fabro"),
"digraph Demo { start [shape=Mdiamond] exit [shape=Msquare] start -> exit }",
)
.await
.unwrap();
let server = MockServer::start_async().await;
let registered = Arc::new(Mutex::new(Vec::new()));
let registration =
dynamic_version_registration_mock(&server, Arc::clone(&registered)).await;
let client = no_proxy_client(&server.url(""));
let spec = validated_spec(json!({
let spec = validated_spec(&json!({
"workflow": "demo",
"target": { "kind": "none" }
}));
@ -536,20 +545,22 @@ mod tests {
async fn workflow_version_worker_capabilities_gate_selector_and_goal_file_before_reads() {
let temp = tempfile::tempdir().unwrap();
let workflow = temp.path().join("same-name.fabro");
std::fs::write(
fs::write(
&workflow,
"digraph HostCopy { start [shape=Mdiamond] exit [shape=Msquare] start -> exit }",
)
.await
.unwrap();
std::fs::write(
fs::write(
temp.path().join("goal.md"),
"host goal that must not be read",
)
.await
.unwrap();
let client = no_proxy_client("http://127.0.0.1:9");
let adapter = ServerRunCreateAdapter::worker(EnvironmentProvider::Daytona, None, None);
let selector = validated_spec(json!({
let selector = validated_spec(&json!({
"workflow": "same-name.fabro",
"target": { "kind": "none" }
}));
@ -564,7 +575,7 @@ mod tests {
);
let workflow_version_id: WorkflowVersionId = fabro_types::BlobHash::new(b"stored").into();
let goal_file = validated_spec(json!({
let goal_file = validated_spec(&json!({
"workflow": {
"kind": "stored",
"workflow_version_id": workflow_version_id
@ -584,7 +595,7 @@ mod tests {
let client = no_proxy_client("http://127.0.0.1:9");
let adapter = ServerRunCreateAdapter::worker(EnvironmentProvider::Docker, None, None);
let invalid_graph = validated_spec(json!({
let invalid_graph = validated_spec(&json!({
"workflow": {
"kind": "inline",
"entrypoint": "workflow.fabro",
@ -597,7 +608,7 @@ mod tests {
.await
.expect_err("invalid graph should fail before registration");
let undefined_input = validated_spec(json!({
let undefined_input = validated_spec(&json!({
"workflow": {
"kind": "inline",
"entrypoint": "workflow.fabro",
@ -624,7 +635,7 @@ mod tests {
async fn workflow_version_target_failure_precedes_registration() {
let client = no_proxy_client("http://127.0.0.1:9");
let adapter = ServerRunCreateAdapter::worker(EnvironmentProvider::Docker, None, None);
let spec = validated_spec(json!({
let spec = validated_spec(&json!({
"workflow": {
"kind": "inline",
"entrypoint": "workflow.fabro",
@ -642,17 +653,20 @@ mod tests {
assert!(
error
.to_string()
.contains("parent run has no canonical target")
.contains("parent run has no canonical target"),
"unexpected error: {error:#}"
);
}
#[tokio::test]
async fn workflow_version_shared_goal_file_and_explicit_target_are_preserved() {
let temp = tempfile::tempdir().unwrap();
std::fs::write(temp.path().join("goal.md"), "goal from shared filesystem").unwrap();
fs::write(temp.path().join("goal.md"), "goal from shared filesystem")
.await
.unwrap();
let client = no_proxy_client("http://127.0.0.1:9");
let workflow_version_id: WorkflowVersionId = fabro_types::BlobHash::new(b"stored").into();
let spec = validated_spec(json!({
let spec = validated_spec(&json!({
"workflow": {
"kind": "stored",
"workflow_version_id": workflow_version_id
@ -679,7 +693,7 @@ mod tests {
async fn workflow_version_standalone_git_fallback_reports_excluded_local_bytes() {
let temp = tempfile::tempdir().unwrap();
let workspace = temp.path().join("workspace");
std::fs::create_dir(&workspace).unwrap();
fs::create_dir(&workspace).await.unwrap();
run_git(&workspace, &[
"init",
"--quiet",
@ -688,7 +702,9 @@ mod tests {
]);
run_git(&workspace, &["config", "user.name", "Fabro Test"]);
run_git(&workspace, &["config", "user.email", "fabro@example.com"]);
std::fs::write(workspace.join("tracked.txt"), "committed").unwrap();
fs::write(workspace.join("tracked.txt"), "committed")
.await
.unwrap();
run_git(&workspace, &["add", "tracked.txt"]);
run_git(&workspace, &["commit", "--quiet", "-m", "initial"]);
run_git(&workspace, &[
@ -701,10 +717,12 @@ mod tests {
run_git(&workspace, &[
"remote", "set-url", "--push", "origin", &missing,
]);
std::fs::write(workspace.join("dirty.txt"), "uncommitted").unwrap();
fs::write(workspace.join("dirty.txt"), "uncommitted")
.await
.unwrap();
let workflow_version_id: WorkflowVersionId = fabro_types::BlobHash::new(b"stored").into();
let spec = validated_spec(json!({
let spec = validated_spec(&json!({
"workflow": {
"kind": "stored",
"workflow_version_id": workflow_version_id