diff --git a/docs/public/administration/server-configuration.mdx b/docs/public/administration/server-configuration.mdx index d299ec116..3dc364f7c 100644 --- a/docs/public/administration/server-configuration.mdx +++ b/docs/public/administration/server-configuration.mdx @@ -307,11 +307,11 @@ Vault values and their migration backups retain Fabro's plaintext-at-rest behavi The `[run.*]` sections in `settings.toml` act as defaults for every run. -On a same-machine setup, `settings.toml` is the shared machine-default layer under `workflow.toml` and `.fabro/project.toml`. +Run creation uses the registered workflow version's `workflow.toml` above the server defaults, with explicit `RunIntent` arguments at highest precedence. The same rules apply to local and remote clients. Project and user config are not submitted to the create endpoint. Manifest-based preflight, validation, and graph previews still accept their bundled configuration layers. -On a remote setup, the client bundles workflow, project, and user config into the run manifest. The server then layers those bundled client configs over its own local defaults for run-shaped fields. Server-owned values like `[server.storage]`, `[server.api]`, `[server.web]`, and `[server.scheduler]` always come from the server machine's own `settings.toml` or `fabro server start` flags. +Server-owned values like `[server.storage]`, `[server.api]`, `[server.web]`, and `[server.scheduler]` come from the server machine's `settings.toml` or `fabro server start` flags. Environments and MCP server references resolve against the server's catalogs. -Merge rules follow the normative matrix: TOML `[run.inputs]` tables replace wholesale, CLI `-I` / `--input` values replayed from run manifests merge per key at highest precedence, environment `env` and `labels` merge by key, `[run.prepare.steps]` replaces whole-list, and `[[run.hooks]]` merge by optional `id`. Most other fields use "higher-precedence wins" field-wise merging. +Merge rules follow the normative matrix: TOML `[run.inputs]` tables replace wholesale, explicit intent inputs merge per key at highest precedence, environment `env` and `labels` merge by key, `[run.prepare.steps]` replaces whole-list, and `[[run.hooks]]` merge by optional `id`. Most other fields use "higher-precedence wins" field-wise merging. ### `[server.logging]` section diff --git a/docs/public/api-reference/fabro-api.yaml b/docs/public/api-reference/fabro-api.yaml index 7b73bd821..57d8689ad 100644 --- a/docs/public/api-reference/fabro-api.yaml +++ b/docs/public/api-reference/fabro-api.yaml @@ -1176,12 +1176,12 @@ paths: tags: [Runs] summary: Create Run description: >- - Creates a new workflow run in `submitted` status from either a - self-contained legacy manifest or an immutable workflow-version intent. + Creates a new workflow run in `submitted` status from an + immutable workflow-version intent. Creation does not start or schedule the run. - Failures return the standard error body. The intent lane responds + Failures return the standard error body. The endpoint responds `404` (`workflow_version_not_found`, `environment_not_found`), `422` (`run_intent_invalid`, `target_invalid`, `target_environment_unsupported`, @@ -1194,7 +1194,7 @@ paths: content: application/json: schema: - $ref: "#/components/schemas/CreateRunRequest" + $ref: "#/components/schemas/RunIntent" responses: "201": description: Run created @@ -1209,7 +1209,7 @@ paths: # the CLI's error display contract # (run_create_failure_shows_action_context_and_response_body). "400": - description: Invalid JSON or legacy manifest + description: Invalid JSON headers: x-request-id: $ref: "#/components/headers/XRequestId" @@ -9326,14 +9326,6 @@ components: workflow_version_id: $ref: "#/components/schemas/WorkflowVersionId" - CreateRunRequest: - description: >- - Transitional create body used while callers migrate independently from - self-contained manifests to immutable workflow-version intents. - oneOf: - - $ref: "#/components/schemas/RunManifest" - - $ref: "#/components/schemas/RunIntent" - RunIntent: description: >- A request to create, but not start, one run from an immutable workflow @@ -12376,8 +12368,6 @@ components: type: string provenance: $ref: "#/components/schemas/RunProvenance" - manifest_blob: - type: ["string", "null"] definition_blob: type: ["string", "null"] spec_blob: diff --git a/docs/public/reference/architecture.mdx b/docs/public/reference/architecture.mdx index b372953c4..aa42d36e5 100644 --- a/docs/public/reference/architecture.mdx +++ b/docs/public/reference/architecture.mdx @@ -46,7 +46,7 @@ Key server config options: ### Run lifecycle -1. **Submit** — `POST /api/v1/runs` with a Graphviz workflow source. The run is created with status `submitted` and the response returns immediately with the run ID. +1. **Submit** — Register immutable workflow content with `POST /api/v1/workflow-versions`, then submit `POST /api/v1/runs` with its `workflow_version_id` and an explicit target. The run is created with status `submitted` and the response returns immediately with the run ID. 2. **Start request** — `POST /api/v1/runs/{id}/start` moves normal runs to `runnable`. Parent-generated [child runs](/execution/child-runs) may move to `pending` with `approval_required`. 3. **Approve if needed** — `POST /api/v1/runs/{id}/approve` moves an approval-gated run to `runnable`; `deny` fails it with `approval_denied`. 4. **Schedule** — A background scheduler promotes `runnable` runs to `running` in FIFO order, up to the concurrency limit. diff --git a/docs/public/reference/server-operations.mdx b/docs/public/reference/server-operations.mdx index 812770802..33f95db05 100644 --- a/docs/public/reference/server-operations.mdx +++ b/docs/public/reference/server-operations.mdx @@ -147,10 +147,20 @@ its snapshot, so make the rollback boundary explicit before restoring it. ## Submitting runs -Workflows are submitted via the REST API and executed in the background. The exact request body is documented in the API reference: +Register the workflow content, create a run from that version, then request execution. This example uses a clone-based default environment and an empty workspace target; add the authentication headers required by your server: ```bash -curl -X POST http://localhost:3000/api/v1/runs +version_id=$(curl --fail-with-body -sS http://localhost:3000/api/v1/workflow-versions \ + -H 'Content-Type: application/json' \ + -d '{"entrypoint":"workflow.fabro","files":{"workflow.fabro":"digraph Demo { start [shape=Mdiamond]; exit [shape=Msquare]; start -> exit; }"},"workflow_dependencies":{}}' \ + | jq -r '.workflow_version_id') + +run_id=$(curl --fail-with-body -sS http://localhost:3000/api/v1/runs \ + -H 'Content-Type: application/json' \ + -d "$(jq -n --arg id "$version_id" '{workflow_version_id:$id,target:{kind:"none"},args:{}}')" \ + | jq -r '.id') + +curl --fail-with-body -X POST "http://localhost:3000/api/v1/runs/$run_id/start" ``` The server returns immediately with a run ID. After a start request, a background scheduler promotes `runnable` runs to `running` in FIFO order, up to the concurrency limit. Parent-generated [child runs](/execution/child-runs) can remain `pending` until a user approves them. diff --git a/lib/apps/fabro-cli/src/commands/run/attach.rs b/lib/apps/fabro-cli/src/commands/run/attach.rs index 069237687..f6cc167d9 100644 --- a/lib/apps/fabro-cli/src/commands/run/attach.rs +++ b/lib/apps/fabro-cli/src/commands/run/attach.rs @@ -849,7 +849,6 @@ mod tests { source_directory: None, labels: std::collections::HashMap::default(), provenance: test_support::test_run_provenance(), - manifest_blob: None, definition_blob: None, spec_blob: None, git: None, diff --git a/lib/apps/fabro-cli/tests/it/cmd/support.rs b/lib/apps/fabro-cli/tests/it/cmd/support.rs index 0e1d223ef..0256417a5 100644 --- a/lib/apps/fabro-cli/tests/it/cmd/support.rs +++ b/lib/apps/fabro-cli/tests/it/cmd/support.rs @@ -937,10 +937,9 @@ async fn seed_dry_run(context: &TestContext, state: SeededRunState) -> RunSetup serde_json::json!({ "dry_run": true, "auto_approve": true, - "sandbox": "local", - "label": test_labels(context), + "labels": test_label_map(context), }), - None, + false, ) .await; @@ -954,7 +953,6 @@ async fn seed_dry_run(context: &TestContext, state: SeededRunState) -> RunSetup } async fn seed_git_backed_changed_run(context: &TestContext) -> SeededGitRunSetup { - let base_sha = "1111111111111111111111111111111111111111"; let step_one_sha = "2222222222222222222222222222222222222222"; let step_two_sha = "3333333333333333333333333333333333333333"; let run = create_seeded_run( @@ -963,18 +961,14 @@ async fn seed_git_backed_changed_run(context: &TestContext) -> SeededGitRunSetup changed_git_workflow_source(), serde_json::json!({ "provider": "openai", - "sandbox": "local", - "label": test_labels(context), + "labels": test_label_map(context), }), - Some(serde_json::json!({ - "origin_url": "https://github.com/fabro-sh/seeded-fixture.git", - "branch": "main", - "sha": base_sha, - "dirty": "clean", - })), + true, ) .await; + let base_sha = run_git(&context.temp_dir, &["rev-parse", "HEAD"]); + let base_sha = base_sha.trim(); let (client, base_url) = server_endpoint(&context.storage_dir) .expect("test server endpoint should be available for seeded run events"); append_seeded_git_completion_events( @@ -995,25 +989,20 @@ async fn seed_git_backed_changed_run(context: &TestContext) -> SeededGitRunSetup } async fn seed_git_backed_noop_run(context: &TestContext) -> RunSetup { - let base_sha = "1111111111111111111111111111111111111111"; let run = create_seeded_run( context, "flow.fabro", noop_git_workflow_source(), serde_json::json!({ "provider": "openai", - "sandbox": "local", - "label": test_labels(context), + "labels": test_label_map(context), }), - Some(serde_json::json!({ - "origin_url": "https://github.com/fabro-sh/seeded-fixture.git", - "branch": "main", - "sha": base_sha, - "dirty": "clean", - })), + true, ) .await; + let base_sha = run_git(&context.temp_dir, &["rev-parse", "HEAD"]); + let base_sha = base_sha.trim(); let (client, base_url) = server_endpoint(&context.storage_dir) .expect("test server endpoint should be available for seeded run events"); append_seeded_git_noop_events(&client, &base_url, &run, context, base_sha).await; @@ -1026,10 +1015,9 @@ async fn seed_artifact_run(context: &TestContext) -> RunSetup { "artifact_run.fabro", artifact_workflow_source(), serde_json::json!({ - "sandbox": "local", - "label": test_labels(context), + "labels": test_label_map(context), }), - None, + false, ) .await; @@ -1065,32 +1053,56 @@ async fn create_seeded_run( target_path: &str, source: &str, args: serde_json::Value, - git: Option, + git: bool, ) -> RunSetup { - let mut manifest = serde_json::json!({ - "version": 1, - "cwd": context.temp_dir.display().to_string(), - "target": { - "path": target_path, - }, - "args": args, - "workflows": { - (target_path): { - "source": source, - "files": {}, - }, - }, - }); - if let Some(git) = git { - manifest["git"] = git; - } - + let target = if git { + let sha = init_remote_fixture(&context.temp_dir, "main"); + run_git(&context.temp_dir, &[ + "remote", + "add", + "origin", + "https://github.com/fabro-sh/seeded-fixture.git", + ]); + serde_json::json!({"kind": "git", "repo": "fabro-sh/seeded-fixture", "branch": "main", "sha": sha}) + } else { + serde_json::json!({"kind": "none"}) + }; let (client, base_url) = server_endpoint(&context.storage_dir) .expect("test server endpoint should be available for seeded run creation"); + let path = + fabro_types::WorkflowPath::new(target_path).expect("seeded workflow path should be valid"); + let version = fabro_types::WorkflowVersion::new( + path.clone(), + std::collections::BTreeMap::from([(path, source.to_string())]), + std::collections::BTreeMap::new(), + ) + .expect("seeded workflow registration should succeed"); + let registered = client + .post(format!("{base_url}/api/v1/workflow-versions")) + .json(&version) + .send() + .await + .expect("seeded workflow registration should succeed"); + let registered = expect_reqwest_status( + registered, + fabro_http::StatusCode::CREATED, + "register seeded workflow", + ) + .await; + let registered: serde_json::Value = registered + .json() + .await + .expect("workflow registration response should be JSON"); + let intent = serde_json::json!({ + "workflow_version_id": registered["workflow_version_id"], + "target": target, + "environment_id": "default", + "args": args, + }); let response = client .post(format!("{base_url}/api/v1/runs")) .header("user-agent", "fabro-cli/test") - .json(&manifest) + .json(&intent) .send() .await .expect("seeded run create request should execute"); @@ -1703,6 +1715,18 @@ async fn append_run_event( .await; } +fn test_label_map(context: &TestContext) -> std::collections::HashMap { + test_labels(context) + .into_iter() + .map(|label| { + let (key, value) = label + .split_once('=') + .expect("test labels should contain a key and value"); + (key.to_string(), value.to_string()) + }) + .collect() +} + fn test_labels(context: &TestContext) -> Vec { vec![context.test_run_label(), context.test_case_label()] } diff --git a/lib/apps/fabro-cli/tests/it/support/mod.rs b/lib/apps/fabro-cli/tests/it/support/mod.rs index f4a2d8f43..d4fc878e8 100644 --- a/lib/apps/fabro-cli/tests/it/support/mod.rs +++ b/lib/apps/fabro-cli/tests/it/support/mod.rs @@ -53,7 +53,6 @@ pub(crate) fn run_projection_json(run_id: &str, status: &serde_json::Value) -> s source_directory: Some("/srv/repo".to_string()), labels: std::collections::HashMap::default(), provenance: test_support::test_run_provenance(), - manifest_blob: None, definition_blob: None, spec_blob: None, git: None, diff --git a/lib/apps/fabro-server/src/run_compiler.rs b/lib/apps/fabro-server/src/run_compiler.rs index 0d076f9d6..bc985f4c8 100644 --- a/lib/apps/fabro-server/src/run_compiler.rs +++ b/lib/apps/fabro-server/src/run_compiler.rs @@ -4,9 +4,9 @@ //! The pipeline has four stages, each its own function with typed input and //! output: //! -//! 1. [`normalize_source`] — resolve the bundle entrypoint and parse the -//! bundle-relative settings sources (workflow and project layers, with -//! dockerfile references inlined from bundled files). +//! 1. [`normalize_source`] — resolve the bundle entrypoint and retain the +//! admitted workflow settings, whose dockerfile references are already +//! inlined. //! 2. [`layer_settings`] + [`apply_run_variables`] + graph compilation — layer //! settings from every configured source, substitute the run-scoped variable //! snapshot, then parse/transform/validate the graph through the @@ -30,8 +30,8 @@ use std::sync::Arc; use fabro_config::parse::{self, ParseError, SettingsSource}; use fabro_config::{ - CliLayer, EnvironmentDockerfileLayer, EnvironmentImageLayer, EnvironmentLayer, MergeMap, - RunLayer, SettingsLayer, WorkflowSettingsBuilder, + EnvironmentDockerfileLayer, EnvironmentImageLayer, EnvironmentLayer, MergeMap, RunLayer, + SettingsLayer, WorkflowSettingsBuilder, }; use fabro_llm::lithos_catalog::Catalog; use fabro_types::settings::interp::{InterpString, ResolveError}; @@ -50,23 +50,6 @@ use fabro_workflow::workflow_bundle::{BundledWorkflow, WorkflowBundle}; use lithos_llm::catalog::ProviderId; use tokio::task; -/// One project settings source in the acquired source's path namespace. -#[derive(Debug)] -pub(crate) struct ProjectSettingsSource { - pub(crate) path: std::result::Result, - pub(crate) toml: String, -} - -/// A project settings path that the source adapter could not normalize. -#[derive(Debug, thiserror::Error)] -pub(crate) enum ProjectSettingsPathError { - #[error("project settings path is missing")] - Missing, - - #[error("invalid project settings path: {path}")] - Invalid { path: String }, -} - /// Transport-neutral inputs for compiling one submitted run. /// /// Identity (`run_id`), lineage, title, git metadata, and provenance are @@ -81,10 +64,8 @@ pub(crate) struct RawRunCompilerInput { pub(crate) server_run_defaults: RunLayer, pub(crate) server_environment_defaults: MergeMap, pub(crate) server_mcp_catalog: HashMap, - pub(crate) settings_input: RunCompilerSettingsInput, - pub(crate) user_toml: Vec, + pub(crate) workflow_layer: Option, pub(crate) run_overrides: Option, - pub(crate) cli_overrides: Option, pub(crate) input_overrides: HashMap, pub(crate) inline_goal_override: Option, pub(crate) run_id: Option, @@ -97,55 +78,38 @@ pub(crate) struct RawRunCompilerInput { pub(crate) target: Option, pub(crate) provenance: RunProvenance, pub(crate) web_url: Option, - pub(crate) submitted_manifest_bytes: Option>, pub(crate) automation: Option, } -/// Settings already admitted by the caller, or the unchanged legacy manifest -/// inputs that still need their historical parsing and lookup behavior. -#[derive(Debug)] -pub(crate) enum RunCompilerSettingsInput { - LegacyManifest { - project_settings: Vec, - }, - Admitted { - workflow_layer: Option>, - }, -} - -/// Stage-one output: the selected bundled workflow and all client settings -/// sources have been parsed and normalized, but no settings have been layered. +/// Stage-one output: the selected bundled workflow and admitted settings, +/// before layering server defaults and run overrides. pub(crate) struct NormalizedRun { workflow_bundle: WorkflowBundle, entrypoint: ManifestPath, workflow: BundledWorkflow, workflow_layer: Option, - project_layers: Vec, - user_toml: Vec, cwd: PathBuf, server_run_defaults: RunLayer, server_environment_defaults: MergeMap, server_mcp_catalog: HashMap, run_overrides: Option, - cli_overrides: Option, input_overrides: HashMap, inline_goal_override: Option, metadata: RunMetadata, } struct RunMetadata { - run_id: Option, - storage_root: PathBuf, - workflow_slug: Option, + run_id: Option, + storage_root: PathBuf, + workflow_slug: Option, workflow_version_id: Option, - target: Option, - submitted_manifest_bytes: Option>, - title: Option, - automation: Option, - git: Option, - parent_id: Option, - provenance: RunProvenance, - web_url: Option, + target: Option, + title: Option, + automation: Option, + git: Option, + parent_id: Option, + provenance: RunProvenance, + web_url: Option, } /// Settings-layered output. Variable substitution is a separate stage so @@ -182,18 +146,6 @@ impl PreparedRun { self } - pub(crate) fn with_identity( - mut self, - run_id: Option, - parent_id: Option, - title: Option, - ) -> Self { - self.layered.metadata.run_id = run_id; - self.layered.metadata.parent_id = parent_id; - self.layered.metadata.title = title; - self - } - pub(crate) fn parent_id(&self) -> Option { self.layered.metadata.parent_id } @@ -247,9 +199,7 @@ pub(crate) enum RunCompilerError { Workflow(#[from] WorkflowError), } -// The `Display` strings below are pinned to the pre-extraction wire -// contract: both the create handler and the manifest preparation path render -// them directly into HTTP 400 details. +// Shared source errors also serve manifest preview operations. #[derive(Debug, thiserror::Error)] pub(crate) enum InvalidSourceError { #[error("manifest target path is missing from workflows map")] @@ -277,17 +227,11 @@ pub(crate) enum InvalidSettingsError { source: ParseError, }, - #[error(transparent)] - User(fabro_config::Error), - #[error("failed to resolve manifest settings")] Resolve { #[source] source: fabro_config::ResolveErrors, }, - - #[error("{}", project_path_error(.source))] - ProjectPath { source: ProjectSettingsPathError }, } #[derive(Debug, thiserror::Error)] @@ -305,23 +249,12 @@ pub(crate) enum VariableInterpolationError { pub(crate) type Result = std::result::Result; -fn project_path_error(source: &ProjectSettingsPathError) -> String { - match source { - ProjectSettingsPathError::Missing => { - "invalid manifest project config path: missing path".to_string() - } - ProjectSettingsPathError::Invalid { path } => { - format!("invalid manifest project config path: {path}") - } - } -} - fn invalid_settings(source: InvalidSettingsError) -> RunCompilerError { RunCompilerError::InvalidSettings(Box::new(source)) } -/// Normalize the bundle entrypoint and parse workflow/project settings while -/// resolving dockerfile references against the selected workflow's files. +/// Normalize the bundle entrypoint while retaining the admitted workflow +/// settings. pub(crate) fn normalize_source(input: RawRunCompilerInput) -> Result { let RawRunCompilerInput { workflow_bundle, @@ -330,10 +263,8 @@ pub(crate) fn normalize_source(input: RawRunCompilerInput) -> Result Result Result { - let workflow_layer = workflow - .config - .as_ref() - .map(|config| { - settings_layer_with_resolved_dockerfiles( - &config.source, - &config.path, - &workflow.files, - SettingsSource::Workflow, - ) - }) - .transpose()?; - let project_layers = project_settings - .into_iter() - .map(|project| { - let path = project.path.map_err(|source| { - invalid_settings(InvalidSettingsError::ProjectPath { source }) - })?; - settings_layer_with_resolved_dockerfiles( - &project.toml, - &path, - &workflow.files, - SettingsSource::Project, - ) - }) - .collect::>>()?; - (workflow_layer, project_layers) - } - RunCompilerSettingsInput::Admitted { workflow_layer } => { - (workflow_layer.map(|layer| *layer), Vec::new()) - } - }; - Ok(NormalizedRun { workflow_bundle, entrypoint, workflow, workflow_layer, - project_layers, - user_toml, cwd, server_run_defaults, server_environment_defaults, server_mcp_catalog, run_overrides, - cli_overrides, input_overrides, inline_goal_override, metadata: RunMetadata { @@ -413,7 +305,6 @@ pub(crate) fn normalize_source(input: RawRunCompilerInput) -> Result Result { entrypoint, workflow, workflow_layer, - project_layers, - user_toml, cwd, server_run_defaults, server_environment_defaults, server_mcp_catalog, run_overrides, - cli_overrides, input_overrides, inline_goal_override, metadata, @@ -450,20 +338,9 @@ pub(crate) fn layer_settings(normalized: NormalizedRun) -> Result { if let Some(run) = run_overrides { builder = builder.run_overrides(run); } - if let Some(cli) = cli_overrides { - builder = builder.cli_overrides(cli); - } if let Some(layer) = workflow_layer { builder = builder.workflow_layer(layer); } - for layer in project_layers { - builder = builder.project_layer(layer); - } - for source in user_toml { - builder = builder - .user_toml(&source) - .map_err(|source| invalid_settings(InvalidSettingsError::User(source)))?; - } let mut settings = builder .build() .map_err(|source| invalid_settings(InvalidSettingsError::Resolve { source }))?; @@ -562,7 +439,7 @@ fn pin_models(compiled: GraphCompiledRun, catalog: &Catalog) -> Result CreateRunPersistenceInput { let PinnedRun { @@ -575,7 +452,6 @@ pub(crate) fn assemble_run(pinned: PinnedRun) -> CreateRunPersistenceInput { workflow_slug, workflow_version_id, target, - submitted_manifest_bytes, title, automation, git, @@ -589,7 +465,6 @@ pub(crate) fn assemble_run(pinned: PinnedRun) -> CreateRunPersistenceInput { workflow_slug, workflow_version_id, target, - submitted_manifest_bytes, title, automation, git, @@ -735,6 +610,15 @@ mod tests { ) -> RawRunCompilerInput { let entrypoint = manifest_path("flows/workflow.fabro"); let workflow = workflow(&entrypoint, workflow_toml, files); + let workflow_layer = workflow.config.as_ref().map(|config| { + settings_layer_with_resolved_dockerfiles( + &config.source, + &config.path, + &workflow.files, + SettingsSource::Workflow, + ) + .expect("valid admitted workflow settings") + }); RawRunCompilerInput { workflow_bundle: WorkflowBundle::new(HashMap::from([(entrypoint.clone(), workflow)])), entrypoint, @@ -742,12 +626,8 @@ mod tests { server_run_defaults: RunLayer::default(), server_environment_defaults: fabro_environment::seeded_catalog_layer(), server_mcp_catalog: HashMap::new(), - settings_input: RunCompilerSettingsInput::LegacyManifest { - project_settings: Vec::new(), - }, - user_toml: Vec::new(), + workflow_layer, run_overrides: None, - cli_overrides: None, input_overrides: HashMap::new(), inline_goal_override: None, run_id: Some(RunId::new()), @@ -760,7 +640,6 @@ mod tests { target: None, provenance: provenance(), web_url: None, - submitted_manifest_bytes: None, automation: None, } } @@ -795,7 +674,7 @@ mod tests { } #[test] - fn normalize_source_rejects_missing_dockerfile_with_pinned_message() { + fn settings_parser_rejects_missing_dockerfile_with_pinned_message() { let workflow_toml = r#" _version = 1 @@ -803,7 +682,12 @@ _version = 1 dockerfile = { path = "Dockerfile" } "#; - let Err(error) = normalize_source(raw_input(Some(workflow_toml), HashMap::new())) else { + let Err(error) = settings_layer_with_resolved_dockerfiles( + workflow_toml, + &manifest_path("flows/workflow.toml"), + &HashMap::new(), + SettingsSource::Workflow, + ) else { panic!("missing dockerfile should fail"); }; @@ -818,7 +702,7 @@ dockerfile = { path = "Dockerfile" } } #[test] - fn normalize_source_preserves_settings_parse_source_chain() { + fn settings_parser_preserves_parse_source_chain() { let workflow_toml = r#" _version = 1 @@ -826,7 +710,12 @@ _version = 1 key = "value" "#; - let Err(error) = normalize_source(raw_input(Some(workflow_toml), HashMap::new())) else { + let Err(error) = settings_layer_with_resolved_dockerfiles( + workflow_toml, + &manifest_path("flows/workflow.toml"), + &HashMap::new(), + SettingsSource::Workflow, + ) else { panic!("unknown settings key should fail"); }; @@ -885,30 +774,6 @@ target = "workflow" include = ["reports/{{ vars.owner }}/*.json"] "#; let mut input = raw_input(Some(workflow_toml), HashMap::new()); - let RunCompilerSettingsInput::LegacyManifest { project_settings } = - &mut input.settings_input - else { - panic!("test fixture should use legacy manifest settings"); - }; - project_settings.push(ProjectSettingsSource { - path: Ok(manifest_path(".fabro/project.toml")), - toml: r#" -_version = 1 - -[run.metadata] -layer = "project" -"# - .to_string(), - }); - input.user_toml = vec![ - r#" -_version = 1 - -[run.metadata] -layer = "user" -"# - .to_string(), - ]; input.run_overrides = Some( toml::from_str::( r#" @@ -1033,7 +898,6 @@ include = ["reports/{{ vars.path }}/*.json"] trigger_id: Some("schedule".to_string()), workflow_source: None, }; - let submitted = b"submitted manifest".to_vec(); let workflow_version_id = fabro_types::test_support::test_workflow_version_id(); let mut input = raw_input(None, HashMap::new()); input.run_id = Some(run_id); @@ -1042,7 +906,6 @@ include = ["reports/{{ vars.path }}/*.json"] input.workflow_slug = Some("compiler-boundary".to_string()); input.workflow_version_id = Some(workflow_version_id); input.web_url = Some(format!("https://fabro.test/runs/{run_id}")); - input.submitted_manifest_bytes = Some(submitted.clone()); input.automation = Some(automation.clone()); input.input_overrides.insert( "target".to_string(), @@ -1064,10 +927,6 @@ include = ["reports/{{ vars.path }}/*.json"] assert_eq!(persistence.run_id(), run_id); assert_eq!(persistence.workflow_slug(), Some("compiler-boundary")); assert_eq!(persistence.workflow_version_id(), Some(workflow_version_id)); - assert_eq!( - persistence.submitted_manifest_bytes(), - Some(submitted.as_slice()) - ); assert_eq!(persistence.automation(), Some(&automation)); assert_eq!( persistence diff --git a/lib/apps/fabro-server/src/run_files.rs b/lib/apps/fabro-server/src/run_files.rs index 348165c42..d4d87a015 100644 --- a/lib/apps/fabro-server/src/run_files.rs +++ b/lib/apps/fabro-server/src/run_files.rs @@ -2294,7 +2294,6 @@ index 1111111..2222222 160000 source_directory: None, labels: HashMap::default(), provenance: test_support::test_run_provenance(), - manifest_blob: None, definition_blob: None, spec_blob: None, git: None, diff --git a/lib/apps/fabro-server/src/server/handler/events.rs b/lib/apps/fabro-server/src/server/handler/events.rs index 8dfd3cd70..70dbe9942 100644 --- a/lib/apps/fabro-server/src/server/handler/events.rs +++ b/lib/apps/fabro-server/src/server/handler/events.rs @@ -633,7 +633,6 @@ mod stage_events_tests { target: None, automation: None, provenance: test_support::test_run_provenance(), - manifest_blob: None, spec_blob: None, git: None, fork_source_ref: None, diff --git a/lib/apps/fabro-server/src/server/handler/pair.rs b/lib/apps/fabro-server/src/server/handler/pair.rs index cebb64b56..95fe1decb 100644 --- a/lib/apps/fabro-server/src/server/handler/pair.rs +++ b/lib/apps/fabro-server/src/server/handler/pair.rs @@ -1055,7 +1055,6 @@ mod tests { target: None, automation: None, provenance: test_support::test_run_provenance(), - manifest_blob: None, spec_blob: None, git: None, fork_source_ref: None, diff --git a/lib/apps/fabro-server/src/server/handler/runs.rs b/lib/apps/fabro-server/src/server/handler/runs.rs index 4020d29eb..3437e9b0f 100644 --- a/lib/apps/fabro-server/src/server/handler/runs.rs +++ b/lib/apps/fabro-server/src/server/handler/runs.rs @@ -3,7 +3,6 @@ use std::io::ErrorKind; use std::path::PathBuf; use std::sync::Arc; -use anyhow::Context as _; use axum::extract::{Path, Query, State}; use axum::http::{HeaderMap, StatusCode, header}; use axum::response::{IntoResponse, Response}; @@ -15,10 +14,10 @@ use base64::engine::general_purpose::STANDARD as BASE64_STANDARD; use bytes::Bytes; use chrono::{DateTime, Utc}; use fabro_api::types::{ - BoardColumn, ManifestConfigType, ManifestGoalType, RunIntent, RunManifest, SubmitAnswerRequest, - UpdateRunParentRequest, UpdateRunRequest, + BoardColumn, RunIntent, RunManifest, SubmitAnswerRequest, UpdateRunParentRequest, + UpdateRunRequest, }; -use fabro_config::{CliLayer, RunLayer, Storage, project}; +use fabro_config::{Storage, project}; use fabro_environment::{DEFAULT_ENVIRONMENT_ID, EnvironmentId}; use fabro_interview::AnswerSubmission; use fabro_llm::Client as LlmClient; @@ -38,9 +37,9 @@ use fabro_util::error as error_util; use fabro_util::version::FABRO_VERSION; use fabro_workflow::command_log::{command_log_path, read_json_string_blob, read_log_slice}; use fabro_workflow::run_status::RunStatus; -use fabro_workflow::workflow_bundle::WorkflowBundle; use fabro_workflow::{Error as WorkflowError, operations}; use lithos_llm::catalog::ProviderId; +use serde::de::IgnoredAny; use strum::VariantArray as _; use tokio::fs; use tracing::info; @@ -56,10 +55,7 @@ use crate::principal_middleware::{ RequireCommandLog, RequireRunManagementTarget, RequireRunScoped, RequireRunStageScoped, RequiredRunManagementActor, RequiredUser, }; -use crate::run_compiler::{ - self, ProjectSettingsPathError, ProjectSettingsSource, RawRunCompilerInput, RunCompilerError, - RunCompilerSettingsInput, -}; +use crate::run_compiler::{self, RawRunCompilerInput}; use crate::run_files::{list_run_commits, list_run_files}; use crate::run_intent::{ EnvironmentSelectionError, PreparedIntentTarget, RunIntentAdmissionError, @@ -528,79 +524,32 @@ async fn create_run( headers: HeaderMap, body: Bytes, ) -> Response { - // Both lanes parse the raw bytes directly so serde_json keeps its - // duplicate-key rejection and line/column error locations; a JSON `Value` - // round-trip would silently collapse duplicate keys to last-key-wins. - let intent_error = match serde_json::from_slice::(&body) { - Ok(intent) => { - return Box::pin(create_run_from_intent(state, CreateRunFromIntentRequest { - intent, - explicit_run_id: None, - actor, - headers, - automation: None, - })) - .await; - } - Err(err) => err, + // Decode the original bytes so duplicate fields and error locations survive. + let intent = match serde_json::from_slice::(&body) { + Ok(intent) => intent, + Err(error) => return create_run_parse_error(&body, &error), }; - let req = match serde_json::from_slice::(&body) { - Ok(req) => req, - Err(manifest_error) => { - return create_run_parse_error(&body, &intent_error, &manifest_error); - } - }; - let explicit_title_supplied = req.title.is_some(); - Box::pin(create_run_from_manifest( - state, - CreateRunFromManifestRequest { - manifest: req, - submitted_manifest_bytes: body.to_vec(), - explicit_run_id: None, - explicit_title_supplied, - actor, - headers, - automation: None, - target: None, - }, - )) + Box::pin(create_run_from_intent(state, CreateRunFromIntentRequest { + intent, + explicit_run_id: None, + actor, + headers, + automation: None, + })) .await } -/// Attribute a create-run body that neither lane accepted. A body carrying -/// any of the legacy manifest's required keys is a defective manifest even -/// when a stray `workflow_version_id` rides along, and keeps the manifest -/// lane's `400` contract; only an intent-shaped body gets the intent `422`. -fn create_run_parse_error( - body: &[u8], - intent_error: &serde_json::Error, - manifest_error: &serde_json::Error, -) -> Response { - let value = match serde_json::from_slice::(body) { - Ok(value) => value, - Err(err) => { - return ApiError::with_code(StatusCode::BAD_REQUEST, err.to_string(), "invalid_json") - .into_response(); - } - }; - let has_key = |key: &str| { - value - .as_object() - .is_some_and(|object| object.contains_key(key)) - }; - if has_key("workflow_version_id") - && !has_key("version") - && !has_key("cwd") - && !has_key("workflows") - { - return ApiError::with_code( - StatusCode::UNPROCESSABLE_ENTITY, - intent_error.to_string(), - "run_intent_invalid", - ) - .into_response(); +fn create_run_parse_error(body: &[u8], intent_error: &serde_json::Error) -> Response { + if let Err(error) = serde_json::from_slice::(body) { + return ApiError::with_code(StatusCode::BAD_REQUEST, error.to_string(), "invalid_json") + .into_response(); } - ApiError::bad_request(manifest_error.to_string()).into_response() + ApiError::with_code( + StatusCode::UNPROCESSABLE_ENTITY, + intent_error.to_string(), + "run_intent_invalid", + ) + .into_response() } pub(crate) struct CreateRunFromIntentRequest { @@ -719,12 +668,8 @@ pub(crate) async fn create_run_from_intent( server_run_defaults: state.manifest_run_defaults().as_ref().clone(), server_environment_defaults: state.environment_store().catalog_layer().as_ref().clone(), server_mcp_catalog: state.mcp_server_store().catalog_settings(), - settings_input: RunCompilerSettingsInput::Admitted { - workflow_layer: lowered.workflow_layer.map(Box::new), - }, - user_toml: Vec::new(), + workflow_layer: lowered.workflow_layer, run_overrides: Some(run_overrides), - cli_overrides: None, input_overrides, inline_goal_override: intent.goal, run_id: explicit_run_id, @@ -739,7 +684,6 @@ pub(crate) async fn create_run_from_intent( target: None, provenance: run_provenance(&headers, &actor), web_url: None, - submitted_manifest_bytes: None, automation, }; let normalized = match run_compiler::normalize_source(raw_compiler_input) { @@ -775,18 +719,11 @@ pub(crate) async fn create_run_from_intent( return response; } let prepared = prepared.with_web_url(state.run_web_url(&run_id)); - finalize_created_run( - state, - prepared, - explicit_title_supplied, - entrypoint, - CreatedRunErrorStyle::Intent, - ) - .await + finalize_created_run(state, prepared, explicit_title_supplied, entrypoint).await } -/// Shared parent-link validation for both create lanes: a run must not be -/// its own parent, and an explicit parent must pass [`validate_parent_link`]. +/// A run must not be its own parent; an explicit parent must pass +/// [`validate_parent_link`]. async fn validate_optional_parent( state: &AppState, run_id: RunId, @@ -803,91 +740,12 @@ async fn validate_optional_parent( .map_err(IntoResponse::into_response) } -/// Which endpoint dialect's pinned error mapping the shared creation tail -/// speaks: the RunIntent admission contract or the legacy manifest wire -/// contract. -enum CreatedRunErrorStyle { - Intent, - LegacyManifest, -} - -impl CreatedRunErrorStyle { - fn compiler_error(&self, error: RunCompilerError) -> Response { - match self { - Self::Intent => run_intent_admission_error(error.into()), - Self::LegacyManifest => run_compiler_error_response(error), - } - } - - fn persist_error(&self, error: &WorkflowError) -> Response { - match self { - Self::Intent => { - tracing::error!( - error = %error, - error_chain = ?error_util::collect_chain(error), - "Failed to persist admitted run intent" - ); - intent_error( - StatusCode::INTERNAL_SERVER_ERROR, - "failed to persist run", - "run_persistence_failed", - ) - } - Self::LegacyManifest => ApiError::new( - StatusCode::INTERNAL_SERVER_ERROR, - format!("Failed to persist run state: {error}"), - ) - .into_response(), - } - } - - fn missing_summary_error(&self) -> Response { - match self { - Self::Intent => intent_error( - StatusCode::INTERNAL_SERVER_ERROR, - "created run summary is unavailable", - "run_persistence_failed", - ), - Self::LegacyManifest => ApiError::not_found("Run not found.").into_response(), - } - } - - fn summary_error(&self, error: &dyn std::fmt::Display) -> Response { - match self { - Self::Intent => { - tracing::error!(error = %error, "Failed to read admitted run summary"); - intent_error( - StatusCode::INTERNAL_SERVER_ERROR, - "failed to read created run", - "run_persistence_failed", - ) - } - Self::LegacyManifest => { - ApiError::new(StatusCode::INTERNAL_SERVER_ERROR, error.to_string()).into_response() - } - } - } - - fn log_created(&self, run_id: RunId) { - // The legacy manifest lane logs its "Run created" line before - // compilation, so only the intent lane logs here. - if matches!(self, Self::Intent) { - info!(run_id = %run_id, "Run created from intent"); - } - } -} - -/// The shared tail of both run-creation lanes: resolve LLM readiness, compile -/// and pin, persist, register the managed run, spawn title generation, and -/// render the 201 response. Identity (run id, parent link, web URL) must -/// already be resolved on `prepared`; only error mapping differs per lane, -/// through [`CreatedRunErrorStyle`]. +/// Compile, persist, register, and render the admitted run. async fn finalize_created_run( state: Arc, prepared: run_compiler::PreparedRun, explicit_title_supplied: bool, title_generation_target: ManifestPath, - style: CreatedRunErrorStyle, ) -> Response { let catalog = state.catalog(); // Resolve once: we need both the provider IDs (for the run create input @@ -914,7 +772,7 @@ async fn finalize_created_run( .await { Ok(pinned) => pinned, - Err(error) => return style.compiler_error(error), + Err(error) => return run_intent_admission_error(error.into()), }; let persistence_input = run_compiler::assemble_run(pinned); let created = match Box::pin(operations::persist_create_run( @@ -924,7 +782,14 @@ async fn finalize_created_run( .await { Ok(created) => created, - Err(error) => return style.persist_error(&error), + Err(error) => { + tracing::error!(error = %error, error_chain = ?error_util::collect_chain(&error), "Failed to persist admitted run intent"); + return intent_error( + StatusCode::INTERNAL_SERVER_ERROR, + "failed to persist run", + "run_persistence_failed", + ); + } }; let created_at = created.run_id.created_at(); let summary = match state @@ -934,8 +799,21 @@ async fn finalize_created_run( .await { Ok(Some(summary)) => summary, - Ok(None) => return style.missing_summary_error(), - Err(error) => return style.summary_error(&error), + Ok(None) => { + return intent_error( + StatusCode::INTERNAL_SERVER_ERROR, + "created run summary is unavailable", + "run_persistence_failed", + ); + } + Err(error) => { + tracing::error!(error = %error, "Failed to read admitted run summary"); + return intent_error( + StatusCode::INTERNAL_SERVER_ERROR, + "failed to read created run", + "run_persistence_failed", + ); + } }; let deterministic_title = summary.title.clone(); { @@ -972,7 +850,7 @@ async fn finalize_created_run( } } } - style.log_created(created.run_id); + info!(run_id = %created.run_id, "Run created from intent"); ( StatusCode::CREATED, Json(state.decorate_run_summary(summary).await), @@ -1068,9 +946,7 @@ fn run_intent_admission_error(error: RunIntentAdmissionError) -> Response { "credential_store_error", ), }, - // The top-level compiler/lowering message is the same curated detail - // the legacy manifest lane returns for identical defects; the full - // source chain stays in the warn log above. + // Return the curated compiler detail; retain its source chain in the log. RunIntentAdmissionError::Compiler(error) => intent_error( StatusCode::UNPROCESSABLE_ENTITY, format!("run intent could not be compiled: {error}"), @@ -1199,249 +1075,6 @@ async fn validate_intent_environment( Ok(()) } -pub(crate) struct CreateRunFromManifestRequest { - pub(crate) manifest: RunManifest, - pub(crate) submitted_manifest_bytes: Vec, - /// Run ID preallocated by server-side automation code, never supplied by - /// an HTTP create body. - pub(crate) explicit_run_id: Option, - pub(crate) explicit_title_supplied: bool, - pub(crate) actor: Principal, - pub(crate) headers: HeaderMap, - pub(crate) automation: Option, - /// Trusted canonical target supplied by an internal manifest producer. - /// Public legacy manifest requests always leave this absent. - pub(crate) target: Option, -} - -struct ManifestRunCompilerAdapter { - workflow_bundle: WorkflowBundle, - entrypoint: ManifestPath, - cwd: PathBuf, - project_settings: Vec, - user_toml: Vec, - run_overrides: Option, - cli_overrides: Option, - input_overrides: HashMap, - inline_goal_override: Option, -} - -fn adapt_manifest_source_for_run_compiler( - manifest: &RunManifest, -) -> anyhow::Result { - if manifest.version != 1 { - anyhow::bail!("unsupported manifest version {}", manifest.version); - } - let cwd = PathBuf::from(&manifest.cwd); - let entrypoint = ManifestPath::from_wire(&manifest.target.path) - .ok_or_else(|| anyhow::anyhow!("invalid manifest target path: {}", manifest.target.path))?; - let workflow_bundle = run_manifest::workflow_bundle_from_manifest(&manifest.workflows)?; - if workflow_bundle.workflow(&entrypoint).is_none() { - anyhow::bail!("manifest target path is missing from workflows map"); - } - let overrides = run_manifest::manifest_args_overrides(manifest.args.as_ref()) - .context("failed to parse manifest args")?; - let project_settings = manifest - .configs - .iter() - .filter(|config| config.type_ == ManifestConfigType::Project) - .filter_map(|config| config.source.as_ref().map(|source| (config, source))) - .map(|(config, source)| ProjectSettingsSource { - path: normalize_project_settings_path(config.path.as_deref(), &cwd), - toml: source.clone(), - }) - .collect(); - let user_toml = manifest - .configs - .iter() - .filter(|config| config.type_ == ManifestConfigType::User) - .filter_map(|config| config.source.clone()) - .collect(); - let inline_goal_override = manifest - .goal - .as_ref() - .filter(|goal| goal.type_ != ManifestGoalType::Graph) - .map(|goal| goal.text.clone()); - - Ok(ManifestRunCompilerAdapter { - workflow_bundle, - entrypoint, - cwd, - project_settings, - user_toml, - run_overrides: overrides.run, - cli_overrides: overrides.cli, - input_overrides: overrides.input_overrides, - inline_goal_override, - }) -} - -fn normalize_project_settings_path( - path: Option<&str>, - cwd: &std::path::Path, -) -> Result { - let path = path.ok_or(ProjectSettingsPathError::Missing)?; - let path_ref = std::path::Path::new(path); - let manifest_path = if path_ref.is_absolute() { - ManifestPath::from_absolute(path_ref, cwd) - } else { - ManifestPath::from_wire(path) - }; - manifest_path.ok_or_else(|| ProjectSettingsPathError::Invalid { - path: path.to_string(), - }) -} - -struct ManifestRunIdentity { - run_id: Option, - parent_id: Option, - title: Option, -} - -fn manifest_run_identity( - manifest: &RunManifest, - explicit_run_id: Option, -) -> anyhow::Result { - let title = manifest - .title - .as_ref() - .map(|title| fabro_types::normalize_explicit_run_title(title.as_str())) - .transpose()?; - let parent_id = manifest - .parent_id - .as_deref() - .map(str::parse::) - .transpose() - .context("invalid parent run ID")?; - Ok(ManifestRunIdentity { - run_id: explicit_run_id, - parent_id, - title, - }) -} - -/// Map a [`RunCompilerError`] onto the create endpoint's pre-extraction wire -/// contract. The 400 details for source, settings, and interpolation errors -/// are the error types' own `Display` strings, which are pinned to the -/// legacy messages. -fn run_compiler_error_response(error: RunCompilerError) -> Response { - match error { - RunCompilerError::InvalidSource(_) - | RunCompilerError::InvalidSettings(_) - | RunCompilerError::VariableInterpolation(_) => { - ApiError::bad_request(error.to_string()).into_response() - } - RunCompilerError::Workflow( - WorkflowError::ValidationFailed { .. } | WorkflowError::Parse(_), - ) => ApiError::bad_request("Validation failed").into_response(), - RunCompilerError::Workflow( - err @ (WorkflowError::ModelSelection(_) | WorkflowError::ModelReference(_)), - ) => ApiError::bad_request(err.to_string()).into_response(), - RunCompilerError::Workflow(err) => ApiError::new( - StatusCode::INTERNAL_SERVER_ERROR, - format!("Failed to persist run state: {err}"), - ) - .into_response(), - } -} - -pub(crate) async fn create_run_from_manifest( - state: Arc, - request: CreateRunFromManifestRequest, -) -> Response { - let CreateRunFromManifestRequest { - manifest, - submitted_manifest_bytes, - explicit_run_id, - explicit_title_supplied, - actor, - headers, - automation, - target, - } = request; - let manifest_run_defaults = state.manifest_run_defaults(); - let manifest_environment_defaults = state.environment_store().catalog_layer(); - let manifest_mcp_server_catalog = state.mcp_server_store().catalog_settings(); - let manifest_adapter = match adapt_manifest_source_for_run_compiler(&manifest) { - Ok(adapter) => adapter, - Err(err) => return ApiError::bad_request(err.to_string()).into_response(), - }; - let title_generation_target = manifest_adapter.entrypoint.clone(); - let raw_compiler_input = RawRunCompilerInput { - workflow_bundle: manifest_adapter.workflow_bundle, - entrypoint: manifest_adapter.entrypoint, - cwd: manifest_adapter.cwd, - server_run_defaults: manifest_run_defaults.as_ref().clone(), - server_environment_defaults: manifest_environment_defaults.as_ref().clone(), - server_mcp_catalog: manifest_mcp_server_catalog, - settings_input: RunCompilerSettingsInput::LegacyManifest { - project_settings: manifest_adapter.project_settings, - }, - user_toml: manifest_adapter.user_toml, - run_overrides: manifest_adapter.run_overrides, - cli_overrides: manifest_adapter.cli_overrides, - input_overrides: manifest_adapter.input_overrides, - inline_goal_override: manifest_adapter.inline_goal_override, - run_id: None, - title: None, - parent_id: None, - git: manifest.git.clone(), - storage_root: state.server_storage_dir(), - workflow_slug: None, - workflow_version_id: None, - target, - provenance: run_provenance(&headers, &actor), - web_url: None, - submitted_manifest_bytes: Some(submitted_manifest_bytes), - automation, - }; - let normalized = match run_compiler::normalize_source(raw_compiler_input) { - Ok(normalized) => normalized, - Err(err) => return run_compiler_error_response(err), - }; - let layered = match run_compiler::layer_settings(normalized) { - Ok(layered) => layered, - Err(err) => return run_compiler_error_response(err), - }; - let vars = match snapshot_run_variables(&state).await { - Ok(vars) => vars, - Err(err) => { - return ApiError::new(StatusCode::INTERNAL_SERVER_ERROR, err.to_string()) - .into_response(); - } - }; - let prepared = match run_compiler::apply_run_variables(layered, vars) { - Ok(prepared) => prepared, - Err(err) => return run_compiler_error_response(err), - }; - let identity = match manifest_run_identity(&manifest, explicit_run_id) { - Ok(identity) => identity, - Err(err) => return ApiError::bad_request(err.to_string()).into_response(), - }; - let prepared = prepared.with_identity(identity.run_id, identity.parent_id, identity.title); - let (prepared, run_id) = prepared.resolve_run_id(); - let prepared = prepared.with_web_url(state.run_web_url(&run_id)); - let provider = run_manifest::effective_sandbox_provider(&prepared.settings().run); - if let Some(error) = - run_manifest::sandbox_provider_policy_error(&state.server_settings(), &provider) - { - return ApiError::bad_request(error).into_response(); - } - if let Err(response) = validate_optional_parent(&state, run_id, prepared.parent_id()).await { - return response; - } - info!(run_id = %run_id, "Run created"); - - finalize_created_run( - state, - prepared, - explicit_title_supplied, - title_generation_target, - CreatedRunErrorStyle::LegacyManifest, - ) - .await -} - struct GeneratedTitleTask { state: Arc, run_id: RunId, diff --git a/lib/apps/fabro-server/src/server/handler/sessions.rs b/lib/apps/fabro-server/src/server/handler/sessions.rs index 9b3a01f58..94eadaba8 100644 --- a/lib/apps/fabro-server/src/server/handler/sessions.rs +++ b/lib/apps/fabro-server/src/server/handler/sessions.rs @@ -1894,7 +1894,6 @@ enabled = true source_directory: None, labels: HashMap::default(), provenance: test_support::test_run_provenance(), - manifest_blob: None, definition_blob: None, spec_blob: None, git: None, @@ -2058,15 +2057,17 @@ mod resume_tests { } /// A completed local dry run, so the session has a sandbox to reconnect. - async fn completed_run(app: &axum::Router) -> RunId { - let manifest = serde_json::json!({ - "version": 1, - "cwd": std::env::temp_dir().display().to_string(), - "args": { "dry_run": true }, - "target": { "path": "workflow.fabro" }, - "workflows": { "workflow.fabro": { "source": DOT, "files": {} } }, - }); - let created = json_response(app, post_json("/runs", &manifest), StatusCode::CREATED).await; + async fn completed_run(app: &axum::Router, workspace: &std::path::Path) -> RunId { + let path = fabro_types::WorkflowPath::new("workflow.fabro").unwrap(); + let version = fabro_types::WorkflowVersion::new( + path.clone(), + std::collections::BTreeMap::from([(path, DOT.to_string())]), + std::collections::BTreeMap::new(), + ) + .unwrap(); + let id = crate::test_support::test_register_workflow_version(app, &version, None).await; + let intent = serde_json::json!({"workflow_version_id": id, "target": {"kind": "folder", "path": workspace}, "environment_id": "local", "args": {"dry_run": true}}); + let created = json_response(app, post_json("/runs", &intent), StatusCode::CREATED).await; let run_id = created["id"].as_str().unwrap().to_string(); let start = Request::builder() .method("POST") @@ -2141,7 +2142,8 @@ mod resume_tests { let state = twin_backed_state(twin.base_url.clone(), &namespace); spawn_scheduler(Arc::clone(&state)); let app = build_test_router(Arc::clone(&state)); - let run_id = completed_run(&app).await; + let workspace = tempfile::tempdir().unwrap(); + let run_id = completed_run(&app, workspace.path()).await; let created = json_response( &app, diff --git a/lib/apps/fabro-server/src/server/tests.rs b/lib/apps/fabro-server/src/server/tests.rs index 2fd594239..65700bbbd 100644 --- a/lib/apps/fabro-server/src/server/tests.rs +++ b/lib/apps/fabro-server/src/server/tests.rs @@ -10,7 +10,7 @@ use std::sync::{Arc as StdArc, Mutex as StdMutex}; use async_zip::base::read::mem::ZipFileReader; use axum::body::Body; use axum::http::{Method, Request, header}; -use chrono::{Duration as ChronoDuration, SubsecRound as _, Utc}; +use chrono::{Duration as ChronoDuration, Utc}; use fabro_automation::AutomationId; use fabro_config::bind::Bind; use fabro_config::{ @@ -709,7 +709,11 @@ async fn create_run_with_bearer(app: &Router, bearer: &str) -> RunId { .uri(api("/runs")) .header(header::AUTHORIZATION, format!("Bearer {bearer}")) .header(header::CONTENT_TYPE, "application/json") - .body(manifest_body(MINIMAL_DOT)) + .body(Body::from( + test_intent_with_bearer(app, "workflow.fabro", MINIMAL_DOT, None, Some(bearer)) + .await + .to_string(), + )) .unwrap(), ) .await @@ -3210,8 +3214,34 @@ fn manifest_body(dot_source: &str) -> Body { Body::from(serde_json::to_string(&minimal_manifest_json(dot_source)).unwrap()) } -fn manifest_body_for(target_path: &str, dot_source: &str) -> Body { - Body::from(serde_json::to_string(&manifest_json(target_path, dot_source)).unwrap()) +async fn test_intent_with_bearer( + app: &Router, + entrypoint: &str, + source: &str, + config: Option<&str>, + bearer: Option<&str>, +) -> serde_json::Value { + let entrypoint = fabro_types::WorkflowPath::new(entrypoint).unwrap(); + let mut files = std::collections::BTreeMap::from([(entrypoint.clone(), source.to_string())]); + if let Some(config) = config { + files.insert( + entrypoint.resolve_reference("workflow.toml").unwrap(), + config.to_string(), + ); + } + let version = + fabro_types::WorkflowVersion::new(entrypoint, files, std::collections::BTreeMap::new()) + .unwrap(); + let id = crate::test_support::test_register_workflow_version(app, &version, bearer).await; + json!({"workflow_version_id": id, "target": {"kind": "none"}, "args": {}}) +} + +async fn test_intent(app: &Router, source: &str) -> serde_json::Value { + test_intent_with_bearer(app, "workflow.fabro", source, None, None).await +} + +async fn intent_body(app: &Router, source: &str) -> Body { + Body::from(test_intent(app, source).await.to_string()) } async fn create_run(app: &Router, dot_source: &str) -> String { @@ -3219,7 +3249,7 @@ async fn create_run(app: &Router, dot_source: &str) -> String { .method("POST") .uri(api("/runs")) .header("content-type", "application/json") - .body(manifest_body(dot_source)) + .body(intent_body(app, dot_source).await) .unwrap(); let response = app.clone().oneshot(req).await.unwrap(); let body = body_json(response.into_body()).await; @@ -3252,7 +3282,7 @@ url = "http://127.0.0.1:32276" .method("POST") .uri(api("/runs")) .header("content-type", "application/json") - .body(manifest_body(MINIMAL_DOT)) + .body(intent_body(&app, MINIMAL_DOT).await) .unwrap(), ) .await @@ -3335,7 +3365,7 @@ url = "http://127.0.0.1:32276" .method("POST") .uri(api("/runs")) .header("content-type", "application/json") - .body(manifest_body(MINIMAL_DOT)) + .body(intent_body(&app, MINIMAL_DOT).await) .unwrap(), ) .await @@ -3363,7 +3393,7 @@ async fn create_run_without_explicit_title_returns_deterministic_then_updates_ge .unwrap(); let app = crate::test_support::build_test_router(Arc::clone(&state)); - let body = post_run_manifest(&app, minimal_manifest_json(MINIMAL_DOT)).await; + let body = post_run_intent(&app, test_intent(&app, MINIMAL_DOT).await).await; let run_id: RunId = body["id"].as_str().unwrap().parse().unwrap(); assert_eq!(body["title"], "Test"); @@ -3387,10 +3417,10 @@ async fn create_run_with_explicit_title_skips_generated_title_work() { .await .unwrap(); let app = crate::test_support::build_test_router(Arc::clone(&state)); - let mut manifest = minimal_manifest_json(MINIMAL_DOT); - manifest["title"] = json!("Caller title"); + let mut intent = test_intent(&app, MINIMAL_DOT).await; + intent["title"] = json!("Caller title"); - let body = post_run_manifest(&app, manifest).await; + let body = post_run_intent(&app, intent).await; let run_id: RunId = body["id"].as_str().unwrap().parse().unwrap(); // The spawn gate is synchronous in `create_run`, so once the response // returns we know no title task was scheduled. No sleep needed. @@ -3416,17 +3446,18 @@ async fn create_run_without_ready_llm_provider_rejects_implicit_model_selection( let app = crate::test_support::build_test_router(Arc::clone(&state)); let response = app + .clone() .oneshot( Request::builder() .method("POST") .uri(api("/runs")) .header("content-type", "application/json") - .body(Body::from(minimal_manifest_json(MINIMAL_DOT).to_string())) + .body(Body::from(test_intent(&app, MINIMAL_DOT).await.to_string())) .unwrap(), ) .await .unwrap(); - let body = response_json!(response, StatusCode::BAD_REQUEST).await; + let body = response_json!(response, StatusCode::UNPROCESSABLE_ENTITY).await; assert!( body["errors"][0]["detail"] @@ -3460,7 +3491,7 @@ async fn generated_title_failure_leaves_deterministic_title_unchanged() { .unwrap(); let app = crate::test_support::build_test_router(Arc::clone(&state)); - let body = post_run_manifest(&app, minimal_manifest_json(MINIMAL_DOT)).await; + let body = post_run_intent(&app, test_intent(&app, MINIMAL_DOT).await).await; let run_id: RunId = body["id"].as_str().unwrap().parse().unwrap(); wait_for_mock_hits(&title_mock, 1).await; tokio::time::sleep(std::time::Duration::from_millis(25)).await; @@ -3500,7 +3531,7 @@ async fn generated_title_does_not_overwrite_user_title_edit() { .unwrap(); let app = crate::test_support::build_test_router(Arc::clone(&state)); - let body = post_run_manifest(&app, minimal_manifest_json(MINIMAL_DOT)).await; + let body = post_run_intent(&app, test_intent(&app, MINIMAL_DOT).await).await; let run_id: RunId = body["id"].as_str().unwrap().parse().unwrap(); let patch = Request::builder() .method("PATCH") @@ -3528,7 +3559,7 @@ async fn generated_title_does_not_overwrite_user_title_edit() { assert_eq!(title_update_event_count(&state, run_id).await, 1); } -async fn post_run_manifest(app: &Router, manifest: serde_json::Value) -> serde_json::Value { +async fn post_run_intent(app: &Router, manifest: serde_json::Value) -> serde_json::Value { let response = post_run_intent_response(app, manifest).await; response_json!(response, StatusCode::CREATED).await } @@ -3612,7 +3643,7 @@ async fn post_runs_run_intent_derives_workflow_slug_from_immutable_entrypoint() ] { let workflow_version_id = store_workflow_version_with_entrypoint(&state, entrypoint, MINIMAL_DOT, None).await; - let body = post_run_manifest( + let body = post_run_intent( &app, json!({ "workflow_version_id": workflow_version_id, @@ -3676,7 +3707,7 @@ docker = "workflow-owned:latest" ) .await; let submitted_sha = "ABCDEF0123456789ABCDEF0123456789ABCDEF01"; - let body = post_run_manifest( + let body = post_run_intent( &app, json!({ "workflow_version_id": workflow_version_id, @@ -3786,7 +3817,7 @@ async fn post_runs_run_intent_creates_submitted_none_target_without_git_projecti let state = test_app_state(); let app = crate::test_support::build_test_router(Arc::clone(&state)); let workflow_version_id = store_workflow_version(&state, MINIMAL_DOT, None).await; - let body = post_run_manifest( + let body = post_run_intent( &app, json!({ "workflow_version_id": workflow_version_id, @@ -3819,7 +3850,6 @@ async fn post_runs_run_intent_creates_submitted_none_target_without_git_projecti assert_eq!(projection.spec.source_directory, None); assert_eq!(projection.spec.git, None); assert!(projection.spec.settings.run.clone.enabled); - assert_eq!(projection.spec.manifest_blob, None); assert!(projection.spec.definition_blob.is_some()); } @@ -3835,7 +3865,7 @@ async fn post_runs_run_intent_args_true_override_resolved_settings_without_start .build(); let app = crate::test_support::build_test_router(Arc::clone(&state)); let workflow_version_id = store_workflow_version(&state, MINIMAL_DOT, None).await; - let body = post_run_manifest( + let body = post_run_intent( &app, json!({ "workflow_version_id": workflow_version_id, @@ -3927,7 +3957,7 @@ async fn post_runs_run_intent_dry_run_uses_configured_target_provider() { for (state, workflow_toml, target, args) in cases { let app = crate::test_support::build_test_router(Arc::clone(&state)); let workflow_version_id = store_workflow_version(&state, MINIMAL_DOT, workflow_toml).await; - let body = post_run_manifest( + let body = post_run_intent( &app, json!({ "workflow_version_id": workflow_version_id, @@ -4030,7 +4060,7 @@ script = "pwd > setup-working-directory.txt" .await .unwrap(); let workflow_version_id = store_workflow_version(&state, MINIMAL_DOT, None).await; - let body = post_run_manifest( + let body = post_run_intent( &app, json!({ "workflow_version_id": workflow_version_id, @@ -4115,7 +4145,7 @@ preserve = true let app = crate::test_support::build_test_router(Arc::clone(&state)); let workflow_version_id = store_workflow_version(&state, MINIMAL_DOT, None).await; - let explicit_false = post_run_manifest( + let explicit_false = post_run_intent( &app, json!({ "workflow_version_id": workflow_version_id, @@ -4128,7 +4158,7 @@ preserve = true }), ) .await; - let omitted = post_run_manifest( + let omitted = post_run_intent( &app, json!({ "workflow_version_id": workflow_version_id, @@ -4211,7 +4241,7 @@ async fn post_runs_run_intent_canonicalizes_and_persists_a_local_folder_target() ) .await; - let body = post_run_manifest( + let body = post_run_intent( &app, folder_intent(workflow_version_id, submitted.to_string_lossy()), ) @@ -4248,7 +4278,6 @@ async fn post_runs_run_intent_canonicalizes_and_persists_a_local_folder_target() projection.spec.settings.run.environment.provider, SandboxProviderKind::LOCAL ); - assert_eq!(projection.spec.manifest_blob, None); assert!(projection.spec.definition_blob.is_some()); } @@ -4300,8 +4329,8 @@ async fn post_runs_run_intent_accepts_disabled_pull_requests_for_local_environme ) .await; - // `post_run_manifest` asserts the `201 Created` admission outcome. - post_run_manifest(&app, folder_intent(workflow_version_id, target.path())).await; + // `post_run_intent` asserts the `201 Created` admission outcome. + post_run_intent(&app, folder_intent(workflow_version_id, target.path())).await; } #[tokio::test] @@ -4315,7 +4344,7 @@ async fn post_runs_run_intent_accepts_automatic_pull_requests_for_configured_doc ) .await; - let body = post_run_manifest( + let body = post_run_intent( &app, json!({ "workflow_version_id": workflow_version_id, @@ -4374,7 +4403,7 @@ async fn post_runs_run_intent_observes_folder_git_metadata_without_a_remote_call let app = crate::test_support::build_test_router(Arc::clone(&state)); let workflow_version_id = store_workflow_version(&state, MINIMAL_DOT, None).await; - let body = post_run_manifest(&app, folder_intent(workflow_version_id, canonical)).await; + let body = post_run_intent(&app, folder_intent(workflow_version_id, canonical)).await; let run_id = body["id"].as_str().unwrap().parse::().unwrap(); let projection = state .stores @@ -4623,7 +4652,7 @@ async fn post_runs_run_intent_accepts_none_target_with_ready_daytona_environment .build(); let app = crate::test_support::build_test_router(Arc::clone(&state)); let workflow_version_id = store_workflow_version(&state, MINIMAL_DOT, None).await; - let body = post_run_manifest( + let body = post_run_intent( &app, json!({ "workflow_version_id": workflow_version_id, @@ -4656,7 +4685,7 @@ async fn post_runs_run_intent_accepts_none_target_with_ready_daytona_environment } #[tokio::test] -async fn post_runs_run_intent_dispatches_errors_without_changing_legacy_lane() { +async fn post_runs_rejects_legacy_manifests() { let state = test_app_state(); let app = crate::test_support::build_test_router(Arc::clone(&state)); let malformed = app @@ -4691,47 +4720,72 @@ async fn post_runs_run_intent_dispatches_errors_without_changing_legacy_lane() { let mut legacy = minimal_manifest_json(MINIMAL_DOT); legacy["workflow_version_id"] = json!(fabro_types::test_support::test_workflow_version_id()); - let legacy = post_run_manifest(&app, legacy).await; - assert_eq!(legacy["lifecycle"]["status"]["kind"], "submitted"); + let response = post_run_intent_response(&app, legacy).await; + let body = response_json!(response, StatusCode::UNPROCESSABLE_ENTITY).await; + assert_eq!(body["errors"][0]["code"], "run_intent_invalid"); } #[tokio::test] async fn post_runs_attributes_parse_failures_and_rejects_duplicate_keys() { let state = test_app_state(); let app = crate::test_support::build_test_router(Arc::clone(&state)); - let post = |body: String| { - Request::builder() - .method("POST") - .uri(api("/runs")) - .header("content-type", "application/json") - .body(Body::from(body)) + let id = fabro_types::test_support::test_workflow_version_id(); + for (raw, expected_detail) in [ + ("{}".to_string(), "missing field"), + ( + minimal_manifest_json(MINIMAL_DOT).to_string(), + "unknown field", + ), + ( + format!( + r#"{{"workflow_version_id":"{id}","target":{{"kind":"none"}},"cwd":"/tmp","args":{{}}}}"# + ), + "unknown field", + ), + ( + format!( + r#"{{"workflow_version_id":"{id}","workflow_version_id":"{id}","target":{{"kind":"none"}},"args":{{}}}}"# + ), + "duplicate field", + ), + ( + format!( + r#"{{"workflow_version_id":"{id}","target":{{"kind":"none"}},"args":{{"dry_run":true,"dry_run":false}}}}"# + ), + "duplicate field", + ), + ] { + let response = app + .clone() + .oneshot( + Request::builder() + .method("POST") + .uri(api("/runs")) + .header("content-type", "application/json") + .body(Body::from(raw)) + .unwrap(), + ) + .await + .unwrap(); + let body = response_json!(response, StatusCode::UNPROCESSABLE_ENTITY).await; + assert_eq!(body["errors"][0]["code"], "run_intent_invalid"); + assert!( + body["errors"][0]["detail"] + .as_str() + .unwrap() + .contains(expected_detail) + ); + } + assert!(state.runs.lock().unwrap().is_empty()); + assert!( + state + .stores + .run_summaries + .list_identities() + .await .unwrap() - }; - - // A defective manifest keeps the manifest lane's 400 contract even when - // a stray workflow_version_id rides along. - let mut manifest = minimal_manifest_json(MINIMAL_DOT); - manifest["workflow_version_id"] = json!(fabro_types::test_support::test_workflow_version_id()); - manifest["cwd"] = json!(42); - let response = app - .clone() - .oneshot(post(manifest.to_string())) - .await - .unwrap(); - let body = response_json!(response, StatusCode::BAD_REQUEST).await; - let detail = body["errors"][0]["detail"].as_str().unwrap(); - assert!(detail.contains("invalid type: integer `42`"), "{detail}"); - - // Duplicate JSON keys are ambiguous: they must be rejected, not - // collapsed to last-key-wins by a Value round-trip. - let duplicated = format!( - r#"{{"version":1,"cwd":"/tmp","cwd":"/other","target":{{"path":"workflow.fabro"}},"workflows":{{"workflow.fabro":{{"source":{source},"files":{{}}}}}}}}"#, - source = serde_json::to_string(MINIMAL_DOT).unwrap() + .is_empty() ); - let response = app.clone().oneshot(post(duplicated)).await.unwrap(); - let body = response_json!(response, StatusCode::BAD_REQUEST).await; - let detail = body["errors"][0]["detail"].as_str().unwrap(); - assert!(detail.contains("duplicate field"), "{detail}"); } #[tokio::test] @@ -4923,74 +4977,13 @@ enabled = false } #[tokio::test] -async fn post_runs_ignores_removed_run_id_input() { - let app = crate::test_support::build_test_router(test_app_state()); - let submitted_run_id = RunId::new(); - let mut manifest = minimal_manifest_json(MINIMAL_DOT); - manifest["run_id"] = json!(submitted_run_id.to_string()); - - let created = post_run_manifest(&app, manifest).await; - let allocated_run_id = created["id"] - .as_str() - .expect("create response should contain an id") - .parse::() - .expect("create response id should be valid"); - - assert_ne!(allocated_run_id, submitted_run_id); -} - -/// Old clients may still send the removed `target.identifier` and -/// `goal.path` manifest properties. The server must keep accepting such -/// bodies as unknown fields: the workflow is selected by `target.path` -/// and the goal comes from the resolved `goal.text`. -#[tokio::test] -async fn post_runs_accepts_legacy_manifest_metadata_properties() { - let state = test_app_state(); - let app = crate::test_support::build_test_router(Arc::clone(&state)); - let picked_dot = r"digraph PickedFlow { - start [shape=Mdiamond] - exit [shape=Msquare] - start -> exit - }"; - let decoy_dot = r"digraph DecoyFlow { - start [shape=Mdiamond] - exit [shape=Msquare] - start -> exit - }"; - let manifest = serde_json::json!({ - "version": 1, - "cwd": "/tmp", - "target": { - // Legacy display metadata: deliberately names the decoy entry - // to prove selection never reads it. - "identifier": "decoy.fabro", - "path": "picked.fabro", - }, - "goal": { - "type": "file", - "text": "Goal text from the legacy body", - "path": "/tmp/original/goal.md", - }, - "workflows": { - "picked.fabro": { "source": picked_dot, "files": {} }, - "decoy.fabro": { "source": decoy_dot, "files": {} }, - }, - }); - - let created = post_run_manifest(&app, manifest).await; - let run_id = created["id"] - .as_str() - .expect("create response should contain an id") - .parse::() - .expect("create response id should be valid"); - - let run_store = state.stores.runs.open_run_reader(&run_id).await.unwrap(); - let run_state = run_store.state().await.unwrap(); - assert_eq!(run_state.spec.graph.name, "PickedFlow"); - assert_eq!( - run_state.spec.graph.goal(), - "Goal text from the legacy body" - ); +async fn post_runs_rejects_removed_run_id_input() { + let app = test_app_with(); + let mut intent = test_intent(&app, MINIMAL_DOT).await; + intent["run_id"] = json!(RunId::new()); + let response = post_run_intent_response(&app, intent).await; + let body = response_json!(response, StatusCode::UNPROCESSABLE_ENTITY).await; + assert_eq!(body["errors"][0]["code"], "run_intent_invalid"); } #[tokio::test] @@ -5000,10 +4993,10 @@ async fn post_runs_create_regression_keeps_api_behavior_without_automation_metad .vault_entries([(EnvVars::OPENAI_API_KEY, "test-openai-api-key")]) .build(); let app = crate::test_support::build_test_router(Arc::clone(&state)); - let mut manifest = minimal_manifest_json(MINIMAL_DOT); - manifest["title"] = json!("API title"); + let mut intent = test_intent(&app, MINIMAL_DOT).await; + intent["title"] = json!("API title"); - let body = post_run_manifest(&app, manifest).await; + let body = post_run_intent(&app, intent).await; let run_id: RunId = body["id"].as_str().unwrap().parse().unwrap(); assert_eq!(body["title"], "API title"); @@ -5019,109 +5012,6 @@ async fn post_runs_create_regression_keeps_api_behavior_without_automation_metad assert!(summary.automation.is_none()); } -#[tokio::test] -async fn create_run_from_manifest_helper_persists_without_automation_metadata() { - let state = TestAppStateBuilder::new() - .env_lookup(|_| None) - .vault_entries([(EnvVars::OPENAI_API_KEY, "test-openai-api-key")]) - .build(); - let manifest: RunManifest = serde_json::from_value(minimal_manifest_json(MINIMAL_DOT)).unwrap(); - let submitted_manifest_bytes = serde_json::to_vec(&manifest).unwrap(); - let run_id = RunId::new(); - - let response = Box::pin(handler::runs::create_run_from_manifest( - Arc::clone(&state), - handler::runs::CreateRunFromManifestRequest { - manifest, - submitted_manifest_bytes, - explicit_run_id: Some(run_id), - explicit_title_supplied: false, - actor: Principal::System { - system_kind: SystemActorKind::Engine, - }, - headers: HeaderMap::new(), - automation: None, - target: None, - }, - )) - .await; - - let body = response_json!(response, StatusCode::CREATED).await; - assert_eq!(body["id"], run_id.to_string()); - assert!(body["automation"].is_null()); - let summary = state - .stores - .run_summaries - .get(&run_id, Utc::now()) - .await - .unwrap() - .unwrap(); - assert!(summary.automation.is_none()); - let run_store = state.stores.runs.open_run_reader(&run_id).await.unwrap(); - assert!(run_store.state().await.unwrap().spec.target.is_none()); -} - -#[tokio::test] -async fn create_run_from_manifest_helper_persists_automation_metadata_and_exact_target() { - let state = TestAppStateBuilder::new() - .env_lookup(|_| None) - .vault_entries([(EnvVars::OPENAI_API_KEY, "test-openai-api-key")]) - .build(); - let manifest: RunManifest = serde_json::from_value(minimal_manifest_json(MINIMAL_DOT)).unwrap(); - let submitted_manifest_bytes = serde_json::to_vec(&manifest).unwrap(); - let run_id = RunId::new(); - let automation = fabro_types::AutomationRef { - id: "nightly".to_string(), - name: Some("Nightly".to_string()), - trigger_id: Some("schedule".to_string()), - workflow_source: None, - }; - let target = RunTarget::Git(GitRunTarget { - repo: "fabro-sh/fabro".to_string(), - branch: "main".to_string(), - tag: Some("v1.2.3".to_string()), - sha: Some("0123456789abcdef0123456789abcdef01234567".to_string()), - }); - - let response = Box::pin(handler::runs::create_run_from_manifest( - Arc::clone(&state), - handler::runs::CreateRunFromManifestRequest { - manifest, - submitted_manifest_bytes, - explicit_run_id: Some(run_id), - explicit_title_supplied: false, - actor: Principal::System { - system_kind: SystemActorKind::Engine, - }, - headers: HeaderMap::new(), - automation: Some(automation.clone()), - target: Some(target.clone()), - }, - )) - .await; - - let body = response_json!(response, StatusCode::CREATED).await; - assert_eq!(body["automation"]["id"], automation.id); - assert_eq!( - body["automation"]["name"], - automation.name.as_deref().unwrap() - ); - assert_eq!( - body["automation"]["trigger_id"], - automation.trigger_id.as_deref().unwrap() - ); - let summary = state - .stores - .run_summaries - .get(&run_id, Utc::now()) - .await - .unwrap() - .unwrap(); - assert_eq!(summary.automation, Some(automation)); - let run_store = state.stores.runs.open_run_reader(&run_id).await.unwrap(); - assert_eq!(run_store.state().await.unwrap().spec.target, Some(target)); -} - #[tokio::test] async fn create_run_from_intent_helper_persists_automation_version_and_exact_target() { let state = TestAppStateBuilder::new() @@ -5196,310 +5086,7 @@ async fn create_run_from_intent_helper_persists_automation_version_and_exact_tar } #[tokio::test] -async fn create_run_from_manifest_pins_compiled_and_persisted_behavior() { - let state = TestAppStateBuilder::new() - .runtime_settings( - default_test_server_settings(), - manifest_run_defaults_from_toml( - r#" -[run.metadata] -server-label = "server" -layer = "server" -"#, - ), - ) - .env_lookup(|_| None) - .vault_entries([(EnvVars::OPENAI_API_KEY, "test-openai-api-key")]) - .build(); - let run_id = RunId::new(); - let dot = r#"digraph CompilePin { - graph [goal="Graph goal", target="{{ inputs.target }}"] - start [shape=Mdiamond] - work [prompt="Ship {{ inputs.target }}", model="gpt-5.4"] - exit [shape=Msquare] - start -> work -> exit - }"#; - let mut manifest_json = minimal_manifest_json(dot); - manifest_json["title"] = json!(" Pinned create "); - manifest_json["goal"] = json!({ - "type": "value", - "text": "Inline release goal" - }); - manifest_json["args"] = json!({ - "model": "gpt-5.4", - "input": ["target=payments"] - }); - manifest_json["configs"] = json!([{ - "type": "project", - "path": "/tmp/project/.fabro/project.toml", - "source": r#" -_version = 1 - -[project] -name = "payments-project" - -[run.metadata] -project-label = "project" -layer = "project" -"# - }]); - manifest_json["cwd"] = json!("/tmp/project"); - manifest_json["git"] = json!({ - "origin_url": "https://github.com/acme/payments.git", - "branch": "feature/compiler", - "sha": "0123456789abcdef", - "dirty": "clean" - }); - let manifest: RunManifest = serde_json::from_value(manifest_json).unwrap(); - let submitted_manifest_bytes = serde_json::to_vec(&manifest).unwrap(); - let mut headers = HeaderMap::new(); - headers.insert( - header::USER_AGENT, - "fabro-cli/9.8.7".parse().expect("user agent should parse"), - ); - - let response = Box::pin(handler::runs::create_run_from_manifest( - Arc::clone(&state), - handler::runs::CreateRunFromManifestRequest { - manifest, - submitted_manifest_bytes: submitted_manifest_bytes.clone(), - explicit_run_id: Some(run_id), - explicit_title_supplied: true, - actor: Principal::System { - system_kind: SystemActorKind::Engine, - }, - headers, - automation: None, - target: None, - }, - )) - .await; - - let body = response_json!(response, StatusCode::CREATED).await; - assert_eq!(body["id"], run_id.to_string()); - assert_eq!(body["title"], "Pinned create"); - assert_eq!(body["lifecycle"]["status"]["kind"], "submitted"); - - let run_store = state.stores.runs.open_run_reader(&run_id).await.unwrap(); - let events = run_store.list_events().await.unwrap(); - assert_eq!( - events - .iter() - .map(|envelope| envelope.event.event_name()) - .collect::>(), - vec!["run.created", "run.submitted"] - ); - let run_state = run_store.state().await.unwrap(); - let spec = &run_state.spec; - assert!( - spec.target.is_none(), - "legacy manifest GitContext must not become canonical target authority" - ); - assert_eq!(spec.run_id, run_id); - assert_eq!(spec.graph.goal(), "Inline release goal"); - assert_eq!( - spec.graph.attrs.get("target").and_then(AttrValue::as_str), - Some("{{ inputs.target }}") - ); - assert_eq!( - spec.graph.nodes["work"] - .attrs - .get("prompt") - .and_then(AttrValue::as_str), - Some("Ship payments") - ); - assert_eq!( - spec.graph.nodes["work"] - .attrs - .get("model") - .and_then(AttrValue::as_str), - Some("gpt-5.4") - ); - assert_eq!( - spec.graph.nodes["work"] - .attrs - .get("provider") - .and_then(AttrValue::as_str), - Some("openai") - ); - assert_eq!(spec.settings.run.model.name.as_deref(), Some("gpt-5.4")); - assert_eq!(spec.settings.run.model.provider.as_deref(), Some("openai")); - assert_eq!( - spec.settings.run.inputs.get("target"), - Some(&toml::Value::String("payments".to_string())) - ); - assert_eq!( - spec.settings.project.name.as_deref(), - Some("payments-project") - ); - assert_eq!( - spec.labels.get("project-label").map(String::as_str), - Some("project") - ); - assert_eq!( - spec.labels.get("layer").map(String::as_str), - Some("project") - ); - assert_eq!( - spec.git.as_ref().map(|git| git.origin_url.as_str()), - Some("https://github.com/acme/payments.git") - ); - - let created = events[0].event.to_value().unwrap(); - assert_eq!(created["properties"]["title"], "Pinned create"); - assert_eq!(created["properties"]["labels"]["project-label"], "project"); - assert_eq!( - created["properties"]["provenance"]["client"]["user_agent"], - "fabro-cli/9.8.7" - ); - assert_eq!( - created["properties"]["provenance"]["subject"]["kind"], - "system" - ); - let manifest_blob = created["properties"]["manifest_blob"] - .as_str() - .expect("run.created should carry the submitted source blob") - .parse::() - .unwrap(); - let persisted_manifest = run_store - .read_blob(&manifest_blob) - .await - .unwrap() - .expect("submitted source blob should exist"); - assert_eq!(persisted_manifest.as_ref(), submitted_manifest_bytes); -} - -#[tokio::test] -async fn create_run_from_manifest_pins_compiler_http_error_mappings() { - let cases = [ - ( - { - let mut manifest = minimal_manifest_json(MINIMAL_DOT); - manifest["version"] = json!(2); - manifest - }, - "unsupported manifest version 2", - ), - ( - minimal_manifest_json( - r#"digraph Test { - graph [goal="Test"] - start [shape=Mdiamond] - work [prompt="Use {{ vars.MISSING }}"] - exit [shape=Msquare] - start -> work -> exit - }"#, - ), - "Validation failed", - ), - ( - { - let mut manifest = minimal_manifest_json( - r#"digraph Test { - graph [goal="Test"] - start [shape=Mdiamond] - work [prompt="Do work", model="gpt-5.4", provider="missing-provider"] - exit [shape=Msquare] - start -> work -> exit - }"#, - ); - manifest["args"] = json!({ - "model": "gpt-5.4", - "provider": "missing-provider" - }); - manifest - }, - "Model selection failed: unknown model provider 'missing-provider'", - ), - ]; - - for (manifest_json, expected_detail) in cases { - let state = TestAppStateBuilder::new() - .env_lookup(|_| None) - .vault_entries([(EnvVars::OPENAI_API_KEY, "test-openai-api-key")]) - .build(); - let manifest: RunManifest = serde_json::from_value(manifest_json).unwrap(); - let submitted_manifest_bytes = serde_json::to_vec(&manifest).unwrap(); - - let response = Box::pin(handler::runs::create_run_from_manifest( - state, - handler::runs::CreateRunFromManifestRequest { - manifest, - submitted_manifest_bytes, - explicit_run_id: Some(RunId::new()), - explicit_title_supplied: true, - actor: Principal::System { - system_kind: SystemActorKind::Engine, - }, - headers: HeaderMap::new(), - automation: None, - target: None, - }, - )) - .await; - - let body = response_json!(response, StatusCode::BAD_REQUEST).await; - assert_eq!(body["errors"][0]["detail"], expected_detail); - } -} - -#[tokio::test] -async fn create_run_from_manifest_preserves_competing_preparation_error_precedence() { - let mut workflow_before_title = minimal_manifest_json(MINIMAL_DOT); - workflow_before_title["workflows"]["workflow.fabro"]["config"] = json!({ - "path": "workflow.toml", - "source": "_version = 1\n[run.unknown]\nvalue = true\n", - }); - workflow_before_title["title"] = json!(" "); - - let mut project_parse_before_later_path = minimal_manifest_json(MINIMAL_DOT); - project_parse_before_later_path["configs"] = json!([ - { - "type": "project", - "path": "/tmp/.fabro/project.toml", - "source": "_version = 1\n[run.unknown]\nvalue = true\n", - }, - { - "type": "project", - "source": "_version = 1\n", - }, - ]); - - for (manifest_json, expected_detail) in [ - (workflow_before_title, "Failed to parse run config TOML"), - ( - project_parse_before_later_path, - "Failed to parse run config TOML", - ), - ] { - let state = TestAppStateBuilder::new().build(); - let manifest: RunManifest = serde_json::from_value(manifest_json).unwrap(); - let submitted_manifest_bytes = serde_json::to_vec(&manifest).unwrap(); - - let response = Box::pin(handler::runs::create_run_from_manifest( - state, - handler::runs::CreateRunFromManifestRequest { - manifest, - submitted_manifest_bytes, - explicit_run_id: None, - explicit_title_supplied: true, - actor: Principal::System { - system_kind: SystemActorKind::Engine, - }, - headers: HeaderMap::new(), - automation: None, - target: None, - }, - )) - .await; - - let body = response_json!(response, StatusCode::BAD_REQUEST).await; - assert_eq!(body["errors"][0]["detail"], expected_detail); - } -} - -#[tokio::test] -async fn create_run_from_manifest_resolves_generated_id_after_variable_snapshot() { +async fn create_run_from_intent_snapshots_variables_before_allocating_id() { let state = TestAppStateBuilder::new() .env_lookup(|_| None) .vault_entries([(EnvVars::OPENAI_API_KEY, "test-openai-api-key")]) @@ -5509,41 +5096,56 @@ async fn create_run_from_manifest_resolves_generated_id_after_variable_snapshot( .variables .set("OWNER", "payments", None) .await - .expect("test variable should persist"); - let manifest: RunManifest = serde_json::from_value(minimal_manifest_json( + .unwrap(); + let version = store_workflow_version( + &state, r#"digraph Test { - graph [goal="Test"] - start [shape=Mdiamond] - work [prompt="Ship {{ vars.OWNER }}"] - exit [shape=Msquare] - start -> work -> exit - }"#, - )) + graph [goal="Test"] + start [shape=Mdiamond] + work [prompt="Ship {{ vars.OWNER }}"] + exit [shape=Msquare] + start -> work -> exit + }"#, + None, + ) + .await; + let intent = serde_json::from_value( + json!({"workflow_version_id": version, "target": {"kind":"none"}, "args":{}}), + ) .unwrap(); - let submitted_manifest_bytes = serde_json::to_vec(&manifest).unwrap(); - - let response = Box::pin(handler::runs::create_run_from_manifest( - state, - handler::runs::CreateRunFromManifestRequest { - manifest, - submitted_manifest_bytes, + let response = Box::pin(handler::runs::create_run_from_intent( + Arc::clone(&state), + handler::runs::CreateRunFromIntentRequest { + intent, explicit_run_id: None, - explicit_title_supplied: false, actor: Principal::System { system_kind: SystemActorKind::Engine, }, headers: HeaderMap::new(), automation: None, - target: None, }, )) .await; - let body = response_json!(response, StatusCode::CREATED).await; let run_id = body["id"].as_str().unwrap().parse::().unwrap(); - // RunId is a ULID whose timestamp only has millisecond precision, so - // truncate the variable timestamp to milliseconds before comparing. - assert!(run_id.created_at() >= variable.updated_at.trunc_subsecs(3)); + assert!(run_id.created_at().timestamp_millis() >= variable.updated_at.timestamp_millis()); + let projection = state + .stores + .runs + .open_run_reader(&run_id) + .await + .unwrap() + .state() + .await + .unwrap(); + assert_eq!( + projection.spec.graph.nodes["work"] + .attrs + .get("prompt") + .and_then(AttrValue::as_str), + Some("Ship payments") + ); + assert_eq!(projection.status, RunStatus::Submitted); } #[tokio::test] @@ -5798,15 +5400,11 @@ async fn validate_endpoint_returns_template_source_coordinates() { } async fn create_run_for_target(app: &Router, target_path: &str, dot_source: &str) -> String { - let req = Request::builder() - .method("POST") - .uri(api("/runs")) - .header("content-type", "application/json") - .body(manifest_body_for(target_path, dot_source)) - .unwrap(); - let response = app.clone().oneshot(req).await.unwrap(); - let body = body_json(response.into_body()).await; - body["id"].as_str().unwrap().to_string() + let intent = test_intent_with_bearer(app, target_path, dot_source, None, None).await; + post_run_intent(app, intent).await["id"] + .as_str() + .unwrap() + .to_string() } async fn create_run_for_target_with_workflow_name( @@ -5815,20 +5413,12 @@ async fn create_run_for_target_with_workflow_name( dot_source: &str, workflow_name: &str, ) -> String { - let mut manifest = manifest_json(target_path, dot_source); - manifest["workflows"][target_path]["config"] = serde_json::json!({ - "path": "workflow.toml", - "source": format!("_version = 1\n\n[workflow]\nname = {workflow_name:?}\n"), - }); - let req = Request::builder() - .method("POST") - .uri(api("/runs")) - .header("content-type", "application/json") - .body(Body::from(serde_json::to_string(&manifest).unwrap())) - .unwrap(); - let response = app.clone().oneshot(req).await.unwrap(); - let body = body_json(response.into_body()).await; - body["id"].as_str().unwrap().to_string() + let config = format!("_version = 1\n[workflow]\nname = {workflow_name:?}\n"); + let intent = test_intent_with_bearer(app, target_path, dot_source, Some(&config), None).await; + post_run_intent(app, intent).await["id"] + .as_str() + .unwrap() + .to_string() } fn named_workflow_dot(name: &str, goal: &str) -> String { @@ -6312,7 +5902,6 @@ async fn append_default_run_created(run_store: &fabro_store::RunDatabase, run_id target: None, automation: None, provenance: test_support::test_run_provenance(), - manifest_blob: None, spec_blob: None, git: None, fork_source_ref: None, @@ -6366,7 +5955,6 @@ async fn create_slack_notification_run( target: None, automation: None, provenance: test_support::test_run_provenance(), - manifest_blob: None, spec_blob: None, git: None, fork_source_ref: None, @@ -7444,7 +7032,6 @@ async fn list_run_stages_distinguishes_visits() { target: None, automation: None, provenance: test_support::test_run_provenance(), - manifest_blob: None, spec_blob: None, git: None, fork_source_ref: None, @@ -7584,7 +7171,6 @@ async fn list_run_stages_exposes_execution_identity_for_resumed_stage() { target: None, automation: None, provenance: test_support::test_run_provenance(), - manifest_blob: None, spec_blob: None, git: None, fork_source_ref: None, @@ -8778,7 +8364,6 @@ async fn create_completed_run_ready_for_pull_request( git: git.clone(), labels: HashMap::new(), provenance: test_support::test_run_provenance(), - manifest_blob: None, definition_blob: None, spec_blob: None, fork_source_ref: None, @@ -8798,7 +8383,6 @@ async fn create_completed_run_ready_for_pull_request( target: run_spec.target.clone(), automation: None, provenance: run_spec.provenance.clone(), - manifest_blob: None, spec_blob: None, git, fork_source_ref: None, @@ -10018,14 +9602,14 @@ async fn static_favicon_is_served() { } #[tokio::test] -async fn post_runs_starts_run_and_returns_id() { +async fn post_runs_creates_submitted_run_and_returns_id() { let app = test_app_with(); let req = Request::builder() .method("POST") .uri(api("/runs")) .header("content-type", "application/json") - .body(manifest_body(MINIMAL_DOT)) + .body(intent_body(&app, MINIMAL_DOT).await) .unwrap(); let response = app.oneshot(req).await.unwrap(); @@ -10035,18 +9619,11 @@ async fn post_runs_starts_run_and_returns_id() { } #[tokio::test] -async fn post_runs_invalid_dot_returns_bad_request() { +async fn workflow_registration_rejects_invalid_dot_before_run_creation() { let app = test_app_with(); - - let req = Request::builder() - .method("POST") - .uri(api("/runs")) - .header("content-type", "application/json") - .body(manifest_body("not a graph")) - .unwrap(); - - let response = app.oneshot(req).await.unwrap(); - assert_status!(response, StatusCode::BAD_REQUEST).await; + let response = app.oneshot(Request::builder().method("POST").uri(api("/workflow-versions")).header("content-type", "application/json").body(Body::from(json!({"entrypoint":"workflow.fabro","files":{"workflow.fabro":"not a graph"},"workflow_dependencies":{}}).to_string())).unwrap()).await.unwrap(); + let body = response_json!(response, StatusCode::UNPROCESSABLE_ENTITY).await; + assert_eq!(body["errors"][0]["code"], "workflow_version_invalid"); } #[tokio::test(flavor = "multi_thread", worker_threads = 2)] @@ -10295,7 +9872,7 @@ async fn get_questions_returns_empty_list() { .method("POST") .uri(api("/runs")) .header("content-type", "application/json") - .body(manifest_body(MINIMAL_DOT)) + .body(intent_body(&app, MINIMAL_DOT).await) .unwrap(); let response = app.clone().oneshot(req).await.unwrap(); @@ -10527,7 +10104,7 @@ async fn get_run_state_returns_projection() { .method("POST") .uri(api("/runs")) .header("content-type", "application/json") - .body(manifest_body(MINIMAL_DOT)) + .body(intent_body(&app, MINIMAL_DOT).await) .unwrap(); let response = app.clone().oneshot(req).await.unwrap(); @@ -12485,7 +12062,7 @@ async fn get_run_state_includes_provenance_from_user_agent() { .uri(api("/runs")) .header("content-type", "application/json") .header("user-agent", "fabro-cli/1.2.3") - .body(manifest_body(MINIMAL_DOT)) + .body(intent_body(&app, MINIMAL_DOT).await) .unwrap(); let response = app.clone().oneshot(req).await.unwrap(); @@ -12574,7 +12151,17 @@ async fn dev_token_web_login_authorizes_cookie_backed_api_requests() { .uri(api("/runs")) .header(header::CONTENT_TYPE, "application/json") .header(header::COOKIE, &session_cookie) - .body(manifest_body(MINIMAL_DOT)) + .body(Body::from( + test_intent_with_bearer( + &app, + "workflow.fabro", + MINIMAL_DOT, + None, + Some(DEV_TOKEN), + ) + .await + .to_string(), + )) .unwrap(), ) .await @@ -12602,16 +12189,16 @@ async fn dev_token_web_login_authorizes_cookie_backed_api_requests() { } #[tokio::test] -async fn create_run_persists_manifest_and_definition_blobs_without_bundle_file() { +async fn create_run_persists_definition_and_spec_blobs_without_manifest() { let state = test_app_state(); let app = crate::test_support::build_test_router(Arc::clone(&state)); - let raw_manifest = serde_json::to_string_pretty(&minimal_manifest_json(MINIMAL_DOT)).unwrap(); + let raw_intent = serde_json::to_string_pretty(&test_intent(&app, MINIMAL_DOT).await).unwrap(); let req = Request::builder() .method("POST") .uri(api("/runs")) .header("content-type", "application/json") - .body(Body::from(raw_manifest.clone())) + .body(Body::from(raw_intent.clone())) .unwrap(); let response = app.clone().oneshot(req).await.unwrap(); @@ -12622,24 +12209,14 @@ async fn create_run_persists_manifest_and_definition_blobs_without_bundle_file() let events = run_store.list_events().await.unwrap(); let created = events[0].event.to_value().unwrap(); let submitted = events[1].event.to_value().unwrap(); - let manifest_blob = created["properties"]["manifest_blob"] - .as_str() - .expect("run.created should carry manifest_blob") - .parse::() - .unwrap(); + assert!(created["properties"].get("manifest_blob").is_none()); + assert!(created["properties"]["spec_blob"].is_string()); let definition_blob = submitted["properties"]["definition_blob"] .as_str() .expect("run.submitted should carry definition_blob") .parse::() .unwrap(); - let submitted_manifest_bytes = run_store - .read_blob(&manifest_blob) - .await - .unwrap() - .expect("submitted manifest blob should exist"); - assert_eq!(submitted_manifest_bytes.as_ref(), raw_manifest.as_bytes()); - let accepted_definition_bytes = run_store .read_blob(&definition_blob) .await @@ -12664,7 +12241,7 @@ async fn list_run_events_returns_paginated_json() { .method("POST") .uri(api("/runs")) .header("content-type", "application/json") - .body(manifest_body(MINIMAL_DOT)) + .body(intent_body(&app, MINIMAL_DOT).await) .unwrap(); let response = app.clone().oneshot(req).await.unwrap(); @@ -12783,7 +12360,7 @@ async fn append_run_event_rejects_run_id_mismatch() { .method("POST") .uri(api("/runs")) .header("content-type", "application/json") - .body(manifest_body(MINIMAL_DOT)) + .body(intent_body(&app, MINIMAL_DOT).await) .unwrap(); let response = app.clone().oneshot(req).await.unwrap(); @@ -12938,7 +12515,7 @@ async fn get_checkpoint_returns_null_initially() { .method("POST") .uri(api("/runs")) .header("content-type", "application/json") - .body(manifest_body(MINIMAL_DOT)) + .body(intent_body(&app, MINIMAL_DOT).await) .unwrap(); let response = app.clone().oneshot(req).await.unwrap(); @@ -12965,7 +12542,7 @@ async fn write_and_read_run_blob_accepts_uppercase_hash() { .method("POST") .uri(api("/runs")) .header("content-type", "application/json") - .body(manifest_body(MINIMAL_DOT)) + .body(intent_body(&app, MINIMAL_DOT).await) .unwrap(); let response = app.clone().oneshot(req).await.unwrap(); @@ -13303,30 +12880,14 @@ async fn create_run_keeps_missing_project_and_workflow_names_absent() { let state = test_app_state(); let app = crate::test_support::build_test_router(Arc::clone(&state)); - let manifest = serde_json::json!({ - "version": 1, - "cwd": "/tmp/project", - "target": { - "path": "workflow.fabro", - }, - "configs": [ - { - "path": "/tmp/project/.fabro/project.toml", - "source": "_version = 1\n", - "type": "project", - } - ], - "workflows": { - "workflow.fabro": { - "source": "digraph Demo { start [shape=Mdiamond] exit [shape=Msquare] start -> exit }", - "config": { - "path": "workflow.toml", - "source": "_version = 1\n", - }, - "files": {}, - } - }, - }); + let intent = test_intent_with_bearer( + &app, + "workflow.fabro", + "digraph Demo { start [shape=Mdiamond] exit [shape=Msquare] start -> exit }", + Some("_version = 1\n"), + None, + ) + .await; let response = app .clone() @@ -13335,7 +12896,7 @@ async fn create_run_keeps_missing_project_and_workflow_names_absent() { .method("POST") .uri(api("/runs")) .header("content-type", "application/json") - .body(Body::from(manifest.to_string())) + .body(Body::from(intent.to_string())) .unwrap(), ) .await @@ -14160,7 +13721,7 @@ async fn create_run_returns_submitted() { .method("POST") .uri(api("/runs")) .header("content-type", "application/json") - .body(manifest_body(MINIMAL_DOT)) + .body(intent_body(&app, MINIMAL_DOT).await) .unwrap(); let response = app.oneshot(req).await.unwrap(); @@ -14173,14 +13734,14 @@ async fn create_run_returns_submitted() { async fn create_run_accepts_explicit_title() { let state = test_app_state(); let app = crate::test_support::build_test_router(Arc::clone(&state)); - let mut manifest = minimal_manifest_json(MINIMAL_DOT); - manifest["title"] = json!(" Explicit server title "); + let mut intent = test_intent(&app, MINIMAL_DOT).await; + intent["title"] = json!(" Explicit server title "); let req = Request::builder() .method("POST") .uri(api("/runs")) .header("content-type", "application/json") - .body(Body::from(serde_json::to_string(&manifest).unwrap())) + .body(Body::from(serde_json::to_string(&intent).unwrap())) .unwrap(); let response = app.clone().oneshot(req).await.unwrap(); @@ -14210,17 +13771,17 @@ async fn create_run_rejects_invalid_titles() { "First\nSecond".to_string(), "x".repeat(101), ] { - let mut manifest = minimal_manifest_json(MINIMAL_DOT); - manifest["title"] = json!(title); + let mut intent = test_intent(&app, MINIMAL_DOT).await; + intent["title"] = json!(title); let req = Request::builder() .method("POST") .uri(api("/runs")) .header("content-type", "application/json") - .body(Body::from(serde_json::to_string(&manifest).unwrap())) + .body(Body::from(serde_json::to_string(&intent).unwrap())) .unwrap(); let response = app.clone().oneshot(req).await.unwrap(); - assert_status!(response, StatusCode::BAD_REQUEST).await; + assert_status!(response, StatusCode::UNPROCESSABLE_ENTITY).await; } } @@ -14234,7 +13795,7 @@ async fn start_run_transitions_to_runnable() { .method("POST") .uri(api("/runs")) .header("content-type", "application/json") - .body(manifest_body(MINIMAL_DOT)) + .body(intent_body(&app, MINIMAL_DOT).await) .unwrap(); let response = app.clone().oneshot(req).await.unwrap(); let body = body_json(response.into_body()).await; @@ -14270,8 +13831,9 @@ async fn worker_started_child_run_requires_approval_before_becoming_runnable() { let user_jwt = issue_test_user_jwt(); let parent_run_id = create_run_with_bearer(&app, &user_jwt).await; let worker_token = issue_test_run_tools_worker_token(&parent_run_id); - let mut child_manifest = minimal_manifest_json(MINIMAL_DOT); - child_manifest["parent_id"] = json!(parent_run_id.to_string()); + let mut child_intent = + test_intent_with_bearer(&app, "workflow.fabro", MINIMAL_DOT, None, Some(&user_jwt)).await; + child_intent["parent_id"] = json!(parent_run_id.to_string()); let response = app .clone() @@ -14279,7 +13841,7 @@ async fn worker_started_child_run_requires_approval_before_becoming_runnable() { Method::POST, "/runs", &worker_token, - &child_manifest, + &child_intent, )) .await .unwrap(); @@ -14371,8 +13933,9 @@ async fn denying_pending_child_run_fails_with_approval_denied() { let user_jwt = issue_test_user_jwt(); let parent_run_id = create_run_with_bearer(&app, &user_jwt).await; let worker_token = issue_test_run_tools_worker_token(&parent_run_id); - let mut child_manifest = minimal_manifest_json(MINIMAL_DOT); - child_manifest["parent_id"] = json!(parent_run_id.to_string()); + let mut child_intent = + test_intent_with_bearer(&app, "workflow.fabro", MINIMAL_DOT, None, Some(&user_jwt)).await; + child_intent["parent_id"] = json!(parent_run_id.to_string()); let response = app .clone() @@ -14380,7 +13943,7 @@ async fn denying_pending_child_run_fails_with_approval_denied() { Method::POST, "/runs", &worker_token, - &child_manifest, + &child_intent, )) .await .unwrap(); @@ -14574,7 +14137,7 @@ async fn start_run_conflict_when_not_submitted() { .method("POST") .uri(api("/runs")) .header("content-type", "application/json") - .body(manifest_body(MINIMAL_DOT)) + .body(intent_body(&app, MINIMAL_DOT).await) .unwrap(); let response = app.clone().oneshot(req).await.unwrap(); let body = body_json(response.into_body()).await; @@ -15498,20 +15061,7 @@ async fn get_graph_returns_svg() { .uri(api("/runs")) .header("content-type", "application/json") .body(Body::from( - serde_json::to_string(&serde_json::json!({ - "version": 1, - "cwd": "/tmp", - "target": { - "path": "workflow.fabro", - }, - "workflows": { - "workflow.fabro": { - "source": MINIMAL_DOT, - "files": {}, - }, - }, - })) - .unwrap(), + serde_json::to_string(&test_intent(&app, MINIMAL_DOT).await).unwrap(), )) .unwrap(); @@ -15557,20 +15107,7 @@ async fn get_graph_source_returns_dot() { .uri(api("/runs")) .header("content-type", "application/json") .body(Body::from( - serde_json::to_string(&serde_json::json!({ - "version": 1, - "cwd": "/tmp", - "target": { - "path": "workflow.fabro", - }, - "workflows": { - "workflow.fabro": { - "source": MINIMAL_DOT, - "files": {}, - }, - }, - })) - .unwrap(), + serde_json::to_string(&test_intent(&app, MINIMAL_DOT).await).unwrap(), )) .unwrap(); @@ -15802,7 +15339,7 @@ async fn list_runs_returns_started_run() { .method("POST") .uri(api("/runs")) .header("content-type", "application/json") - .body(manifest_body(MINIMAL_DOT)) + .body(intent_body(&app, MINIMAL_DOT).await) .unwrap(); let response = app.clone().oneshot(req).await.unwrap(); @@ -16008,7 +15545,6 @@ async fn create_preserved_local_sandbox_run(state: &Arc, run_id: RunId target: None, automation: None, provenance: test_support::test_run_provenance(), - manifest_blob: None, spec_blob: None, git: None, fork_source_ref: None, @@ -16627,7 +16163,7 @@ async fn delete_run_removes_durable_run() { .method("POST") .uri(api("/runs")) .header("content-type", "application/json") - .body(manifest_body(MINIMAL_DOT)) + .body(intent_body(&app, MINIMAL_DOT).await) .unwrap(); let response = app.clone().oneshot(req).await.unwrap(); @@ -16760,7 +16296,6 @@ async fn delete_run_retry_after_missing_provider_resource_removes_metadata() { target: None, automation: None, provenance: test_support::test_run_provenance(), - manifest_blob: None, spec_blob: None, git: None, fork_source_ref: None, @@ -16835,7 +16370,7 @@ async fn delete_active_run_requires_force() { .method("POST") .uri(api("/runs")) .header("content-type", "application/json") - .body(manifest_body(MINIMAL_DOT)) + .body(intent_body(&app, MINIMAL_DOT).await) .unwrap(); let response = app.clone().oneshot(req).await.unwrap(); @@ -16876,7 +16411,7 @@ async fn delete_active_run_force_succeeds() { .method("POST") .uri(api("/runs")) .header("content-type", "application/json") - .body(manifest_body(MINIMAL_DOT)) + .body(intent_body(&app, MINIMAL_DOT).await) .unwrap(); let response = app.clone().oneshot(req).await.unwrap(); @@ -17149,7 +16684,7 @@ async fn post_runs_returns_submitted_status() { .method("POST") .uri(api("/runs")) .header("content-type", "application/json") - .body(manifest_body(MINIMAL_DOT)) + .body(intent_body(&app, MINIMAL_DOT).await) .unwrap(); let response = app.clone().oneshot(req).await.unwrap(); @@ -17236,7 +16771,7 @@ level = "debug" .method("POST") .uri(api("/runs")) .header("content-type", "application/json") - .body(manifest_body(MINIMAL_DOT)) + .body(intent_body(&app, MINIMAL_DOT).await) .unwrap(); let response = app.oneshot(req).await.unwrap(); @@ -18329,7 +17864,24 @@ id = "local" ); let app = crate::test_support::build_test_router(Arc::clone(&state)); - let run_id_str = create_and_start_run(&app, MINIMAL_DOT).await; + let workspace = tempfile::tempdir().unwrap(); + let mut intent = test_intent(&app, MINIMAL_DOT).await; + intent["target"] = json!({"kind": "folder", "path": workspace.path()}); + intent["environment_id"] = json!("local"); + let run_id_str = post_run_intent(&app, intent).await["id"] + .as_str() + .unwrap() + .to_string(); + app.clone() + .oneshot( + Request::builder() + .method("POST") + .uri(api(&format!("/runs/{run_id_str}/start"))) + .body(Body::empty()) + .unwrap(), + ) + .await + .unwrap(); let run_id = run_id_str.parse::().unwrap(); let runner = tokio::spawn( @@ -18597,7 +18149,7 @@ async fn submit_answer_to_unstarted_run_returns_conflict() { .method("POST") .uri(api("/runs")) .header("content-type", "application/json") - .body(manifest_body(MINIMAL_DOT)) + .body(intent_body(&app, MINIMAL_DOT).await) .unwrap(); let response = app.clone().oneshot(req).await.unwrap(); diff --git a/lib/apps/fabro-server/src/test_support.rs b/lib/apps/fabro-server/src/test_support.rs index 0f6b0e2d5..d3da04a44 100644 --- a/lib/apps/fabro-server/src/test_support.rs +++ b/lib/apps/fabro-server/src/test_support.rs @@ -6,10 +6,11 @@ use std::sync::{Arc, OnceLock}; use std::time::Duration; use anyhow::Context as _; +use axum::body::{self, Body}; use axum::extract::Request; #[cfg(test)] use axum::extract::State as AxumState; -use axum::http::{HeaderValue, header}; +use axum::http::{HeaderValue, Request as HttpRequest, StatusCode, header}; use axum::middleware::Next; use axum::response::Response; use axum::{Router, middleware}; @@ -23,7 +24,10 @@ use fabro_sandbox::SandboxInventory; use fabro_static::EnvVars; use fabro_store::{ArtifactStore, Database, test_support as store_test_support}; use fabro_types::settings::ServerAuthMethod; -use fabro_types::{AuthMethod, IdpIdentity, SandboxProviderKind, ServerSettings}; +use fabro_types::{ + AuthMethod, IdpIdentity, SandboxProviderKind, ServerSettings, WorkflowVersion, + WorkflowVersionId, +}; use fabro_vault::{SecretType, Vault}; use fabro_workflow::handler::HandlerRegistry; use lithos_llm::catalog::ProviderId; @@ -812,6 +816,48 @@ pub(crate) async fn capture_auth_context( response } +/// Register fixture content through the same HTTP boundary as run producers. +pub async fn test_register_workflow_version( + app: &Router, + version: &WorkflowVersion, + bearer: Option<&str>, +) -> WorkflowVersionId { + use tower::ServiceExt as _; + + let mut request = HttpRequest::builder() + .method("POST") + .uri("/api/v1/workflow-versions") + .header(header::CONTENT_TYPE, "application/json"); + if let Some(bearer) = bearer { + request = request.header(header::AUTHORIZATION, format!("Bearer {bearer}")); + } + let response = app + .clone() + .oneshot( + request + .body(Body::from( + serde_json::to_vec(version).expect("fixture version should serialize"), + )) + .expect("version registration request should build"), + ) + .await + .expect("version registration should route"); + let status = response.status(); + let bytes = body::to_bytes(response.into_body(), usize::MAX) + .await + .expect("version registration response body should be readable"); + assert_eq!( + status, + StatusCode::CREATED, + "{}", + String::from_utf8_lossy(&bytes) + ); + let body: serde_json::Value = + serde_json::from_slice(&bytes).expect("version registration response should be JSON"); + serde_json::from_value(body["workflow_version_id"].clone()) + .expect("version registration response should contain a valid ID") +} + #[cfg(test)] mod tests { use fabro_types::settings::ObjectStoreSettings; diff --git a/lib/apps/fabro-server/tests/it/api/events.rs b/lib/apps/fabro-server/tests/it/api/events.rs index 76a78d5cc..9419e4768 100644 --- a/lib/apps/fabro-server/tests/it/api/events.rs +++ b/lib/apps/fabro-server/tests/it/api/events.rs @@ -10,7 +10,7 @@ use object_store::memory::InMemory; use tokio::sync::Barrier; use tower::ServiceExt; -use crate::helpers::{MINIMAL_DOT, api, minimal_manifest_json, response_json, test_settings}; +use crate::helpers::{MINIMAL_DOT, api, minimal_intent_json, response_json, test_settings}; fn app_with_store( object_store: Arc, @@ -36,14 +36,15 @@ fn app_with_store( fabro_server::test_support::build_test_router(state) } -async fn create_run(app: &axum::Router) -> String { +async fn create_run(app: &axum::Router) -> (String, tempfile::TempDir) { + let workspace = tempfile::tempdir().expect("run target workspace should be created"); let request = Request::builder() .method("POST") .uri(api("/runs")) .header("content-type", "application/json") .body(Body::from( - serde_json::to_string(&minimal_manifest_json(MINIMAL_DOT)) - .expect("manifest should serialize"), + serde_json::to_string(&minimal_intent_json(app, MINIMAL_DOT, workspace.path()).await) + .expect("intent should serialize"), )) .expect("create-run request should build"); let body = response_json( @@ -52,10 +53,11 @@ async fn create_run(app: &axum::Router) -> String { "POST /api/v1/runs", ) .await; - body["id"] + let run_id = body["id"] .as_str() .expect("create-run response should include an id") - .to_string() + .to_string(); + (run_id, workspace) } fn append_stage_started_request(run_id: &str, index: usize) -> Request { @@ -116,7 +118,7 @@ async fn concurrent_event_appends_after_restart_keep_projection_cache_contiguous Arc::clone(&blobs), Arc::clone(&run_summaries), ); - let run_id = create_run(&first_app).await; + let (run_id, _run_id_workspace) = create_run(&first_app).await; tokio::time::sleep(Duration::from_millis(25)).await; diff --git a/lib/apps/fabro-server/tests/it/api/run_files.rs b/lib/apps/fabro-server/tests/it/api/run_files.rs index 4cc2cd794..eb09fcf94 100644 --- a/lib/apps/fabro-server/tests/it/api/run_files.rs +++ b/lib/apps/fabro-server/tests/it/api/run_files.rs @@ -21,7 +21,7 @@ use object_store::memory::InMemory as MemoryObjectStore; use tower::ServiceExt; use crate::helpers::{ - MINIMAL_DOT, api, minimal_manifest_json, response_json, response_status, test_app_state, + MINIMAL_DOT, api, minimal_intent_json, response_json, response_status, test_app_state, test_settings, }; @@ -70,7 +70,6 @@ async fn append_completed_run_with_final_patch( target: None, automation: None, provenance: test_support::test_run_provenance(), - manifest_blob: None, spec_blob: None, git: None, fork_source_ref: None, @@ -272,16 +271,17 @@ async fn invalid_scope_returns_400() { #[tokio::test] async fn submitted_run_without_sandbox_returns_empty_envelope() { + let workspace = tempfile::tempdir().unwrap(); // A run that has been created but not started has no base_sha or // run sandbox, so the handler returns an empty envelope. The UI // maps that to R4(a). let app = fabro_server::test_support::build_test_router(test_app_state()); - let manifest = minimal_manifest_json(MINIMAL_DOT); + let intent = minimal_intent_json(&app, MINIMAL_DOT, workspace.path()).await; let create_req = Request::builder() .method("POST") .uri(api("/runs")) .header("content-type", "application/json") - .body(Body::from(serde_json::to_string(&manifest).unwrap())) + .body(Body::from(serde_json::to_string(&intent).unwrap())) .unwrap(); let create_resp = app.clone().oneshot(create_req).await.unwrap(); let create_body = response_json(create_resp, StatusCode::CREATED, "POST /api/v1/runs").await; diff --git a/lib/apps/fabro-server/tests/it/api/runs.rs b/lib/apps/fabro-server/tests/it/api/runs.rs index 6820e9869..c4318acda 100644 --- a/lib/apps/fabro-server/tests/it/api/runs.rs +++ b/lib/apps/fabro-server/tests/it/api/runs.rs @@ -5,22 +5,17 @@ use fabro_types::SandboxProviderKind; use tower::ServiceExt; use crate::helpers::{ - MINIMAL_DOT, api, body_json, minimal_manifest_json, response_json, response_status, + MINIMAL_DOT, api, body_json, minimal_intent_json, response_json, response_status, settings_from_toml, test_app_state_with_options, }; -async fn create_run(app: &axum::Router, mut manifest: serde_json::Value) -> serde_json::Value { - manifest - .as_object_mut() - .expect("manifest should be an object") - .entry("configs") - .or_insert_with(|| serde_json::json!([])); +async fn create_run(app: &axum::Router, intent: serde_json::Value) -> serde_json::Value { let request = Request::builder() .method("POST") .uri(api("/runs")) .header("content-type", "application/json") .body(Body::from( - serde_json::to_string(&manifest).expect("manifest should serialize"), + serde_json::to_string(&intent).expect("intent should serialize"), )) .expect("create run request should build"); response_json( @@ -54,10 +49,12 @@ async fn request_json( .await } -fn daytona_manifest() -> serde_json::Value { - let mut manifest = minimal_manifest_json(MINIMAL_DOT); - manifest["args"] = serde_json::json!({ "environment": "default" }); - manifest +async fn daytona_intent(app: &axum::Router) -> serde_json::Value { + let workspace = tempfile::tempdir().expect("run target workspace should be created"); + let mut intent = minimal_intent_json(app, MINIMAL_DOT, workspace.path()).await; + intent["environment_id"] = serde_json::json!("default"); + intent["target"] = serde_json::json!({ "kind": "none" }); + intent } fn daytona_disabled_settings() -> crate::helpers::TestAppSettings { @@ -94,11 +91,11 @@ async fn create_run_rejects_disabled_sandbox_provider() { .method("POST") .uri(api("/runs")) .header("content-type", "application/json") - .body(Body::from(daytona_manifest().to_string())) + .body(Body::from(daytona_intent(&app).await.to_string())) .expect("create run request should build"); let body = response_json( app.clone().oneshot(request).await.unwrap(), - StatusCode::BAD_REQUEST, + StatusCode::SERVICE_UNAVAILABLE, "POST /api/v1/runs", ) .await; @@ -117,7 +114,9 @@ async fn preflight_reports_disabled_sandbox_provider() { .method("POST") .uri(api("/preflight")) .header("content-type", "application/json") - .body(Body::from(daytona_manifest().to_string())) + .body(Body::from( + crate::helpers::minimal_manifest_json(MINIMAL_DOT).to_string(), + )) .expect("preflight request should build"); let body = response_json( app.clone().oneshot(request).await.unwrap(), @@ -143,6 +142,7 @@ async fn preflight_reports_disabled_sandbox_provider() { #[tokio::test] async fn run_responses_include_ask_fabro_affordance() { + let workspace = tempfile::tempdir().expect("run target workspace should be created"); let settings = settings_from_toml( r" _version = 1 @@ -153,7 +153,11 @@ _version = 1 .vault_entries([("OPENAI_API_KEY", "test-key")]) .build(); let app = fabro_server::test_support::build_test_router(state); - let created = create_run(&app, minimal_manifest_json(MINIMAL_DOT)).await; + let created = create_run( + &app, + minimal_intent_json(&app, MINIMAL_DOT, workspace.path()).await, + ) + .await; let run_id = created["id"].as_str().unwrap(); assert_eq!(created["ask_fabro"]["available"], false); @@ -200,7 +204,8 @@ _version = 1 #[tokio::test] async fn retrieve_run_settings_returns_dense_snapshot() { - let storage_dir = tempfile::tempdir().unwrap(); + let workspace = tempfile::tempdir().expect("run target workspace should be created"); + let storage_dir = tempfile::tempdir().expect("run target workspace should be created"); let settings = settings_from_toml(&format!( r#" _version = 1 @@ -231,26 +236,14 @@ slug = "fabro-app" let app = fabro_server::test_support::build_test_router(test_app_state_with_options(settings, 5)); - let mut manifest = minimal_manifest_json(MINIMAL_DOT); - manifest["configs"] = serde_json::json!([{ - "type": "user", - "path": "/tmp/home/.fabro/settings.toml", - "source": r#" -_version = 1 - -[run] -goal = "Ship it" - -[cli.output] -verbosity = "verbose" -"# - }]); + let mut intent = minimal_intent_json(&app, MINIMAL_DOT, workspace.path()).await; + intent["goal"] = serde_json::json!("Ship it"); let create_request = Request::builder() .method("POST") .uri(api("/runs")) .header("content-type", "application/json") - .body(Body::from(serde_json::to_string(&manifest).unwrap())) + .body(Body::from(serde_json::to_string(&intent).unwrap())) .unwrap(); let create_response = app.clone().oneshot(create_request).await.unwrap(); let create_status = create_response.status(); @@ -285,13 +278,18 @@ verbosity = "verbose" #[tokio::test] async fn create_run_can_set_parent_and_list_children() { + let workspace = tempfile::tempdir().expect("run target workspace should be created"); let app = fabro_server::test_support::build_test_router(crate::helpers::test_app_state()); - let parent = create_run(&app, minimal_manifest_json(MINIMAL_DOT)).await; + let parent = create_run( + &app, + minimal_intent_json(&app, MINIMAL_DOT, workspace.path()).await, + ) + .await; let parent_id = parent["id"].as_str().unwrap(); - let mut child_manifest = minimal_manifest_json(MINIMAL_DOT); - child_manifest["parent_id"] = serde_json::json!(parent_id); + let mut child_intent = minimal_intent_json(&app, MINIMAL_DOT, workspace.path()).await; + child_intent["parent_id"] = serde_json::json!(parent_id); - let child = create_run(&app, child_manifest).await; + let child = create_run(&app, child_intent).await; let child_id = child["id"].as_str().unwrap(); assert_eq!(child["parent_id"], parent_id); @@ -312,10 +310,23 @@ async fn create_run_can_set_parent_and_list_children() { #[tokio::test] async fn link_relink_and_unlink_parent_are_idempotent() { + let workspace = tempfile::tempdir().expect("run target workspace should be created"); let app = fabro_server::test_support::build_test_router(crate::helpers::test_app_state()); - let parent_1 = create_run(&app, minimal_manifest_json(MINIMAL_DOT)).await; - let parent_2 = create_run(&app, minimal_manifest_json(MINIMAL_DOT)).await; - let child = create_run(&app, minimal_manifest_json(MINIMAL_DOT)).await; + let parent_1 = create_run( + &app, + minimal_intent_json(&app, MINIMAL_DOT, workspace.path()).await, + ) + .await; + let parent_2 = create_run( + &app, + minimal_intent_json(&app, MINIMAL_DOT, workspace.path()).await, + ) + .await; + let child = create_run( + &app, + minimal_intent_json(&app, MINIMAL_DOT, workspace.path()).await, + ) + .await; let parent_1_id = parent_1["id"].as_str().unwrap(); let parent_2_id = parent_2["id"].as_str().unwrap(); let child_id = child["id"].as_str().unwrap(); @@ -403,12 +414,17 @@ async fn link_relink_and_unlink_parent_are_idempotent() { #[tokio::test] async fn deleting_parent_leaves_child_parent_id_as_historical_reference() { + let workspace = tempfile::tempdir().expect("run target workspace should be created"); let app = fabro_server::test_support::build_test_router(crate::helpers::test_app_state()); - let parent = create_run(&app, minimal_manifest_json(MINIMAL_DOT)).await; + let parent = create_run( + &app, + minimal_intent_json(&app, MINIMAL_DOT, workspace.path()).await, + ) + .await; let parent_id = parent["id"].as_str().unwrap(); - let mut child_manifest = minimal_manifest_json(MINIMAL_DOT); - child_manifest["parent_id"] = serde_json::json!(parent_id); - let child = create_run(&app, child_manifest).await; + let mut child_intent = minimal_intent_json(&app, MINIMAL_DOT, workspace.path()).await; + child_intent["parent_id"] = serde_json::json!(parent_id); + let child = create_run(&app, child_intent).await; let child_id = child["id"].as_str().unwrap(); let delete_request = Request::builder() @@ -448,9 +464,18 @@ async fn deleting_parent_leaves_child_parent_id_as_historical_reference() { #[tokio::test] async fn parent_link_validation_rejects_missing_self_and_cycles() { + let workspace = tempfile::tempdir().expect("run target workspace should be created"); let app = fabro_server::test_support::build_test_router(crate::helpers::test_app_state()); - let parent = create_run(&app, minimal_manifest_json(MINIMAL_DOT)).await; - let child = create_run(&app, minimal_manifest_json(MINIMAL_DOT)).await; + let parent = create_run( + &app, + minimal_intent_json(&app, MINIMAL_DOT, workspace.path()).await, + ) + .await; + let child = create_run( + &app, + minimal_intent_json(&app, MINIMAL_DOT, workspace.path()).await, + ) + .await; let parent_id = parent["id"].as_str().unwrap(); let child_id = child["id"].as_str().unwrap(); diff --git a/lib/apps/fabro-server/tests/it/api/sandbox_vnc.rs b/lib/apps/fabro-server/tests/it/api/sandbox_vnc.rs index 2ba6a716d..624b113fb 100644 --- a/lib/apps/fabro-server/tests/it/api/sandbox_vnc.rs +++ b/lib/apps/fabro-server/tests/it/api/sandbox_vnc.rs @@ -3,7 +3,7 @@ use axum::http::{Request, StatusCode}; use tower::ServiceExt; use crate::helpers::{ - MINIMAL_DOT, api, minimal_manifest_json, response_json, response_status, test_app_state, + MINIMAL_DOT, api, minimal_intent_json, response_json, response_status, test_app_state, }; #[tokio::test] @@ -28,13 +28,15 @@ async fn vnc_for_missing_run_returns_not_found() { #[tokio::test] async fn vnc_for_run_without_sandbox_returns_not_found() { + let workspace = tempfile::tempdir().unwrap(); let app = fabro_server::test_support::build_test_router(test_app_state()); let create_req = Request::builder() .method("POST") .uri(api("/runs")) .header("content-type", "application/json") .body(Body::from( - serde_json::to_string(&minimal_manifest_json(MINIMAL_DOT)).unwrap(), + serde_json::to_string(&minimal_intent_json(&app, MINIMAL_DOT, workspace.path()).await) + .unwrap(), )) .unwrap(); let create_response = app.clone().oneshot(create_req).await.unwrap(); diff --git a/lib/apps/fabro-server/tests/it/api/sessions.rs b/lib/apps/fabro-server/tests/it/api/sessions.rs index d192d3539..c4ac167c1 100644 --- a/lib/apps/fabro-server/tests/it/api/sessions.rs +++ b/lib/apps/fabro-server/tests/it/api/sessions.rs @@ -4,17 +4,18 @@ use fabro_static::EnvVars; use tower::ServiceExt; use crate::helpers::{ - MINIMAL_DOT, api, minimal_manifest_json, response_json, response_status, test_app_state, + MINIMAL_DOT, api, minimal_intent_json, response_json, response_status, test_app_state, }; -async fn create_run(app: &axum::Router) -> String { +async fn create_run(app: &axum::Router) -> (String, tempfile::TempDir) { + let workspace = tempfile::tempdir().expect("run target workspace should be created"); let request = Request::builder() .method("POST") .uri(api("/runs")) .header("content-type", "application/json") .body(Body::from( - serde_json::to_string(&minimal_manifest_json(MINIMAL_DOT)) - .expect("manifest should serialize"), + serde_json::to_string(&minimal_intent_json(app, MINIMAL_DOT, workspace.path()).await) + .expect("intent should serialize"), )) .expect("create-run request should build"); let body = response_json( @@ -23,10 +24,11 @@ async fn create_run(app: &axum::Router) -> String { "POST /api/v1/runs", ) .await; - body["id"] + let run_id = body["id"] .as_str() .expect("create-run response should include an id") - .to_string() + .to_string(); + (run_id, workspace) } async fn create_session(app: &axum::Router, run_id: &str, title: &str) -> serde_json::Value { @@ -79,7 +81,7 @@ async fn create_session_response( #[tokio::test] async fn run_bound_session_is_created_as_run_event_and_resolves_by_flat_id() { let app = fabro_server::test_support::build_test_router(test_app_state()); - let run_id = create_run(&app).await; + let (run_id, _run_id_workspace) = create_run(&app).await; let created = create_session(&app, &run_id, "Ask Fabro").await; let session_id = created["id"] @@ -139,7 +141,7 @@ async fn run_bound_session_is_created_as_run_event_and_resolves_by_flat_id() { #[tokio::test] async fn generic_session_creation_requires_the_dedicated_operation_without_advancing_history() { let app = fabro_server::test_support::build_test_router(test_app_state()); - let run_id = create_run(&app).await; + let (run_id, _run_id_workspace) = create_run(&app).await; let before_request = Request::builder() .method("GET") .uri(api(&format!("/runs/{run_id}/events"))) @@ -198,8 +200,8 @@ async fn generic_session_creation_requires_the_dedicated_operation_without_advan #[tokio::test] async fn sessions_are_listed_only_under_their_owning_run() { let app = fabro_server::test_support::build_test_router(test_app_state()); - let first_run_id = create_run(&app).await; - let second_run_id = create_run(&app).await; + let (first_run_id, _first_run_id_workspace) = create_run(&app).await; + let (second_run_id, _second_run_id_workspace) = create_run(&app).await; let created = create_session(&app, &first_run_id, "First run chat").await; let first_request = Request::builder() @@ -234,7 +236,7 @@ async fn sessions_are_listed_only_under_their_owning_run() { #[tokio::test] async fn supplied_session_model_alias_is_canonicalized() { let app = fabro_server::test_support::build_test_router(test_app_state()); - let run_id = create_run(&app).await; + let (run_id, _run_id_workspace) = create_run(&app).await; let created = create_session_with_model(&app, &run_id, "Ask Fabro", "gpt54").await; assert_eq!(created["model"], "gpt-5.4"); @@ -264,7 +266,7 @@ async fn supplied_session_model_alias_is_canonicalized() { #[tokio::test] async fn provider_qualified_session_model_is_canonicalized() { let app = fabro_server::test_support::build_test_router(test_app_state()); - let run_id = create_run(&app).await; + let (run_id, _run_id_workspace) = create_run(&app).await; let created = create_session_with_model(&app, &run_id, "Ask Fabro", "openai/gpt-5.4-mini").await; @@ -276,7 +278,7 @@ async fn provider_qualified_session_model_is_canonicalized() { #[tokio::test] async fn unknown_session_models_preserve_passthrough_on_the_selected_provider() { let app = fabro_server::test_support::build_test_router(test_app_state()); - let run_id = create_run(&app).await; + let (run_id, _run_id_workspace) = create_run(&app).await; for model in ["not-a-real-model", "openai/not-a-real-model"] { let created = create_session_with_model(&app, &run_id, "Ask Fabro", model).await; @@ -288,7 +290,7 @@ async fn unknown_session_models_preserve_passthrough_on_the_selected_provider() #[tokio::test] async fn invalid_session_model_refs_are_rejected_at_creation() { let app = fabro_server::test_support::build_test_router(test_app_state()); - let run_id = create_run(&app).await; + let (run_id, _run_id_workspace) = create_run(&app).await; for model in ["openai", "openai/", "anthropic/gpt-5.4"] { let response = create_session_response( @@ -313,7 +315,7 @@ async fn ambiguous_session_model_refs_are_rejected_at_creation() { .vault_entries([(EnvVars::OPENAI_API_KEY, "test-openai-api-key")]) .build(); let app = fabro_server::test_support::build_test_router(state); - let run_id = create_run(&app).await; + let (run_id, _run_id_workspace) = create_run(&app).await; let response = create_session_response( &app, @@ -332,7 +334,7 @@ async fn ambiguous_session_model_refs_are_rejected_at_creation() { #[tokio::test] async fn session_turn_fails_when_selected_model_provider_becomes_unconfigured() { let app = fabro_server::test_support::build_test_router(test_app_state()); - let run_id = create_run(&app).await; + let (run_id, _run_id_workspace) = create_run(&app).await; let created = create_session_with_model(&app, &run_id, "Ask Fabro", "gpt54").await; let session_id = created["id"] .as_str() @@ -371,7 +373,7 @@ async fn session_turn_fails_when_selected_model_provider_becomes_unconfigured() #[tokio::test] async fn session_metadata_patch_route_is_removed() { let app = fabro_server::test_support::build_test_router(test_app_state()); - let run_id = create_run(&app).await; + let (run_id, _run_id_workspace) = create_run(&app).await; let created = create_session(&app, &run_id, "Ask Fabro").await; let session_id = created["id"] .as_str() @@ -394,7 +396,7 @@ async fn session_metadata_patch_route_is_removed() { #[tokio::test] async fn unsupported_derived_turn_read_routes_are_removed() { let app = fabro_server::test_support::build_test_router(test_app_state()); - let run_id = create_run(&app).await; + let (run_id, _run_id_workspace) = create_run(&app).await; let created = create_session(&app, &run_id, "Ask Fabro").await; let session_id = created["id"] .as_str() @@ -419,7 +421,7 @@ async fn unsupported_derived_turn_read_routes_are_removed() { #[tokio::test] async fn session_events_are_filtered_by_session_and_paginated_by_run_sequence() { let app = fabro_server::test_support::build_test_router(test_app_state()); - let run_id = create_run(&app).await; + let (run_id, _run_id_workspace) = create_run(&app).await; let first = create_session(&app, &run_id, "First").await; let second = create_session(&app, &run_id, "Second").await; let first_id = first["id"].as_str().unwrap(); @@ -482,7 +484,7 @@ async fn session_events_are_filtered_by_session_and_paginated_by_run_sequence() #[tokio::test] async fn inactive_turn_interrupt_returns_conflict() { let app = fabro_server::test_support::build_test_router(test_app_state()); - let run_id = create_run(&app).await; + let (run_id, _run_id_workspace) = create_run(&app).await; let created = create_session(&app, &run_id, "Ask Fabro").await; let session_id = created["id"] .as_str() diff --git a/lib/apps/fabro-server/tests/it/api/system.rs b/lib/apps/fabro-server/tests/it/api/system.rs index b7310d54a..8b0db89d2 100644 --- a/lib/apps/fabro-server/tests/it/api/system.rs +++ b/lib/apps/fabro-server/tests/it/api/system.rs @@ -14,7 +14,7 @@ use tower::ServiceExt; use crate::helpers::{ MINIMAL_DOT, POLL_ATTEMPTS, POLL_INTERVAL, TestAppSettings, api, checked_response, - minimal_manifest_json, minimal_manifest_json_with_dry_run, response_json, response_status, + minimal_intent_json, minimal_intent_json_with_dry_run, response_json, response_status, settings_from_toml, test_app_state_with_options, test_app_with_scheduler, test_settings, wait_for_run_status, }; @@ -43,13 +43,13 @@ fn temp_storage_settings() -> (tempfile::TempDir, TestAppSettings, PathBuf) { (temp, settings, storage_dir) } -async fn create_run(app: &axum::Router, manifest: serde_json::Value) -> String { +async fn create_run(app: &axum::Router, intent: serde_json::Value) -> String { let request = Request::builder() .method("POST") .uri(api("/runs")) .header("content-type", "application/json") .body(Body::from( - serde_json::to_vec(&manifest).expect("manifest fixture should serialize"), + serde_json::to_vec(&intent).expect("intent fixture should serialize"), )) .expect("create-run request should build"); let response = app.clone().oneshot(request).await.unwrap(); @@ -404,10 +404,19 @@ fn assert_percent_if_present(value: &serde_json::Value) { #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn test_app_state_with_options_respects_max_concurrent_runs() { + let workspace = tempfile::tempdir().unwrap(); let app = test_app_with_scheduler(test_app_state_with_options(test_settings(), 1)); - let first_run = create_run(&app, minimal_manifest_json(HUMAN_GATE_DOT)).await; - let second_run = create_run(&app, minimal_manifest_json(HUMAN_GATE_DOT)).await; + let first_run = create_run( + &app, + minimal_intent_json(&app, HUMAN_GATE_DOT, workspace.path()).await, + ) + .await; + let second_run = create_run( + &app, + minimal_intent_json(&app, HUMAN_GATE_DOT, workspace.path()).await, + ) + .await; start_run(&app, &first_run).await; start_run(&app, &second_run).await; @@ -438,10 +447,15 @@ async fn test_app_state_with_options_respects_max_concurrent_runs() { #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn get_system_disk_usage_returns_summary_and_verbose_rows() { + let workspace = tempfile::tempdir().unwrap(); let (_temp, settings, storage_dir) = temp_storage_settings(); let app = test_app_with_scheduler(test_app_state_with_options(settings, 5)); - let run_id = create_run(&app, minimal_manifest_json_with_dry_run(MINIMAL_DOT)).await; + let run_id = create_run( + &app, + minimal_intent_json_with_dry_run(&app, MINIMAL_DOT, workspace.path()).await, + ) + .await; start_run(&app, &run_id).await; let status = wait_for_run_status(&app, &run_id, &["succeeded", "failed"]).await; assert_eq!(status, "succeeded"); @@ -488,10 +502,15 @@ async fn get_system_disk_usage_returns_summary_and_verbose_rows() { #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn prune_runs_supports_dry_run_and_deletion() { + let workspace = tempfile::tempdir().unwrap(); let (_temp, settings, storage_dir) = temp_storage_settings(); let app = test_app_with_scheduler(test_app_state_with_options(settings, 5)); - let run_id = create_run(&app, minimal_manifest_json_with_dry_run(MINIMAL_DOT)).await; + let run_id = create_run( + &app, + minimal_intent_json_with_dry_run(&app, MINIMAL_DOT, workspace.path()).await, + ) + .await; start_run(&app, &run_id).await; let status = wait_for_run_status(&app, &run_id, &["succeeded", "failed"]).await; assert_eq!(status, "succeeded"); diff --git a/lib/apps/fabro-server/tests/it/api/variables.rs b/lib/apps/fabro-server/tests/it/api/variables.rs index c3f971506..caec2775d 100644 --- a/lib/apps/fabro-server/tests/it/api/variables.rs +++ b/lib/apps/fabro-server/tests/it/api/variables.rs @@ -3,7 +3,7 @@ use axum::http::{Method, Request, StatusCode}; use tower::ServiceExt; use crate::helpers::{ - MINIMAL_DOT, api, body_json, minimal_manifest_json, response_json, response_status, + MINIMAL_DOT, api, body_json, minimal_intent_json, response_json, response_status, test_app_state, test_app_state_with_options, test_settings, }; @@ -220,6 +220,7 @@ async fn variables_persist_across_rebuilt_app_state() { #[tokio::test] async fn run_config_substitutes_variables_before_persisting_settings() { + let workspace = tempfile::tempdir().unwrap(); let app = fabro_server::test_support::build_test_router(test_app_state()); let create_variable = app @@ -236,24 +237,12 @@ async fn run_config_substitutes_variables_before_persisting_settings() { .expect("POST /variables should route"); response_status(create_variable, StatusCode::OK, "POST /api/v1/variables").await; - let mut manifest = minimal_manifest_json(MINIMAL_DOT); - manifest["configs"] = serde_json::json!([{ - "type": "project", - "path": ".fabro/project.toml", - "source": r#" -_version = 1 - -[run] -goal = "secret: {{ vars.RUNTIME_TOKEN }}" - -[run.environment] -id = "local" -"# - }]); + let mut intent = minimal_intent_json(&app, MINIMAL_DOT, workspace.path()).await; + intent["goal"] = serde_json::json!("secret: {{ vars.RUNTIME_TOKEN }}"); let create_run = app .clone() - .oneshot(json_request(Method::POST, "/runs", &manifest)) + .oneshot(json_request(Method::POST, "/runs", &intent)) .await .expect("POST /runs should route"); let create_status = create_run.status(); @@ -282,6 +271,7 @@ id = "local" #[tokio::test] async fn run_create_interpolates_variables_into_node_prompts() { + let workspace = tempfile::tempdir().unwrap(); // End-to-end through the real run-create path: a server variable resolves // inside a node `prompt` (a DOT graph attribute the settings substitution // pass never touches), proving the variable store is snapshotted into the @@ -315,7 +305,7 @@ async fn run_create_interpolates_variables_into_node_prompts() { .oneshot(json_request( Method::POST, "/runs", - &minimal_manifest_json(dot), + &minimal_intent_json(&app, dot, workspace.path()).await, )) .await .expect("POST /runs should route"); @@ -383,7 +373,7 @@ async fn run_validate_resolves_variables_in_node_prompts() { .oneshot(json_request( Method::POST, "/validate", - &minimal_manifest_json(dot), + &crate::helpers::minimal_manifest_json(dot), )) .await .expect("POST /validate should route"); diff --git a/lib/apps/fabro-server/tests/it/helpers.rs b/lib/apps/fabro-server/tests/it/helpers.rs index 790fc8dd2..beb521e3b 100644 --- a/lib/apps/fabro-server/tests/it/helpers.rs +++ b/lib/apps/fabro-server/tests/it/helpers.rs @@ -211,16 +211,16 @@ pub(crate) async fn reqwest_status( assert_reqwest_status(response, expected, context).await; } -pub(crate) async fn create_and_start_run_from_manifest( +pub(crate) async fn create_and_start_run_from_intent( app: &axum::Router, - manifest: serde_json::Value, + intent: serde_json::Value, ) -> String { let req = Request::builder() .method("POST") .uri(api("/runs")) .header("content-type", "application/json") .body(Body::from( - serde_json::to_string(&manifest).expect("manifest fixture should serialize"), + serde_json::to_string(&intent).expect("intent fixture should serialize"), )) .expect("create-run request should build"); let response = app.clone().oneshot(req).await.unwrap(); @@ -261,10 +261,31 @@ pub(crate) fn minimal_manifest_json(dot_source: &str) -> serde_json::Value { }) } -pub(crate) fn minimal_manifest_json_with_dry_run(dot_source: &str) -> serde_json::Value { - let mut manifest = minimal_manifest_json(dot_source); - manifest["args"] = serde_json::json!({ "dry_run": true }); - manifest +pub(crate) async fn minimal_intent_json( + app: &axum::Router, + source: &str, + workspace: &std::path::Path, +) -> serde_json::Value { + let path = fabro_types::WorkflowPath::new("workflow.fabro") + .expect("workflow fixture path should be valid"); + let version = fabro_types::WorkflowVersion::new( + path.clone(), + std::collections::BTreeMap::from([(path, source.to_string())]), + std::collections::BTreeMap::new(), + ) + .expect("workflow fixture version should be valid"); + let id = fabro_server::test_support::test_register_workflow_version(app, &version, None).await; + serde_json::json!({"workflow_version_id": id, "target": {"kind": "folder", "path": workspace}, "environment_id": "local", "args": {}}) +} + +pub(crate) async fn minimal_intent_json_with_dry_run( + app: &axum::Router, + source: &str, + workspace: &std::path::Path, +) -> serde_json::Value { + let mut intent = minimal_intent_json(app, source, workspace).await; + intent["args"]["dry_run"] = serde_json::json!(true); + intent } pub(crate) async fn run_json(app: &axum::Router, run_id: &str) -> serde_json::Value { diff --git a/lib/apps/fabro-server/tests/it/scenario/archive.rs b/lib/apps/fabro-server/tests/it/scenario/archive.rs index 1f8f84d37..fb51d1f7b 100644 --- a/lib/apps/fabro-server/tests/it/scenario/archive.rs +++ b/lib/apps/fabro-server/tests/it/scenario/archive.rs @@ -3,19 +3,22 @@ use axum::http::{Request, StatusCode}; use tower::ServiceExt; use crate::helpers::{ - MINIMAL_DOT, api, create_and_start_run_from_manifest, minimal_manifest_json_with_dry_run, + MINIMAL_DOT, api, create_and_start_run_from_intent, minimal_intent_json_with_dry_run, response_json, response_status, test_app_state_with_options, test_app_with_scheduler, test_settings, wait_for_run_status, }; #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn archived_runs_reject_mutations_with_actionable_body() { + let workspace = tempfile::tempdir().unwrap(); let state = test_app_state_with_options(test_settings(), 5); let app = test_app_with_scheduler(state); - let run_id = - create_and_start_run_from_manifest(&app, minimal_manifest_json_with_dry_run(MINIMAL_DOT)) - .await; + let run_id = create_and_start_run_from_intent( + &app, + minimal_intent_json_with_dry_run(&app, MINIMAL_DOT, workspace.path()).await, + ) + .await; let status = wait_for_run_status(&app, &run_id, &["succeeded", "failed"]).await; assert_eq!(status, "succeeded"); @@ -149,14 +152,17 @@ async fn archived_runs_reject_mutations_with_actionable_body() { #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn appending_run_archived_event_directly_is_rejected() { + let workspace = tempfile::tempdir().unwrap(); // Regression: archive/unarchive events must not be injectable via // `append_run_event` — clients must use the operation endpoints. let state = test_app_state_with_options(test_settings(), 5); let app = test_app_with_scheduler(state); - let run_id = - create_and_start_run_from_manifest(&app, minimal_manifest_json_with_dry_run(MINIMAL_DOT)) - .await; + let run_id = create_and_start_run_from_intent( + &app, + minimal_intent_json_with_dry_run(&app, MINIMAL_DOT, workspace.path()).await, + ) + .await; wait_for_run_status(&app, &run_id, &["succeeded", "failed"]).await; let req = Request::builder() @@ -222,12 +228,15 @@ async fn archive_returns_404_for_unknown_run() { #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn list_runs_respects_include_archived_flag() { + let workspace = tempfile::tempdir().unwrap(); let state = test_app_state_with_options(test_settings(), 5); let app = test_app_with_scheduler(state); - let run_id = - create_and_start_run_from_manifest(&app, minimal_manifest_json_with_dry_run(MINIMAL_DOT)) - .await; + let run_id = create_and_start_run_from_intent( + &app, + minimal_intent_json_with_dry_run(&app, MINIMAL_DOT, workspace.path()).await, + ) + .await; wait_for_run_status(&app, &run_id, &["succeeded", "failed"]).await; // Archive it. diff --git a/lib/apps/fabro-server/tests/it/scenario/dry_run.rs b/lib/apps/fabro-server/tests/it/scenario/dry_run.rs index a8b81c715..b13a98c0c 100644 --- a/lib/apps/fabro-server/tests/it/scenario/dry_run.rs +++ b/lib/apps/fabro-server/tests/it/scenario/dry_run.rs @@ -4,10 +4,10 @@ use httpmock::MockServer; use tower::ServiceExt; use crate::helpers::{ - MINIMAL_DOT, api, checked_response, create_and_start_run_from_manifest, minimal_manifest_json, - minimal_manifest_json_with_dry_run, response_json, response_status, - test_app_state_with_options, test_app_with_mock_anthropic, test_app_with_no_providers, - test_app_with_scheduler, test_settings, wait_for_run_status, + MINIMAL_DOT, api, checked_response, create_and_start_run_from_intent, + minimal_intent_json_with_dry_run, response_json, response_status, test_app_state_with_options, + test_app_with_mock_anthropic, test_app_with_no_providers, test_app_with_scheduler, + test_settings, wait_for_run_status, }; fn completion_request(stream: bool) -> Request { @@ -43,12 +43,15 @@ fn completion_request_with_model(stream: bool, model: &str) -> Request { #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn dry_run_serve_starts_and_runs_workflow() { + let workspace = tempfile::tempdir().unwrap(); let state = test_app_state_with_options(test_settings(), 5); let app = test_app_with_scheduler(state); - let run_id = - create_and_start_run_from_manifest(&app, minimal_manifest_json_with_dry_run(MINIMAL_DOT)) - .await; + let run_id = create_and_start_run_from_intent( + &app, + minimal_intent_json_with_dry_run(&app, MINIMAL_DOT, workspace.path()).await, + ) + .await; let status = wait_for_run_status(&app, &run_id, &["succeeded", "failed"]).await; assert_eq!(status, "succeeded"); @@ -100,20 +103,15 @@ async fn test_model_unknown_via_full_router() { } #[tokio::test] -async fn dry_run_serve_rejects_invalid_dot() { +async fn registration_rejects_invalid_dot_without_a_provider() { let app = test_app_with_no_providers(); - - let req = Request::builder() - .method("POST") - .uri(api("/runs")) - .header("content-type", "application/json") - .body(Body::from( - serde_json::to_string(&minimal_manifest_json("not valid dot")).unwrap(), - )) - .unwrap(); - - let response = app.oneshot(req).await.unwrap(); - response_status(response, StatusCode::BAD_REQUEST, "POST /api/v1/runs").await; + let req = Request::builder().method("POST").uri(api("/workflow-versions")).header("content-type", "application/json").body(Body::from(serde_json::json!({"entrypoint":"workflow.fabro","files":{"workflow.fabro":"not valid dot"},"workflow_dependencies":{}}).to_string())).unwrap(); + response_status( + app.oneshot(req).await.unwrap(), + StatusCode::UNPROCESSABLE_ENTITY, + "register invalid workflow", + ) + .await; } #[tokio::test] diff --git a/lib/apps/fabro-server/tests/it/scenario/lifecycle.rs b/lib/apps/fabro-server/tests/it/scenario/lifecycle.rs index 181a98a37..2221e70c1 100644 --- a/lib/apps/fabro-server/tests/it/scenario/lifecycle.rs +++ b/lib/apps/fabro-server/tests/it/scenario/lifecycle.rs @@ -14,7 +14,7 @@ use tokio::time::sleep; use tower::ServiceExt; use crate::helpers::{ - POLL_ATTEMPTS, POLL_INTERVAL, api, minimal_manifest_json, response_json, response_status, + POLL_ATTEMPTS, POLL_INTERVAL, api, minimal_intent_json, response_json, response_status, run_json, test_settings, wait_for_run_status, }; @@ -117,6 +117,7 @@ const GATE_DOT: &str = r#"digraph GateTest { #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn full_http_lifecycle_approve_and_complete() { + let workspace = tempfile::tempdir().unwrap(); let settings = test_settings(); let state = test_app_state_with_runtime_settings_and_registry_factory( settings.server_settings, @@ -132,7 +133,8 @@ async fn full_http_lifecycle_approve_and_complete() { .uri(api("/runs")) .header("content-type", "application/json") .body(Body::from( - serde_json::to_string(&minimal_manifest_json(GATE_DOT)).unwrap(), + serde_json::to_string(&minimal_intent_json(&app, GATE_DOT, workspace.path()).await) + .unwrap(), )) .unwrap(); @@ -209,6 +211,7 @@ async fn full_http_lifecycle_approve_and_complete() { #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn full_http_lifecycle_cancel() { + let workspace = tempfile::tempdir().unwrap(); let settings = test_settings(); let state = test_app_state_with_runtime_settings_and_registry_factory( settings.server_settings, @@ -224,7 +227,8 @@ async fn full_http_lifecycle_cancel() { .uri(api("/runs")) .header("content-type", "application/json") .body(Body::from( - serde_json::to_string(&minimal_manifest_json(GATE_DOT)).unwrap(), + serde_json::to_string(&minimal_intent_json(&app, GATE_DOT, workspace.path()).await) + .unwrap(), )) .unwrap(); let response = app.clone().oneshot(req).await.unwrap(); @@ -291,6 +295,7 @@ async fn full_http_lifecycle_cancel() { #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn cancel_at_human_gate_persists_cancelled_terminal_event() { + let workspace = tempfile::tempdir().unwrap(); let settings = test_settings(); let state = test_app_state_with_runtime_settings_and_registry_factory( settings.server_settings, @@ -305,7 +310,8 @@ async fn cancel_at_human_gate_persists_cancelled_terminal_event() { .uri(api("/runs")) .header("content-type", "application/json") .body(Body::from( - serde_json::to_string(&minimal_manifest_json(GATE_DOT)).unwrap(), + serde_json::to_string(&minimal_intent_json(&app, GATE_DOT, workspace.path()).await) + .unwrap(), )) .unwrap(); let response = app.clone().oneshot(req).await.unwrap(); diff --git a/lib/apps/fabro-server/tests/it/scenario/run_completion.rs b/lib/apps/fabro-server/tests/it/scenario/run_completion.rs index 4595eec10..709f6d325 100644 --- a/lib/apps/fabro-server/tests/it/scenario/run_completion.rs +++ b/lib/apps/fabro-server/tests/it/scenario/run_completion.rs @@ -10,8 +10,8 @@ use tokio::time::sleep; use tower::ServiceExt; use crate::helpers::{ - MINIMAL_DOT, api, checked_response, create_and_start_run_from_manifest, minimal_manifest_json, - minimal_manifest_json_with_dry_run, response_text, test_app_state_with_options, + MINIMAL_DOT, api, checked_response, create_and_start_run_from_intent, minimal_intent_json, + minimal_intent_json_with_dry_run, response_text, test_app_state_with_options, test_app_with_scheduler, test_settings, wait_for_run_status, }; @@ -76,12 +76,15 @@ fn test_app_with_openai_agent_backend(openai_base_url: String, api_key: String) #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn run_completes_and_status_is_completed() { + let workspace = tempfile::tempdir().unwrap(); let state = test_app_state_with_options(test_settings(), 5); let app = test_app_with_scheduler(state); - let run_id = - create_and_start_run_from_manifest(&app, minimal_manifest_json_with_dry_run(MINIMAL_DOT)) - .await; + let run_id = create_and_start_run_from_intent( + &app, + minimal_intent_json_with_dry_run(&app, MINIMAL_DOT, workspace.path()).await, + ) + .await; let status = wait_for_run_status(&app, &run_id, &["succeeded", "failed"]).await; assert_eq!(status, "succeeded"); @@ -89,6 +92,7 @@ async fn run_completes_and_status_is_completed() { #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn agent_run_includes_project_skills_from_local_sandbox_working_directory() { + let workspace = tempfile::tempdir().unwrap(); let project = tempfile::tempdir().expect("project tempdir should create"); let skill_dir = project .path() @@ -117,10 +121,10 @@ async fn agent_run_includes_project_skills_from_local_sandbox_working_directory( .await; let app = test_app_with_openai_agent_backend(twin.base_url.clone(), namespace.clone()); - let mut manifest = minimal_manifest_json(PROJECT_SKILL_AGENT_DOT); - manifest["title"] = serde_json::Value::String("Project skill agent".to_string()); - manifest["cwd"] = serde_json::Value::String(project.path().display().to_string()); - let run_id = create_and_start_run_from_manifest(&app, manifest).await; + let mut intent = minimal_intent_json(&app, PROJECT_SKILL_AGENT_DOT, workspace.path()).await; + intent["title"] = serde_json::Value::String("Project skill agent".to_string()); + intent["target"] = serde_json::json!({"kind": "folder", "path": project.path()}); + let run_id = create_and_start_run_from_intent(&app, intent).await; let status = wait_for_run_status(&app, &run_id, &["succeeded", "failed"]).await; assert_eq!(status, "succeeded"); @@ -145,12 +149,15 @@ async fn agent_run_includes_project_skills_from_local_sandbox_working_directory( #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn attach_run_events_returns_sse_stream() { + let workspace = tempfile::tempdir().unwrap(); let state = test_app_state_with_options(test_settings(), 5); let app = test_app_with_scheduler(state); - let run_id = - create_and_start_run_from_manifest(&app, minimal_manifest_json_with_dry_run(MINIMAL_DOT)) - .await; + let run_id = create_and_start_run_from_intent( + &app, + minimal_intent_json_with_dry_run(&app, MINIMAL_DOT, workspace.path()).await, + ) + .await; // Wait for scheduler to promote run. sleep(std::time::Duration::from_millis(100)).await; @@ -181,12 +188,15 @@ async fn attach_run_events_returns_sse_stream() { #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn attach_run_events_replays_terminal_event_after_completion() { + let workspace = tempfile::tempdir().unwrap(); let state = test_app_state_with_options(test_settings(), 5); let app = test_app_with_scheduler(state); - let run_id = - create_and_start_run_from_manifest(&app, minimal_manifest_json_with_dry_run(MINIMAL_DOT)) - .await; + let run_id = create_and_start_run_from_intent( + &app, + minimal_intent_json_with_dry_run(&app, MINIMAL_DOT, workspace.path()).await, + ) + .await; let status = wait_for_run_status(&app, &run_id, &["succeeded", "failed"]).await; assert_eq!(status, "succeeded"); diff --git a/lib/apps/fabro-server/tests/it/scenario/sse.rs b/lib/apps/fabro-server/tests/it/scenario/sse.rs index d95f1b174..0a7862c78 100644 --- a/lib/apps/fabro-server/tests/it/scenario/sse.rs +++ b/lib/apps/fabro-server/tests/it/scenario/sse.rs @@ -8,7 +8,7 @@ use tower::ServiceExt; use crate::helpers::{ POLL_ATTEMPTS, POLL_INTERVAL, api, checked_response, checked_response_in, - create_and_start_run_from_manifest, minimal_manifest_json_with_dry_run, response_json, + create_and_start_run_from_intent, minimal_intent_json_with_dry_run, response_json, test_app_state_with_options, test_app_with_scheduler, test_settings, wait_for_run_status_not_in, }; @@ -51,12 +51,15 @@ async fn wait_for_checkpoint(app: &axum::Router, run_id: &str) -> serde_json::Va #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn sse_stream_contains_expected_event_types() { + let workspace = tempfile::tempdir().unwrap(); let state = test_app_state_with_options(test_settings(), 5); let app = test_app_with_scheduler(state); - let run_id = - create_and_start_run_from_manifest(&app, minimal_manifest_json_with_dry_run(SIMPLE_DOT)) - .await; + let run_id = create_and_start_run_from_intent( + &app, + minimal_intent_json_with_dry_run(&app, SIMPLE_DOT, workspace.path()).await, + ) + .await; wait_for_run_status_not_in(&app, &run_id, &["runnable", "starting"]).await; diff --git a/lib/apps/fabro-server/tests/it/scenario/usage.rs b/lib/apps/fabro-server/tests/it/scenario/usage.rs index 7215e101c..9ead50a03 100644 --- a/lib/apps/fabro-server/tests/it/scenario/usage.rs +++ b/lib/apps/fabro-server/tests/it/scenario/usage.rs @@ -4,8 +4,8 @@ use tokio::time::sleep; use tower::ServiceExt; use crate::helpers::{ - MINIMAL_DOT, POLL_ATTEMPTS, POLL_INTERVAL, api, create_and_start_run_from_manifest, - minimal_manifest_json, minimal_manifest_json_with_dry_run, test_app_state_with_options, + MINIMAL_DOT, POLL_ATTEMPTS, POLL_INTERVAL, api, create_and_start_run_from_intent, + minimal_intent_json, minimal_intent_json_with_dry_run, test_app_state_with_options, test_app_with_scheduler, test_settings, wait_for_run_status, }; @@ -27,12 +27,15 @@ const WAIT_DOT: &str = r#"digraph Test { #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn aggregate_billing_increments_after_run_completes() { + let workspace = tempfile::tempdir().unwrap(); let state = test_app_state_with_options(test_settings(), 5); let app = test_app_with_scheduler(state); - let run_id = - create_and_start_run_from_manifest(&app, minimal_manifest_json_with_dry_run(MINIMAL_DOT)) - .await; + let run_id = create_and_start_run_from_intent( + &app, + minimal_intent_json_with_dry_run(&app, MINIMAL_DOT, workspace.path()).await, + ) + .await; // Poll until run completes let status = wait_for_run_status(&app, &run_id, &["succeeded", "failed"]).await; @@ -64,10 +67,15 @@ async fn aggregate_billing_increments_after_run_completes() { #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn run_billing_includes_completed_non_llm_stages() { + let workspace = tempfile::tempdir().unwrap(); let state = test_app_state_with_options(test_settings(), 5); let app = test_app_with_scheduler(state); - let run_id = create_and_start_run_from_manifest(&app, minimal_manifest_json(WAIT_DOT)).await; + let run_id = create_and_start_run_from_intent( + &app, + minimal_intent_json(&app, WAIT_DOT, workspace.path()).await, + ) + .await; let status = wait_for_run_status(&app, &run_id, &["succeeded", "failed"]).await; assert_eq!(status, "succeeded"); @@ -78,10 +86,15 @@ async fn run_billing_includes_completed_non_llm_stages() { #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn run_billing_includes_completed_command_stages() { + let workspace = tempfile::tempdir().unwrap(); let state = test_app_state_with_options(test_settings(), 5); let app = test_app_with_scheduler(state); - let run_id = create_and_start_run_from_manifest(&app, minimal_manifest_json(COMMAND_DOT)).await; + let run_id = create_and_start_run_from_intent( + &app, + minimal_intent_json(&app, COMMAND_DOT, workspace.path()).await, + ) + .await; let status = wait_for_run_status(&app, &run_id, &["succeeded", "failed"]).await; assert_eq!(status, "succeeded"); diff --git a/lib/components/fabro-store/src/run_state.rs b/lib/components/fabro-store/src/run_state.rs index 837fe274d..c965b0c3c 100644 --- a/lib/components/fabro-store/src/run_state.rs +++ b/lib/components/fabro-store/src/run_state.rs @@ -872,7 +872,6 @@ fn projection_from_created(event: &EventEnvelope) -> Result { source_directory: props.source_directory.clone(), labels, provenance: props.provenance.clone(), - manifest_blob: props.manifest_blob, definition_blob: None, spec_blob: props.spec_blob, git: props.git.clone(), @@ -4216,10 +4215,7 @@ mod tests { let state = RunProjection::apply_events(&events).unwrap(); let value = serde_json::to_value(&state).unwrap(); - assert_eq!( - value["spec"]["manifest_blob"], - events[0].event.properties().unwrap()["manifest_blob"] - ); + assert!(value["spec"].get("manifest_blob").is_none()); assert_eq!( value["spec"]["definition_blob"], events[1].event.properties().unwrap()["definition_blob"] diff --git a/lib/components/fabro-store/src/run_summary_store.rs b/lib/components/fabro-store/src/run_summary_store.rs index 5181d9be5..191a897ce 100644 --- a/lib/components/fabro-store/src/run_summary_store.rs +++ b/lib/components/fabro-store/src/run_summary_store.rs @@ -1577,7 +1577,6 @@ mod tests { source_directory: None, labels: HashMap::new(), provenance: test_support::test_run_provenance(), - manifest_blob: None, definition_blob: None, spec_blob: None, git: None, diff --git a/lib/components/fabro-store/src/slate/mod.rs b/lib/components/fabro-store/src/slate/mod.rs index 663f72bc2..219ba72ef 100644 --- a/lib/components/fabro-store/src/slate/mod.rs +++ b/lib/components/fabro-store/src/slate/mod.rs @@ -466,7 +466,6 @@ mod tests { source_directory: Some(format!("/tmp/{label}")), labels: std::collections::HashMap::from([("team".to_string(), "infra".to_string())]), provenance: test_support::test_run_provenance(), - manifest_blob: None, definition_blob: None, spec_blob: None, git: Some(fabro_types::GitContext { diff --git a/lib/components/fabro-workflow/src/event/convert.rs b/lib/components/fabro-workflow/src/event/convert.rs index 1537efe47..e74f22813 100644 --- a/lib/components/fabro-workflow/src/event/convert.rs +++ b/lib/components/fabro-workflow/src/event/convert.rs @@ -70,7 +70,6 @@ fn event_body_from_event(event: &Event) -> EventBody { target, automation, provenance, - manifest_blob, spec_blob, git, fork_source_ref, @@ -91,7 +90,6 @@ fn event_body_from_event(event: &Event) -> EventBody { target: target.clone(), automation: automation.clone(), provenance: provenance.clone(), - manifest_blob: *manifest_blob, spec_blob: *spec_blob, git: git.clone(), fork_source_ref: fork_source_ref.clone(), @@ -2086,7 +2084,6 @@ mod tests { target: None, automation: Some(automation.clone()), provenance, - manifest_blob: None, spec_blob: None, git: None, fork_source_ref: None, diff --git a/lib/components/fabro-workflow/src/event/events.rs b/lib/components/fabro-workflow/src/event/events.rs index a16d1488b..69ea3e38e 100644 --- a/lib/components/fabro-workflow/src/event/events.rs +++ b/lib/components/fabro-workflow/src/event/events.rs @@ -43,8 +43,6 @@ pub enum Event { automation: Option, provenance: RunProvenance, #[serde(default, skip_serializing_if = "Option::is_none")] - manifest_blob: Option, - #[serde(default, skip_serializing_if = "Option::is_none")] spec_blob: Option, #[serde(default, skip_serializing_if = "Option::is_none")] git: Option, diff --git a/lib/components/fabro-workflow/src/event/sink.rs b/lib/components/fabro-workflow/src/event/sink.rs index b06c03b9d..ace4deb37 100644 --- a/lib/components/fabro-workflow/src/event/sink.rs +++ b/lib/components/fabro-workflow/src/event/sink.rs @@ -387,7 +387,6 @@ mod tests { target: None, automation: None, provenance: test_support::test_run_provenance(), - manifest_blob: None, spec_blob: None, git: None, fork_source_ref: None, diff --git a/lib/components/fabro-workflow/src/git.rs b/lib/components/fabro-workflow/src/git.rs index c169215f2..bfc59f90f 100644 --- a/lib/components/fabro-workflow/src/git.rs +++ b/lib/components/fabro-workflow/src/git.rs @@ -546,7 +546,6 @@ mod tests { target: None, automation: None, provenance: test_support::test_run_provenance(), - manifest_blob: None, spec_blob: None, git: None, fork_source_ref: None, diff --git a/lib/components/fabro-workflow/src/handler/agent.rs b/lib/components/fabro-workflow/src/handler/agent.rs index e2b0c4a09..0389037de 100644 --- a/lib/components/fabro-workflow/src/handler/agent.rs +++ b/lib/components/fabro-workflow/src/handler/agent.rs @@ -526,7 +526,6 @@ mod tests { target: None, automation: None, provenance: test_support::test_run_provenance(), - manifest_blob: None, spec_blob: None, git: None, fork_source_ref: None, diff --git a/lib/components/fabro-workflow/src/handler/command.rs b/lib/components/fabro-workflow/src/handler/command.rs index 5d4c0dd4c..859f3c7c6 100644 --- a/lib/components/fabro-workflow/src/handler/command.rs +++ b/lib/components/fabro-workflow/src/handler/command.rs @@ -386,7 +386,6 @@ mod tests { source_directory: None, labels: std::collections::HashMap::default(), provenance: test_support::test_run_provenance(), - manifest_blob: None, definition_blob: None, spec_blob: None, git: None, @@ -490,7 +489,6 @@ mod tests { target: None, automation: None, provenance: test_support::test_run_provenance(), - manifest_blob: None, spec_blob: None, git: None, fork_source_ref: None, diff --git a/lib/components/fabro-workflow/src/handler/parallel.rs b/lib/components/fabro-workflow/src/handler/parallel.rs index c2f81653d..0d5fd2723 100644 --- a/lib/components/fabro-workflow/src/handler/parallel.rs +++ b/lib/components/fabro-workflow/src/handler/parallel.rs @@ -1001,7 +1001,6 @@ mod tests { target: None, automation: None, provenance: test_support::test_run_provenance(), - manifest_blob: None, spec_blob: None, git: None, fork_source_ref: None, diff --git a/lib/components/fabro-workflow/src/handler/prompt.rs b/lib/components/fabro-workflow/src/handler/prompt.rs index 1f5137c3e..3666869d2 100644 --- a/lib/components/fabro-workflow/src/handler/prompt.rs +++ b/lib/components/fabro-workflow/src/handler/prompt.rs @@ -261,7 +261,6 @@ mod tests { target: None, automation: None, provenance: test_support::test_run_provenance(), - manifest_blob: None, spec_blob: None, git: None, fork_source_ref: None, diff --git a/lib/components/fabro-workflow/src/operations/archive.rs b/lib/components/fabro-workflow/src/operations/archive.rs index 1f0924bf0..8b2ae1876 100644 --- a/lib/components/fabro-workflow/src/operations/archive.rs +++ b/lib/components/fabro-workflow/src/operations/archive.rs @@ -228,7 +228,6 @@ mod tests { target: None, automation: None, provenance: test_support::test_run_provenance(), - manifest_blob: None, spec_blob: None, git: None, fork_source_ref: None, diff --git a/lib/components/fabro-workflow/src/operations/create.rs b/lib/components/fabro-workflow/src/operations/create.rs index 3847ba7f0..b888ddeeb 100644 --- a/lib/components/fabro-workflow/src/operations/create.rs +++ b/lib/components/fabro-workflow/src/operations/create.rs @@ -35,30 +35,29 @@ use crate::workflow_bundle::{RunDefinition, WorkflowBundle}; #[derive(Clone, Debug)] pub struct CreateRunInput { - pub workflow: WorkflowInput, - pub settings: WorkflowSettings, + pub workflow: WorkflowInput, + pub settings: WorkflowSettings, /// Run-scoped variables (`{{ vars.* }}`) snapshotted from the server's /// variable store at create time, threaded into the template render /// context for prompts and goals. Empty for offline/CLI callers. - pub vars: HashMap, - pub cwd: PathBuf, - pub workflow_slug: Option, - pub workflow_path: Option, - pub workflow_bundle: Option, - pub target: Option, - pub submitted_manifest_bytes: Option>, - pub run_id: Option, - pub title: Option, - pub automation: Option, - pub git: Option, - pub fork_source_ref: Option, - pub parent_id: Option, - pub provenance: RunProvenance, + pub vars: HashMap, + pub cwd: PathBuf, + pub workflow_slug: Option, + pub workflow_path: Option, + pub workflow_bundle: Option, + pub target: Option, + pub run_id: Option, + pub title: Option, + pub automation: Option, + pub git: Option, + pub fork_source_ref: Option, + pub parent_id: Option, + pub provenance: RunProvenance, pub configured_providers: Vec, /// Public URL where this run can be viewed in the web UI, when the server /// has the web UI enabled. Recorded on the `run.created` event so attach /// replays can surface the link. - pub web_url: Option, + pub web_url: Option, } impl CreateRunInput { @@ -78,7 +77,6 @@ impl CreateRunInput { workflow_path, workflow_bundle, target, - submitted_manifest_bytes, run_id: _, title, automation, @@ -105,7 +103,6 @@ impl CreateRunInput { workflow_slug, workflow_version_id: None, target, - submitted_manifest_bytes, title, automation, git, @@ -135,19 +132,18 @@ pub struct CreateRunCompileInput { /// run's scratch directory during pure input assembly. #[derive(Debug)] pub struct CreateRunPersistenceMetadata { - pub run_id: RunId, - pub storage_root: PathBuf, - pub workflow_slug: Option, + pub run_id: RunId, + pub storage_root: PathBuf, + pub workflow_slug: Option, pub workflow_version_id: Option, - pub target: Option, - pub submitted_manifest_bytes: Option>, - pub title: Option, - pub automation: Option, - pub git: Option, - pub fork_source_ref: Option, - pub parent_id: Option, - pub provenance: RunProvenance, - pub web_url: Option, + pub target: Option, + pub title: Option, + pub automation: Option, + pub git: Option, + pub fork_source_ref: Option, + pub parent_id: Option, + pub provenance: RunProvenance, + pub web_url: Option, } #[derive(Debug)] @@ -205,20 +201,19 @@ impl MaterializedRun { /// Complete input for creating a durable run. The run ID and run directory /// are resolved during assembly, before persistence begins. pub struct CreateRunPersistenceInput { - materialized: MaterializedRun, - run_id: RunId, - run_dir: PathBuf, - workflow_slug: Option, + materialized: MaterializedRun, + run_id: RunId, + run_dir: PathBuf, + workflow_slug: Option, workflow_version_id: Option, - target: Option, - submitted_manifest_bytes: Option>, - title: Option, - automation: Option, - git: Option, - fork_source_ref: Option, - parent_id: Option, - provenance: RunProvenance, - web_url: Option, + target: Option, + title: Option, + automation: Option, + git: Option, + fork_source_ref: Option, + parent_id: Option, + provenance: RunProvenance, + web_url: Option, } impl CreateRunPersistenceInput { @@ -242,10 +237,6 @@ impl CreateRunPersistenceInput { self.workflow_version_id } - pub fn submitted_manifest_bytes(&self) -> Option<&[u8]> { - self.submitted_manifest_bytes.as_deref() - } - pub fn automation(&self) -> Option<&AutomationRef> { self.automation.as_ref() } @@ -412,7 +403,6 @@ pub fn assemble_create_run_persistence_input( workflow_slug, workflow_version_id, target, - submitted_manifest_bytes, title, automation, git, @@ -434,7 +424,6 @@ pub fn assemble_create_run_persistence_input( workflow_slug, workflow_version_id, target, - submitted_manifest_bytes, title, automation, git, @@ -457,7 +446,6 @@ pub async fn persist_create_run( workflow_slug, workflow_version_id, target, - submitted_manifest_bytes, title, automation, git, @@ -495,7 +483,6 @@ pub async fn persist_create_run( source_directory, labels, provenance, - manifest_blob: None, definition_blob: None, spec_blob: None, git, @@ -513,7 +500,6 @@ pub async fn persist_create_run( store, &persisted, &raw_source, - submitted_manifest_bytes.as_deref(), definition.as_ref(), title, parent_id, @@ -533,7 +519,6 @@ async fn persist_created_run( store: &Database, persisted: &Persisted, workflow_source: &str, - submitted_manifest_bytes: Option<&[u8]>, accepted_definition: Option<&RunDefinition>, explicit_title: Option, parent_id: Option, @@ -547,8 +532,7 @@ async fn persist_created_run( let spec_bytes = serde_json::to_vec(record) .map_err(|err| Error::engine_with_source("failed to serialize run spec", err))?; let blob_store = store.blobs(); - let (manifest_blob, definition_blob, spec_blob) = tokio::try_join!( - write_optional_blob(&blob_store, submitted_manifest_bytes), + let (definition_blob, spec_blob) = tokio::try_join!( write_optional_blob(&blob_store, definition_bytes.as_deref()), async { blob_store.write(&spec_bytes).await.map_err(store_error) }, )?; @@ -575,7 +559,6 @@ async fn persist_created_run( target: record.target.clone(), automation: record.automation.clone(), provenance: record.provenance.clone(), - manifest_blob, spec_blob: Some(spec_blob), git: record.git.clone(), fork_source_ref: record.fork_source_ref.clone(), @@ -1624,27 +1607,26 @@ mod tests { workflow_source: None, }; let request = CreateRunInput { - workflow: WorkflowInput::DotSource { + workflow: WorkflowInput::DotSource { source: MINIMAL_DOT.to_string(), base_dir: None, }, - settings: test_default_settings(), - vars: HashMap::new(), - cwd: dir.path().to_path_buf(), - workflow_slug: Some("request-slug".to_string()), - workflow_path: None, - workflow_bundle: None, - target: None, - submitted_manifest_bytes: Some(b"submitted manifest".to_vec()), - run_id: Some(fixtures::RUN_1), - title: Some("Assembled run".to_string()), - automation: Some(automation.clone()), - git: None, - fork_source_ref: None, - parent_id: Some(fixtures::RUN_2), - provenance: test_support::test_run_provenance(), + settings: test_default_settings(), + vars: HashMap::new(), + cwd: dir.path().to_path_buf(), + workflow_slug: Some("request-slug".to_string()), + workflow_path: None, + workflow_bundle: None, + target: None, + run_id: Some(fixtures::RUN_1), + title: Some("Assembled run".to_string()), + automation: Some(automation.clone()), + git: None, + fork_source_ref: None, + parent_id: Some(fixtures::RUN_2), + provenance: test_support::test_run_provenance(), configured_providers: test_provider_ids(), - web_url: Some("https://fabro.test/runs/1".to_string()), + web_url: Some("https://fabro.test/runs/1".to_string()), }; let catalog = test_catalog(); let resolved_run_id = fixtures::RUN_64; @@ -1662,10 +1644,6 @@ mod tests { .root() ); assert_eq!(input.workflow_slug(), Some("request-slug")); - assert_eq!( - input.submitted_manifest_bytes(), - Some(b"submitted manifest".as_slice()) - ); assert_eq!(input.automation(), Some(&automation)); assert_eq!( input.materialized().settings().run.model.name.as_deref(), @@ -1737,19 +1715,18 @@ mod tests { let materialized = materialize_create_run(compiled, test_catalog().as_ref()).unwrap(); let input = assemble_create_run_persistence_input(materialized, CreateRunPersistenceMetadata { - run_id: fixtures::RUN_1, - storage_root: PathBuf::from("/tmp/storage"), - workflow_slug: None, + run_id: fixtures::RUN_1, + storage_root: PathBuf::from("/tmp/storage"), + workflow_slug: None, workflow_version_id: None, - target: None, - submitted_manifest_bytes: None, - title: None, - automation: None, - git: None, - fork_source_ref: None, - parent_id: None, - provenance: test_support::test_run_provenance(), - web_url: None, + target: None, + title: None, + automation: None, + git: None, + fork_source_ref: None, + parent_id: None, + provenance: test_support::test_run_provenance(), + web_url: None, }); let definition = input .definition() @@ -1771,24 +1748,23 @@ mod tests { workflow_source: None, }; let request = CreateRunInput { - workflow: WorkflowInput::Path(dot_path.clone()), - settings: test_default_settings(), - vars: HashMap::new(), - cwd: dir.path().to_path_buf(), - workflow_slug: Some("compiled-slug".to_string()), - workflow_path: None, - workflow_bundle: None, - target: None, - submitted_manifest_bytes: Some(b"submitted manifest".to_vec()), - run_id: Some(fixtures::RUN_2), - title: Some("Compiled run".to_string()), - automation: Some(automation.clone()), - git: None, - fork_source_ref: None, - parent_id: None, - provenance: test_support::test_run_provenance(), + workflow: WorkflowInput::Path(dot_path.clone()), + settings: test_default_settings(), + vars: HashMap::new(), + cwd: dir.path().to_path_buf(), + workflow_slug: Some("compiled-slug".to_string()), + workflow_path: None, + workflow_bundle: None, + target: None, + run_id: Some(fixtures::RUN_2), + title: Some("Compiled run".to_string()), + automation: Some(automation.clone()), + git: None, + fork_source_ref: None, + parent_id: None, + provenance: test_support::test_run_provenance(), configured_providers: test_provider_ids(), - web_url: None, + web_url: None, }; let catalog = test_catalog(); let compiled = compile_create_run(compile_input(&request), Arc::clone(&catalog)).unwrap(); @@ -1829,19 +1805,12 @@ mod tests { Some(compiled_source.as_str()) ); assert_eq!(created.workflow_version_id, Some(workflow_version_id)); - let manifest_blob = created - .manifest_blob - .as_ref() - .expect("submitted manifest should be persisted"); - assert_eq!( - run_store - .read_blob(manifest_blob) - .await - .unwrap() - .expect("submitted manifest blob should exist") - .as_ref(), - b"submitted manifest" + assert!( + events[0].event.to_value().unwrap()["properties"] + .get("manifest_blob") + .is_none() ); + assert!(created.spec_blob.is_some()); } #[tokio::test] @@ -1856,27 +1825,26 @@ mod tests { let err = create( &store, CreateRunInput { - workflow: WorkflowInput::DotSource { + workflow: WorkflowInput::DotSource { source: dot.to_string(), base_dir: None, }, - settings: test_default_settings(), - vars: HashMap::new(), - cwd: dir.path().to_path_buf(), - workflow_slug: None, - workflow_path: None, - workflow_bundle: None, - target: None, - submitted_manifest_bytes: None, - run_id: None, - title: None, - automation: None, - git: None, - fork_source_ref: None, - parent_id: None, - provenance: test_support::test_run_provenance(), + settings: test_default_settings(), + vars: HashMap::new(), + cwd: dir.path().to_path_buf(), + workflow_slug: None, + workflow_path: None, + workflow_bundle: None, + target: None, + run_id: None, + title: None, + automation: None, + git: None, + fork_source_ref: None, + parent_id: None, + provenance: test_support::test_run_provenance(), configured_providers: test_provider_ids(), - web_url: None, + web_url: None, }, storage_root, test_catalog(), @@ -1904,11 +1872,11 @@ mod tests { let created = create( &store, CreateRunInput { - workflow: WorkflowInput::DotSource { + workflow: WorkflowInput::DotSource { source: MINIMAL_DOT.to_string(), base_dir: None, }, - settings: settings_from_run_layer({ + settings: settings_from_run_layer({ let mut metadata = HashMap::new(); metadata.insert("env".to_string(), "test".to_string()); RunLayer { @@ -1929,27 +1897,26 @@ mod tests { ..RunLayer::default() } }), - vars: HashMap::new(), - cwd: dir.path().to_path_buf(), - workflow_slug: Some("slug".to_string()), - workflow_path: None, - workflow_bundle: None, - target: None, - submitted_manifest_bytes: None, - run_id: Some(fixtures::RUN_1), - title: None, - automation: None, - git: Some(fabro_types::GitContext { + vars: HashMap::new(), + cwd: dir.path().to_path_buf(), + workflow_slug: Some("slug".to_string()), + workflow_path: None, + workflow_bundle: None, + target: None, + run_id: Some(fixtures::RUN_1), + title: None, + automation: None, + git: Some(fabro_types::GitContext { origin_url: String::new(), branch: "main".to_string(), sha: None, dirty: fabro_types::DirtyStatus::Clean, }), - fork_source_ref: None, - parent_id: None, - provenance: test_support::test_run_provenance(), + fork_source_ref: None, + parent_id: None, + provenance: test_support::test_run_provenance(), configured_providers: test_provider_ids(), - web_url: None, + web_url: None, }, storage_root.clone(), test_catalog(), @@ -2069,7 +2036,6 @@ mod tests { workflow_path: None, workflow_bundle: None, target: None, - submitted_manifest_bytes: None, run_id: None, title: None, automation: None, @@ -2148,11 +2114,11 @@ mod tests { let created = create( &store, CreateRunInput { - workflow: WorkflowInput::DotSource { + workflow: WorkflowInput::DotSource { source: MINIMAL_DOT.to_string(), base_dir: None, }, - settings: settings_from_run_layer(RunLayer { + settings: settings_from_run_layer(RunLayer { prepare: Some(RunPrepareLayer { steps: vec![PrepareStep { script: None, @@ -2173,22 +2139,21 @@ mod tests { }), ..RunLayer::default() }), - vars: HashMap::new(), - cwd: dir.path().to_path_buf(), - workflow_slug: Some("secret-source".to_string()), - workflow_path: None, - workflow_bundle: None, - target: None, - submitted_manifest_bytes: None, - run_id: Some(fixtures::RUN_1), - title: None, - automation: None, - git: None, - fork_source_ref: None, - parent_id: None, - provenance: test_support::test_run_provenance(), + vars: HashMap::new(), + cwd: dir.path().to_path_buf(), + workflow_slug: Some("secret-source".to_string()), + workflow_path: None, + workflow_bundle: None, + target: None, + run_id: Some(fixtures::RUN_1), + title: None, + automation: None, + git: None, + fork_source_ref: None, + parent_id: None, + provenance: test_support::test_run_provenance(), configured_providers: test_provider_ids(), - web_url: None, + web_url: None, }, storage_root, test_catalog(), @@ -2237,11 +2202,11 @@ mod tests { let created = create( &store, CreateRunInput { - workflow: WorkflowInput::DotSource { + workflow: WorkflowInput::DotSource { source: MINIMAL_DOT.to_string(), base_dir: None, }, - settings: settings_from_run_layer({ + settings: settings_from_run_layer({ RunLayer { working_dir: Some("workspace".to_string()), execution: Some(RunExecutionLayer { @@ -2251,22 +2216,21 @@ mod tests { ..RunLayer::default() } }), - vars: HashMap::new(), - cwd: dir.path().to_path_buf(), - workflow_slug: None, - workflow_path: None, - workflow_bundle: None, - target: None, - submitted_manifest_bytes: None, - run_id: Some(fixtures::RUN_2), - title: None, - automation: None, - git: None, - fork_source_ref: None, - parent_id: None, - provenance: test_support::test_run_provenance(), + vars: HashMap::new(), + cwd: dir.path().to_path_buf(), + workflow_slug: None, + workflow_path: None, + workflow_bundle: None, + target: None, + run_id: Some(fixtures::RUN_2), + title: None, + automation: None, + git: None, + fork_source_ref: None, + parent_id: None, + provenance: test_support::test_run_provenance(), configured_providers: test_provider_ids(), - web_url: None, + web_url: None, }, storage_root, test_catalog(), @@ -2288,27 +2252,26 @@ mod tests { let created = create( &store, CreateRunInput { - workflow: WorkflowInput::DotSource { + workflow: WorkflowInput::DotSource { source: MINIMAL_DOT.to_string(), base_dir: None, }, - settings: test_default_settings(), - vars: HashMap::new(), - cwd: dir.path().to_path_buf(), - workflow_slug: None, - workflow_path: None, - workflow_bundle: None, - target: Some(RunTarget::None {}), - submitted_manifest_bytes: None, - run_id: Some(fixtures::RUN_2), - title: None, - automation: None, - git: None, - fork_source_ref: None, - parent_id: None, - provenance: test_support::test_run_provenance(), + settings: test_default_settings(), + vars: HashMap::new(), + cwd: dir.path().to_path_buf(), + workflow_slug: None, + workflow_path: None, + workflow_bundle: None, + target: Some(RunTarget::None {}), + run_id: Some(fixtures::RUN_2), + title: None, + automation: None, + git: None, + fork_source_ref: None, + parent_id: None, + provenance: test_support::test_run_provenance(), configured_providers: test_provider_ids(), - web_url: None, + web_url: None, }, storage_root, test_catalog(), @@ -2345,29 +2308,28 @@ mod tests { let created = create( &store, CreateRunInput { - workflow: WorkflowInput::DotSource { + workflow: WorkflowInput::DotSource { source: MINIMAL_DOT.to_string(), base_dir: None, }, - settings: test_default_settings(), - vars: HashMap::new(), - cwd: dir.path().to_path_buf(), - workflow_slug: None, - workflow_path: None, - workflow_bundle: None, - target: Some(RunTarget::Folder { + settings: test_default_settings(), + vars: HashMap::new(), + cwd: dir.path().to_path_buf(), + workflow_slug: None, + workflow_path: None, + workflow_bundle: None, + target: Some(RunTarget::Folder { path: canonical.clone(), }), - submitted_manifest_bytes: None, - run_id: Some(fixtures::RUN_2), - title: None, - automation: None, - git: Some(git.clone()), - fork_source_ref: None, - parent_id: None, - provenance: test_support::test_run_provenance(), + run_id: Some(fixtures::RUN_2), + title: None, + automation: None, + git: Some(git.clone()), + fork_source_ref: None, + parent_id: None, + provenance: test_support::test_run_provenance(), configured_providers: test_provider_ids(), - web_url: None, + web_url: None, }, storage_root, test_catalog(), @@ -2396,32 +2358,31 @@ mod tests { let created = create( &store, CreateRunInput { - workflow: WorkflowInput::DotSource { + workflow: WorkflowInput::DotSource { source: MINIMAL_DOT.to_string(), base_dir: None, }, - settings: dry_run_only_settings(), - vars: HashMap::new(), - cwd: dir.path().to_path_buf(), - workflow_slug: None, - workflow_path: None, - workflow_bundle: None, - target: None, - submitted_manifest_bytes: None, - run_id: Some(fixtures::RUN_2), - title: None, - automation: None, - git: Some(fabro_types::GitContext { + settings: dry_run_only_settings(), + vars: HashMap::new(), + cwd: dir.path().to_path_buf(), + workflow_slug: None, + workflow_path: None, + workflow_bundle: None, + target: None, + run_id: Some(fixtures::RUN_2), + title: None, + automation: None, + git: Some(fabro_types::GitContext { origin_url: "https://github.com/acme/widgets".to_string(), branch: String::new(), sha: None, dirty: fabro_types::DirtyStatus::Clean, }), - fork_source_ref: None, - parent_id: None, - provenance: test_support::test_run_provenance(), + fork_source_ref: None, + parent_id: None, + provenance: test_support::test_run_provenance(), configured_providers: test_provider_ids(), - web_url: None, + web_url: None, }, storage_root, test_catalog(), @@ -2477,27 +2438,26 @@ mod tests { let created = create( store.as_ref(), CreateRunInput { - workflow: WorkflowInput::DotSource { + workflow: WorkflowInput::DotSource { source: MINIMAL_DOT.to_string(), base_dir: None, }, - settings: dry_run_with_storage(&storage_dir), - vars: HashMap::new(), - cwd: dir.path().to_path_buf(), - workflow_slug: Some("slug".to_string()), - workflow_path: None, - workflow_bundle: None, - target: None, - submitted_manifest_bytes: None, - run_id: Some(fixtures::RUN_3), - title: None, - automation: Some(automation.clone()), - git: None, - fork_source_ref: None, - parent_id: None, - provenance: test_support::test_run_provenance(), + settings: dry_run_with_storage(&storage_dir), + vars: HashMap::new(), + cwd: dir.path().to_path_buf(), + workflow_slug: Some("slug".to_string()), + workflow_path: None, + workflow_bundle: None, + target: None, + run_id: Some(fixtures::RUN_3), + title: None, + automation: Some(automation.clone()), + git: None, + fork_source_ref: None, + parent_id: None, + provenance: test_support::test_run_provenance(), configured_providers: test_provider_ids(), - web_url: None, + web_url: None, }, storage_dir.clone(), test_catalog(), @@ -2532,25 +2492,24 @@ mod tests { let created = create( store.as_ref(), CreateRunInput { - workflow: WorkflowInput::DotSource { + workflow: WorkflowInput::DotSource { source: MINIMAL_DOT.to_string(), base_dir: None, }, - settings: dry_run_with_storage(&storage_dir), - vars: HashMap::new(), - cwd: dir.path().to_path_buf(), - workflow_slug: Some("slug".to_string()), - workflow_path: None, - workflow_bundle: None, - target: None, - submitted_manifest_bytes: None, - run_id: Some(fixtures::RUN_64), - title: None, - automation: None, - git: None, - fork_source_ref: None, - parent_id: None, - provenance: fabro_types::RunProvenance { + settings: dry_run_with_storage(&storage_dir), + vars: HashMap::new(), + cwd: dir.path().to_path_buf(), + workflow_slug: Some("slug".to_string()), + workflow_path: None, + workflow_bundle: None, + target: None, + run_id: Some(fixtures::RUN_64), + title: None, + automation: None, + git: None, + fork_source_ref: None, + parent_id: None, + provenance: fabro_types::RunProvenance { server: Some(fabro_types::RunServerProvenance { version: "0.9.0".to_string(), }), @@ -2566,7 +2525,7 @@ mod tests { ), }, configured_providers: test_provider_ids(), - web_url: None, + web_url: None, }, storage_dir, test_catalog(), diff --git a/lib/components/fabro-workflow/src/operations/fork.rs b/lib/components/fabro-workflow/src/operations/fork.rs index d601cc1bc..0c1395bb0 100644 --- a/lib/components/fabro-workflow/src/operations/fork.rs +++ b/lib/components/fabro-workflow/src/operations/fork.rs @@ -170,7 +170,6 @@ async fn persist_forked_run( target: spec.target.clone(), automation: spec.automation.clone(), provenance: spec.provenance.clone(), - manifest_blob: spec.manifest_blob, // Content-addressed, so the forked run reads the source run's // unredacted spec bytes through the same id. spec_blob: spec.spec_blob, @@ -403,7 +402,6 @@ mod tests { })), automation: None, provenance: test_support::test_run_provenance(), - manifest_blob: None, spec_blob: None, git: Some(fabro_types::GitContext { origin_url: "https://github.com/example/repo".to_string(), diff --git a/lib/components/fabro-workflow/src/operations/retry.rs b/lib/components/fabro-workflow/src/operations/retry.rs index 5e276fdf7..b938610e8 100644 --- a/lib/components/fabro-workflow/src/operations/retry.rs +++ b/lib/components/fabro-workflow/src/operations/retry.rs @@ -55,7 +55,6 @@ pub async fn retry_run( source_directory, labels, provenance: _, - manifest_blob, definition_blob, spec_blob, git, @@ -78,7 +77,6 @@ pub async fn retry_run( target, automation, provenance: input.provenance.clone(), - manifest_blob, // Blobs are content-addressed, so the retried run reads the source // run's unredacted spec bytes through the same id. spec_blob, @@ -178,7 +176,6 @@ mod tests { async fn append_created( store: &fabro_store::RunDatabase, run_id: RunId, - manifest_blob: Option, fork_source_ref: Option, ) { let mut settings = WorkflowSettings::default(); @@ -200,7 +197,6 @@ mod tests { target: Some(run_target()), automation: None, provenance: provenance("source-user"), - manifest_blob, spec_blob: None, git: Some(git_context()), fork_source_ref, @@ -265,14 +261,8 @@ mod tests { async fn seed_retryable_failed_source( store: &Database, source_run_id: RunId, - ) -> (Option, Option, ForkSourceRef) { + ) -> (Option, ForkSourceRef) { let source_store = store.create_run(&source_run_id).await.unwrap(); - let manifest_blob = Some( - source_store - .write_blob(br#"{\"manifest\":true}"#) - .await - .unwrap(), - ); let definition_blob = Some( source_store .write_blob(br#"{\"definition\":true}"#) @@ -283,13 +273,7 @@ mod tests { source_run_id: fixtures::RUN_3, checkpoint_sha: "fork-sha".to_string(), }; - append_created( - &source_store, - source_run_id, - manifest_blob, - Some(fork_source_ref.clone()), - ) - .await; + append_created(&source_store, source_run_id, Some(fork_source_ref.clone())).await; event::append_event(&source_store, &source_run_id, &Event::RunSubmitted { definition_blob, }) @@ -358,14 +342,14 @@ mod tests { .await .unwrap(); append_failed(&source_store, source_run_id, FailureReason::WorkflowError).await; - (manifest_blob, definition_blob, fork_source_ref) + (definition_blob, fork_source_ref) } #[tokio::test] async fn retry_creates_fresh_run_from_durable_definition_only() { let store = memory_store(); let source_run_id = fixtures::RUN_1; - let (manifest_blob, definition_blob, fork_source_ref) = + let (definition_blob, fork_source_ref) = seed_retryable_failed_source(&store, source_run_id).await; let source_event_count = store .open_run(&source_run_id) @@ -415,7 +399,6 @@ mod tests { ); assert_eq!(retry_state.spec.git, Some(git_context())); assert_eq!(retry_state.spec.target, Some(run_target())); - assert_eq!(retry_state.spec.manifest_blob, manifest_blob); assert_eq!(retry_state.spec.definition_blob, definition_blob); assert_eq!(retry_state.spec.fork_source_ref, Some(fork_source_ref)); assert_eq!(retry_state.spec.provenance.subject, actor("retry-user")); @@ -468,7 +451,6 @@ mod tests { target: Some(RunTarget::None {}), automation: None, provenance: provenance("source-user"), - manifest_blob: None, spec_blob: None, git: None, fork_source_ref: None, @@ -533,7 +515,6 @@ mod tests { target: Some(target.clone()), automation: None, provenance: provenance("source-user"), - manifest_blob: None, spec_blob: None, git: None, fork_source_ref: None, @@ -575,7 +556,7 @@ mod tests { let store = memory_store(); let source_run_id = fixtures::RUN_1; let source_store = store.create_run(&source_run_id).await.unwrap(); - append_created(&source_store, source_run_id, None, None).await; + append_created(&source_store, source_run_id, None).await; let definition_blob = Some( source_store .write_blob(br#"{\"definition\":true}"#) @@ -619,7 +600,7 @@ mod tests { let active = fixtures::RUN_2; let active_store = store.create_run(&active).await.unwrap(); - append_created(&active_store, active, None, None).await; + append_created(&active_store, active, None).await; event::append_event(&active_store, &active, &Event::RunSubmitted { definition_blob: None, }) @@ -634,7 +615,7 @@ mod tests { let archived = fixtures::RUN_3; let archived_store = store.create_run(&archived).await.unwrap(); - append_created(&archived_store, archived, None, None).await; + append_created(&archived_store, archived, None).await; append_failed(&archived_store, archived, FailureReason::WorkflowError).await; event::append_event(&archived_store, &archived, &Event::RunArchived { actor: None, diff --git a/lib/components/fabro-workflow/src/operations/start.rs b/lib/components/fabro-workflow/src/operations/start.rs index 76e1f9d4c..bbffa9e69 100644 --- a/lib/components/fabro-workflow/src/operations/start.rs +++ b/lib/components/fabro-workflow/src/operations/start.rs @@ -2397,7 +2397,6 @@ mod tests { workflow_path: None, workflow_bundle: None, target, - submitted_manifest_bytes: None, run_id: Some(fixtures::RUN_1), title: None, automation: None, @@ -2973,35 +2972,34 @@ mod tests { crate::operations::create( &store, crate::operations::CreateRunInput { - workflow: crate::operations::WorkflowInput::Bundled( + workflow: crate::operations::WorkflowInput::Bundled( workflow_bundle .workflow(&ManifestPath::from_wire("workflow.fabro").unwrap()) .unwrap() .clone(), ), - settings: settings_from_run_layer(RunLayer { + settings: settings_from_run_layer(RunLayer { execution: Some(RunExecutionLayer { mode: Some(RunMode::DryRun), ..RunExecutionLayer::default() }), ..RunLayer::default() }), - vars: std::collections::HashMap::new(), - cwd: temp.path().to_path_buf(), - workflow_slug: Some("bundle-child".to_string()), - workflow_path: Some(ManifestPath::from_wire("workflow.fabro").unwrap()), - workflow_bundle: Some(workflow_bundle), - target: None, - submitted_manifest_bytes: None, - run_id: Some(fixtures::RUN_1), - title: None, - automation: None, - git: None, - fork_source_ref: None, - parent_id: None, - provenance: test_support::test_run_provenance(), + vars: std::collections::HashMap::new(), + cwd: temp.path().to_path_buf(), + workflow_slug: Some("bundle-child".to_string()), + workflow_path: Some(ManifestPath::from_wire("workflow.fabro").unwrap()), + workflow_bundle: Some(workflow_bundle), + target: None, + run_id: Some(fixtures::RUN_1), + title: None, + automation: None, + git: None, + fork_source_ref: None, + parent_id: None, + provenance: test_support::test_run_provenance(), configured_providers: test_provider_ids(), - web_url: None, + web_url: None, }, storage_root, test_catalog(), diff --git a/lib/components/fabro-workflow/src/operations/timeline.rs b/lib/components/fabro-workflow/src/operations/timeline.rs index ab0383c79..ca903982c 100644 --- a/lib/components/fabro-workflow/src/operations/timeline.rs +++ b/lib/components/fabro-workflow/src/operations/timeline.rs @@ -252,7 +252,6 @@ mod tests { source_directory: None, labels: HashMap::new(), provenance: test_support::test_run_provenance(), - manifest_blob: None, definition_blob: None, spec_blob: None, git: None, diff --git a/lib/components/fabro-workflow/src/pipeline/execute/tests.rs b/lib/components/fabro-workflow/src/pipeline/execute/tests.rs index 21b40844e..4502e3307 100644 --- a/lib/components/fabro-workflow/src/pipeline/execute/tests.rs +++ b/lib/components/fabro-workflow/src/pipeline/execute/tests.rs @@ -172,7 +172,6 @@ fn persisted_workflow(graph: Graph, source: String, run_dir: &Path, run_id: RunI }), labels: HashMap::new(), provenance: test_support::test_run_provenance(), - manifest_blob: None, definition_blob: None, spec_blob: None, fork_source_ref: None, @@ -221,7 +220,6 @@ async fn seed_created_and_starting( target: None, automation: None, provenance: test_support::test_run_provenance(), - manifest_blob: None, spec_blob: None, git: run_options.pre_run_git.clone(), fork_source_ref: run_options.fork_source_ref.clone(), diff --git a/lib/components/fabro-workflow/src/pipeline/finalize.rs b/lib/components/fabro-workflow/src/pipeline/finalize.rs index cf3239db2..901c02def 100644 --- a/lib/components/fabro-workflow/src/pipeline/finalize.rs +++ b/lib/components/fabro-workflow/src/pipeline/finalize.rs @@ -465,7 +465,6 @@ mod tests { target: None, automation: None, provenance: test_support::test_run_provenance(), - manifest_blob: None, spec_blob: None, git: None, fork_source_ref: None, @@ -585,7 +584,6 @@ mod tests { source_directory: None, labels: HashMap::new(), provenance: test_support::test_run_provenance(), - manifest_blob: None, definition_blob: None, spec_blob: None, git: None, diff --git a/lib/components/fabro-workflow/src/pipeline/initialize.rs b/lib/components/fabro-workflow/src/pipeline/initialize.rs index 28450215a..57d19bbb9 100644 --- a/lib/components/fabro-workflow/src/pipeline/initialize.rs +++ b/lib/components/fabro-workflow/src/pipeline/initialize.rs @@ -840,7 +840,6 @@ mod tests { target: None, automation: None, provenance: test_support::test_run_provenance(), - manifest_blob: None, spec_blob: None, git: None, fork_source_ref, @@ -1008,7 +1007,6 @@ mod tests { }), labels: HashMap::new(), provenance: test_support::test_run_provenance(), - manifest_blob: None, definition_blob: None, spec_blob: None, fork_source_ref, diff --git a/lib/components/fabro-workflow/src/pipeline/persist.rs b/lib/components/fabro-workflow/src/pipeline/persist.rs index c8162479c..78116fb5d 100644 --- a/lib/components/fabro-workflow/src/pipeline/persist.rs +++ b/lib/components/fabro-workflow/src/pipeline/persist.rs @@ -78,7 +78,6 @@ async fn executable_run_spec( // folded source as a compatibility fallback. spec.run_id = folded.run_id; spec.provenance = folded.provenance; - spec.manifest_blob = folded.manifest_blob; spec.definition_blob = folded.definition_blob; spec.spec_blob = folded.spec_blob; spec.fork_source_ref = folded.fork_source_ref; @@ -186,7 +185,6 @@ mod tests { ("team".to_string(), "workflow".to_string()), ]), provenance: test_support::test_run_provenance(), - manifest_blob: None, definition_blob: None, spec_blob: None, fork_source_ref: None, @@ -226,7 +224,6 @@ mod tests { target: record.target.clone(), automation: record.automation.clone(), provenance: record.provenance.clone(), - manifest_blob: None, spec_blob, git: record.git.clone(), fork_source_ref: record.fork_source_ref.clone(), diff --git a/lib/components/fabro-workflow/src/pipeline/pull_request.rs b/lib/components/fabro-workflow/src/pipeline/pull_request.rs index ffcf4e88e..32c46241f 100644 --- a/lib/components/fabro-workflow/src/pipeline/pull_request.rs +++ b/lib/components/fabro-workflow/src/pipeline/pull_request.rs @@ -828,7 +828,6 @@ capabilities = { text = true, tools = true, response_format = { json_object = tr source_directory: None, labels: HashMap::new(), provenance: test_support::test_run_provenance(), - manifest_blob: None, definition_blob: None, spec_blob: None, git: None, @@ -1113,7 +1112,6 @@ capabilities = { text = true, tools = true, response_format = { json_object = tr }), labels: HashMap::new(), provenance: test_support::test_run_provenance(), - manifest_blob: None, definition_blob: None, spec_blob: None, fork_source_ref: None, @@ -1131,7 +1129,6 @@ capabilities = { text = true, tools = true, response_format = { json_object = tr target: run_spec.target.clone(), automation: None, provenance: run_spec.provenance.clone(), - manifest_blob: None, spec_blob: None, git: run_spec.git.clone(), fork_source_ref: None, @@ -1186,7 +1183,6 @@ capabilities = { text = true, tools = true, response_format = { json_object = tr }), labels: HashMap::new(), provenance: test_support::test_run_provenance(), - manifest_blob: None, definition_blob: None, spec_blob: None, fork_source_ref: None, @@ -1204,7 +1200,6 @@ capabilities = { text = true, tools = true, response_format = { json_object = tr target: run_spec.target.clone(), automation: None, provenance: run_spec.provenance.clone(), - manifest_blob: None, spec_blob: None, git: run_spec.git.clone(), fork_source_ref: None, @@ -1610,7 +1605,6 @@ capabilities = { text = true, tools = true, response_format = { json_object = tr git: None, labels: HashMap::new(), provenance: test_support::test_run_provenance(), - manifest_blob: None, definition_blob: None, spec_blob: None, fork_source_ref: None, @@ -1628,7 +1622,6 @@ capabilities = { text = true, tools = true, response_format = { json_object = tr target: run_spec.target.clone(), automation: None, provenance: test_support::test_run_provenance(), - manifest_blob: None, spec_blob: None, git: None, fork_source_ref: None, @@ -1834,7 +1827,6 @@ capabilities = { text = true, tools = true, response_format = { json_object = tr git: None, labels: HashMap::new(), provenance: test_support::test_run_provenance(), - manifest_blob: None, definition_blob: None, spec_blob: None, fork_source_ref: None, @@ -1852,7 +1844,6 @@ capabilities = { text = true, tools = true, response_format = { json_object = tr target: None, automation: None, provenance: test_support::test_run_provenance(), - manifest_blob: None, spec_blob: None, git: None, fork_source_ref: None, diff --git a/lib/components/fabro-workflow/src/run_lookup.rs b/lib/components/fabro-workflow/src/run_lookup.rs index 47d6d1a4e..fc25cb9e2 100644 --- a/lib/components/fabro-workflow/src/run_lookup.rs +++ b/lib/components/fabro-workflow/src/run_lookup.rs @@ -503,7 +503,6 @@ mod tests { target: None, automation: None, provenance: run_spec.provenance.clone(), - manifest_blob: None, spec_blob: None, git: run_spec.git.clone(), fork_source_ref: run_spec.fork_source_ref.clone(), diff --git a/lib/components/fabro-workflow/src/runtime_store.rs b/lib/components/fabro-workflow/src/runtime_store.rs index c80121d5a..51d433dad 100644 --- a/lib/components/fabro-workflow/src/runtime_store.rs +++ b/lib/components/fabro-workflow/src/runtime_store.rs @@ -157,7 +157,6 @@ mod tests { target: None, automation: None, provenance: test_support::test_run_provenance(), - manifest_blob: None, spec_blob: None, git: None, fork_source_ref: None, diff --git a/lib/components/fabro-workflow/src/stage_execution.rs b/lib/components/fabro-workflow/src/stage_execution.rs index d479a83d1..325a70bed 100644 --- a/lib/components/fabro-workflow/src/stage_execution.rs +++ b/lib/components/fabro-workflow/src/stage_execution.rs @@ -209,7 +209,6 @@ mod tests { source_directory: None, labels: std::collections::HashMap::new(), provenance: test_support::test_run_provenance(), - manifest_blob: None, definition_blob: None, spec_blob: None, git: None, diff --git a/lib/components/fabro-workflow/src/test_support.rs b/lib/components/fabro-workflow/src/test_support.rs index 571c04799..8429e2c5d 100644 --- a/lib/components/fabro-workflow/src/test_support.rs +++ b/lib/components/fabro-workflow/src/test_support.rs @@ -222,7 +222,6 @@ async fn initialized( system_kind: fabro_types::SystemActorKind::Engine, }, }, - manifest_blob: None, spec_blob: None, git: run_options.pre_run_git.clone(), fork_source_ref: run_options.fork_source_ref.clone(), diff --git a/lib/foundation/fabro-client/src/client.rs b/lib/foundation/fabro-client/src/client.rs index 478362487..73ebce687 100644 --- a/lib/foundation/fabro-client/src/client.rs +++ b/lib/foundation/fabro-client/src/client.rs @@ -698,10 +698,6 @@ impl Client { Ok(SessionEventStream::new(Box::pin(stream))) } - pub async fn create_run_from_manifest(&self, manifest: types::RunManifest) -> Result { - self.submit_create_run(manifest.into()).await - } - /// Retrieves one canonical server-managed environment by ID. pub async fn retrieve_environment(&self, id: &str) -> Result { let response = self @@ -759,12 +755,8 @@ impl Client { } pub async fn create_run_from_intent(&self, intent: types::RunIntent) -> Result { - self.submit_create_run(intent.into()).await - } - - async fn submit_create_run(&self, body: types::CreateRunRequest) -> Result { let response = self - .send_api(|client| async move { client.create_run().body(body.clone()).send().await }) + .send_api(|client| async move { client.create_run().body(intent.clone()).send().await }) .await?; let status = response.into_inner(); Ok(status.id) diff --git a/lib/foundation/fabro-types/src/run.rs b/lib/foundation/fabro-types/src/run.rs index ee1718505..7d445eea1 100644 --- a/lib/foundation/fabro-types/src/run.rs +++ b/lib/foundation/fabro-types/src/run.rs @@ -79,8 +79,6 @@ pub struct RunSpec { pub labels: HashMap, pub provenance: RunProvenance, #[serde(default, skip_serializing_if = "Option::is_none")] - pub manifest_blob: Option, - #[serde(default, skip_serializing_if = "Option::is_none")] pub definition_blob: Option, /// Unredacted copy of this spec in the blob store. Stored events pass /// through secret redaction, so the spec folded from them is display diff --git a/lib/foundation/fabro-types/src/run_event/mod.rs b/lib/foundation/fabro-types/src/run_event/mod.rs index aef47d827..fc87943cc 100644 --- a/lib/foundation/fabro-types/src/run_event/mod.rs +++ b/lib/foundation/fabro-types/src/run_event/mod.rs @@ -1364,7 +1364,7 @@ mod tests { } #[test] - fn run_created_round_trip_preserves_manifest_blob() { + fn run_created_reads_retired_manifest_blob_without_serializing_it() { let line = json!({ "id": "evt_created_blob", "ts": "2026-04-04T12:00:00.000Z", @@ -1380,13 +1380,25 @@ mod tests { } }); - let parsed = RunEvent::from_value(line.clone()).unwrap(); - let serialized = parsed.to_value().unwrap(); - - assert_eq!( - serialized["properties"]["manifest_blob"], - line["properties"]["manifest_blob"] - ); + for legacy_hash in [ + Some(line["properties"]["manifest_blob"].clone()), + Some(serde_json::Value::Null), + None, + ] { + let mut historical = line.clone(); + historical["properties"] + .as_object_mut() + .unwrap() + .remove("manifest_blob"); + if let Some(hash) = legacy_hash { + historical["properties"]["manifest_blob"] = hash; + } + let parsed = RunEvent::from_value(historical).unwrap(); + assert!(matches!(parsed.body, EventBody::RunCreated(_))); + let serialized = parsed.to_value().unwrap(); + assert_eq!(serialized["event"], "run.created"); + assert!(serialized["properties"].get("manifest_blob").is_none()); + } } #[test] diff --git a/lib/foundation/fabro-types/src/run_event/run.rs b/lib/foundation/fabro-types/src/run_event/run.rs index 5585111bc..33b60f4aa 100644 --- a/lib/foundation/fabro-types/src/run_event/run.rs +++ b/lib/foundation/fabro-types/src/run_event/run.rs @@ -31,8 +31,6 @@ pub struct RunCreatedProps { #[serde(default, skip_serializing_if = "Option::is_none")] pub automation: Option, pub provenance: RunProvenance, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub manifest_blob: Option, /// Unredacted copy of the run spec in the blob store. The settings and /// graph on this event are redacted at the sink; execution loads the /// spec from this blob instead. diff --git a/lib/foundation/fabro-types/src/test_support.rs b/lib/foundation/fabro-types/src/test_support.rs index c32a989c3..30eebd9e3 100644 --- a/lib/foundation/fabro-types/src/test_support.rs +++ b/lib/foundation/fabro-types/src/test_support.rs @@ -50,7 +50,6 @@ pub fn test_run_spec() -> RunSpec { source_directory: None, labels: HashMap::new(), provenance: test_run_provenance(), - manifest_blob: None, definition_blob: None, spec_blob: None, git: None, diff --git a/lib/foundation/fabro-types/tests/run_event_serde.rs b/lib/foundation/fabro-types/tests/run_event_serde.rs index 07dabaa3a..bbbfe4a56 100644 --- a/lib/foundation/fabro-types/tests/run_event_serde.rs +++ b/lib/foundation/fabro-types/tests/run_event_serde.rs @@ -48,7 +48,6 @@ fn run_created_props_round_trip_templated_settings() { })), }), provenance: test_run_provenance(), - manifest_blob: None, spec_blob: None, git: Some(GitContext { origin_url: "https://github.com/fabro-sh/fabro.git".to_string(), @@ -124,7 +123,6 @@ fn run_created_props_omits_web_url_when_absent() { target: None, automation: None, provenance: test_run_provenance(), - manifest_blob: None, spec_blob: None, git: None, fork_source_ref: None, diff --git a/lib/foundation/fabro-types/tests/run_spec_serde.rs b/lib/foundation/fabro-types/tests/run_spec_serde.rs index fd2088770..d930680fa 100644 --- a/lib/foundation/fabro-types/tests/run_spec_serde.rs +++ b/lib/foundation/fabro-types/tests/run_spec_serde.rs @@ -7,7 +7,7 @@ use fabro_types::settings::run::RunGoal; use fabro_types::test_support::{test_run_provenance, test_workflow_version_id}; use fabro_types::{ AutomationRef, GitRunTarget, ResolvedAutomationGitWorkflowSource, RunTarget, WorkflowSettings, - fixtures, + fixtures, test_support, }; fn templated_settings() -> WorkflowSettings { @@ -46,7 +46,6 @@ fn run_spec_round_trips_templated_settings() { source_directory: Some("/Users/client/project".to_string()), labels: HashMap::from([("team".to_string(), "platform".to_string())]), provenance: test_run_provenance(), - manifest_blob: None, definition_blob: None, spec_blob: None, git: Some(GitContext { @@ -118,3 +117,28 @@ fn run_spec_defaults_automation_for_legacy_specs() { let round_trip = serde_json::to_value(&record).expect("record should serialize"); assert!(round_trip.get("workflow_version_id").is_none()); } + +#[test] +fn historical_run_spec_ignores_retired_manifest_blob() { + let spec = test_support::test_run_spec(); + let value = serde_json::to_value(spec).unwrap(); + for hash in [ + Some(serde_json::json!(fabro_types::BlobHash::new( + b"old manifest" + ))), + Some(serde_json::Value::Null), + None, + ] { + let mut historical = value.clone(); + if let Some(hash) = hash { + historical["manifest_blob"] = hash; + } + let decoded: RunSpec = serde_json::from_value(historical).unwrap(); + assert!( + serde_json::to_value(decoded) + .unwrap() + .get("manifest_blob") + .is_none() + ); + } +} diff --git a/lib/packages/fabro-api-client/src/.openapi-generator/FILES b/lib/packages/fabro-api-client/src/.openapi-generator/FILES index 6cdbe44dc..d99912a2f 100644 --- a/lib/packages/fabro-api-client/src/.openapi-generator/FILES +++ b/lib/packages/fabro-api-client/src/.openapi-generator/FILES @@ -123,7 +123,6 @@ models/create-completion-request.ts models/create-environment-request.ts models/create-mcp-server-request.ts models/create-run-pull-request-request.ts -models/create-run-request.ts models/create-run-session-request.ts models/create-secret-request.ts models/create-variable-request.ts diff --git a/lib/packages/fabro-api-client/src/api/runs-api.ts b/lib/packages/fabro-api-client/src/api/runs-api.ts index 8703a61cf..c5fc8411a 100644 --- a/lib/packages/fabro-api-client/src/api/runs-api.ts +++ b/lib/packages/fabro-api-client/src/api/runs-api.ts @@ -36,8 +36,6 @@ import type { CloseRunPullRequestResponse } from '../models'; // @ts-ignore import type { CreateRunPullRequestRequest } from '../models'; // @ts-ignore -import type { CreateRunRequest } from '../models'; -// @ts-ignore import type { DeleteRunResponse } from '../models'; // @ts-ignore import type { DenyRunRequest } from '../models'; @@ -72,6 +70,8 @@ import type { RewindResponse } from '../models'; // @ts-ignore import type { Run } from '../models'; // @ts-ignore +import type { RunIntent } from '../models'; +// @ts-ignore import type { RunManifest } from '../models'; // @ts-ignore import type { StartRunRequest } from '../models'; @@ -372,15 +372,15 @@ export const RunsApiAxiosParamCreator = function (configuration?: Configuration) }; }, /** - * Creates a new workflow run in `submitted` status from either a self-contained legacy manifest or an immutable workflow-version intent. Creation does not start or schedule the run. Failures return the standard error body. The intent lane responds `404` (`workflow_version_not_found`, `environment_not_found`), `422` (`run_intent_invalid`, `target_invalid`, `target_environment_unsupported`, `pull_request_environment_unsupported`, `workflow_version_unusable`, `run_compile_invalid`), `503` (`integration_unavailable`), or `500` (`workflow_version_store_error`, `credential_store_error`, `variable_store_error`, `run_persistence_failed`). + * Creates a new workflow run in `submitted` status from an immutable workflow-version intent. Creation does not start or schedule the run. Failures return the standard error body. The endpoint responds `404` (`workflow_version_not_found`, `environment_not_found`), `422` (`run_intent_invalid`, `target_invalid`, `target_environment_unsupported`, `pull_request_environment_unsupported`, `workflow_version_unusable`, `run_compile_invalid`), `503` (`integration_unavailable`), or `500` (`workflow_version_store_error`, `credential_store_error`, `variable_store_error`, `run_persistence_failed`). * @summary Create Run - * @param {CreateRunRequest} createRunRequest + * @param {RunIntent} runIntent * @param {*} [options] Override http request option. * @throws {RequiredError} */ - createRun: async (createRunRequest: CreateRunRequest, options: RawAxiosRequestConfig = {}): Promise => { - // verify required parameter 'createRunRequest' is not null or undefined - assertParamExists('createRun', 'createRunRequest', createRunRequest) + createRun: async (runIntent: RunIntent, options: RawAxiosRequestConfig = {}): Promise => { + // verify required parameter 'runIntent' is not null or undefined + assertParamExists('createRun', 'runIntent', runIntent) const localVarPath = `/api/v1/runs`; // use dummy base URL string because the URL constructor only accepts absolute URLs. const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); @@ -405,7 +405,7 @@ export const RunsApiAxiosParamCreator = function (configuration?: Configuration) setSearchParams(localVarUrlObj, localVarQueryParameter); let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; - localVarRequestOptions.data = serializeDataIfNeeded(createRunRequest, localVarRequestOptions, configuration) + localVarRequestOptions.data = serializeDataIfNeeded(runIntent, localVarRequestOptions, configuration) return { url: toPathString(localVarUrlObj), @@ -1677,14 +1677,14 @@ export const RunsApiFp = function(configuration?: Configuration) { return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath); }, /** - * Creates a new workflow run in `submitted` status from either a self-contained legacy manifest or an immutable workflow-version intent. Creation does not start or schedule the run. Failures return the standard error body. The intent lane responds `404` (`workflow_version_not_found`, `environment_not_found`), `422` (`run_intent_invalid`, `target_invalid`, `target_environment_unsupported`, `pull_request_environment_unsupported`, `workflow_version_unusable`, `run_compile_invalid`), `503` (`integration_unavailable`), or `500` (`workflow_version_store_error`, `credential_store_error`, `variable_store_error`, `run_persistence_failed`). + * Creates a new workflow run in `submitted` status from an immutable workflow-version intent. Creation does not start or schedule the run. Failures return the standard error body. The endpoint responds `404` (`workflow_version_not_found`, `environment_not_found`), `422` (`run_intent_invalid`, `target_invalid`, `target_environment_unsupported`, `pull_request_environment_unsupported`, `workflow_version_unusable`, `run_compile_invalid`), `503` (`integration_unavailable`), or `500` (`workflow_version_store_error`, `credential_store_error`, `variable_store_error`, `run_persistence_failed`). * @summary Create Run - * @param {CreateRunRequest} createRunRequest + * @param {RunIntent} runIntent * @param {*} [options] Override http request option. * @throws {RequiredError} */ - async createRun(createRunRequest: CreateRunRequest, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { - const localVarAxiosArgs = await localVarAxiosParamCreator.createRun(createRunRequest, options); + async createRun(runIntent: RunIntent, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + const localVarAxiosArgs = await localVarAxiosParamCreator.createRun(runIntent, options); const localVarOperationServerIndex = configuration?.serverIndex ?? 0; const localVarOperationServerBasePath = operationServerMap['RunsApi.createRun']?.[localVarOperationServerIndex]?.url; return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath); @@ -2137,14 +2137,14 @@ export const RunsApiFactory = function (configuration?: Configuration, basePath? return localVarFp.closeRunPullRequest(id, options).then((request) => request(axios, basePath)); }, /** - * Creates a new workflow run in `submitted` status from either a self-contained legacy manifest or an immutable workflow-version intent. Creation does not start or schedule the run. Failures return the standard error body. The intent lane responds `404` (`workflow_version_not_found`, `environment_not_found`), `422` (`run_intent_invalid`, `target_invalid`, `target_environment_unsupported`, `pull_request_environment_unsupported`, `workflow_version_unusable`, `run_compile_invalid`), `503` (`integration_unavailable`), or `500` (`workflow_version_store_error`, `credential_store_error`, `variable_store_error`, `run_persistence_failed`). + * Creates a new workflow run in `submitted` status from an immutable workflow-version intent. Creation does not start or schedule the run. Failures return the standard error body. The endpoint responds `404` (`workflow_version_not_found`, `environment_not_found`), `422` (`run_intent_invalid`, `target_invalid`, `target_environment_unsupported`, `pull_request_environment_unsupported`, `workflow_version_unusable`, `run_compile_invalid`), `503` (`integration_unavailable`), or `500` (`workflow_version_store_error`, `credential_store_error`, `variable_store_error`, `run_persistence_failed`). * @summary Create Run - * @param {CreateRunRequest} createRunRequest + * @param {RunIntent} runIntent * @param {*} [options] Override http request option. * @throws {RequiredError} */ - createRun(createRunRequest: CreateRunRequest, options?: RawAxiosRequestConfig): AxiosPromise { - return localVarFp.createRun(createRunRequest, options).then((request) => request(axios, basePath)); + createRun(runIntent: RunIntent, options?: RawAxiosRequestConfig): AxiosPromise { + return localVarFp.createRun(runIntent, options).then((request) => request(axios, basePath)); }, /** * Durably requests creation of a pull request for a completed run. The server generates the pull request content and creates the GitHub pull request after this request returns. Poll the URL in the Location response header until the creation succeeds or fails. If a creation is already pending for the run, the response returns that creation unchanged; any different `model` or `force` values in the new request are ignored. @@ -2518,14 +2518,14 @@ export class RunsApi extends BaseAPI { } /** - * Creates a new workflow run in `submitted` status from either a self-contained legacy manifest or an immutable workflow-version intent. Creation does not start or schedule the run. Failures return the standard error body. The intent lane responds `404` (`workflow_version_not_found`, `environment_not_found`), `422` (`run_intent_invalid`, `target_invalid`, `target_environment_unsupported`, `pull_request_environment_unsupported`, `workflow_version_unusable`, `run_compile_invalid`), `503` (`integration_unavailable`), or `500` (`workflow_version_store_error`, `credential_store_error`, `variable_store_error`, `run_persistence_failed`). + * Creates a new workflow run in `submitted` status from an immutable workflow-version intent. Creation does not start or schedule the run. Failures return the standard error body. The endpoint responds `404` (`workflow_version_not_found`, `environment_not_found`), `422` (`run_intent_invalid`, `target_invalid`, `target_environment_unsupported`, `pull_request_environment_unsupported`, `workflow_version_unusable`, `run_compile_invalid`), `503` (`integration_unavailable`), or `500` (`workflow_version_store_error`, `credential_store_error`, `variable_store_error`, `run_persistence_failed`). * @summary Create Run - * @param {CreateRunRequest} createRunRequest + * @param {RunIntent} runIntent * @param {*} [options] Override http request option. * @throws {RequiredError} */ - public createRun(createRunRequest: CreateRunRequest, options?: RawAxiosRequestConfig) { - return RunsApiFp(this.configuration).createRun(createRunRequest, options).then((request) => request(this.axios, this.basePath)); + public createRun(runIntent: RunIntent, options?: RawAxiosRequestConfig) { + return RunsApiFp(this.configuration).createRun(runIntent, options).then((request) => request(this.axios, this.basePath)); } /** diff --git a/lib/packages/fabro-api-client/src/models/create-run-request.ts b/lib/packages/fabro-api-client/src/models/create-run-request.ts deleted file mode 100644 index 376302b5a..000000000 --- a/lib/packages/fabro-api-client/src/models/create-run-request.ts +++ /dev/null @@ -1,42 +0,0 @@ -/* tslint:disable */ -/* eslint-disable */ -/** - * Fabro Run API - * HTTP API for managing Fabro workflow run executions. - * - * The version of the OpenAPI document: 0.2.0 - * - * - * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). - * https://openapi-generator.tech - * Do not edit the class manually. - */ - - -// May contain unused imports in some cases -// @ts-ignore -import type { GitContext } from './git-context'; -// May contain unused imports in some cases -// @ts-ignore -import type { ManifestConfig } from './manifest-config'; -// May contain unused imports in some cases -// @ts-ignore -import type { ManifestWorkflow } from './manifest-workflow'; -// May contain unused imports in some cases -// @ts-ignore -import type { RunIntent } from './run-intent'; -// May contain unused imports in some cases -// @ts-ignore -import type { RunIntentArgs } from './run-intent-args'; -// May contain unused imports in some cases -// @ts-ignore -import type { RunManifest } from './run-manifest'; -// May contain unused imports in some cases -// @ts-ignore -import type { RunTarget } from './run-target'; - -/** - * @type CreateRunRequest - * Transitional create body used while callers migrate independently from self-contained manifests to immutable workflow-version intents. - */ -export type CreateRunRequest = RunIntent | RunManifest; diff --git a/lib/packages/fabro-api-client/src/models/index.ts b/lib/packages/fabro-api-client/src/models/index.ts index 2fbea26e0..6281b70ac 100644 --- a/lib/packages/fabro-api-client/src/models/index.ts +++ b/lib/packages/fabro-api-client/src/models/index.ts @@ -94,7 +94,6 @@ export * from './create-completion-request'; export * from './create-environment-request'; export * from './create-mcp-server-request'; export * from './create-run-pull-request-request'; -export * from './create-run-request'; export * from './create-run-session-request'; export * from './create-secret-request'; export * from './create-variable-request'; diff --git a/lib/packages/fabro-api-client/src/models/run-spec.ts b/lib/packages/fabro-api-client/src/models/run-spec.ts index 2907d939d..6c05283aa 100644 --- a/lib/packages/fabro-api-client/src/models/run-spec.ts +++ b/lib/packages/fabro-api-client/src/models/run-spec.ts @@ -50,7 +50,6 @@ export interface RunSpec { 'source_directory'?: string | null; 'labels'?: { [key: string]: string; }; 'provenance': RunProvenance; - 'manifest_blob'?: string | null; 'definition_blob'?: string | null; 'spec_blob'?: string | null; 'git'?: GitContext | null;