mirror of
https://github.com/fabro-sh/fabro.git
synced 2026-10-01 02:04:24 +00:00
Retire legacy manifest run creation
This commit is contained in:
parent
05ebd0fd1b
commit
ea11538038
67 changed files with 1109 additions and 2028 deletions
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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<serde_json::Value>,
|
||||
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<String, String> {
|
||||
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<String> {
|
||||
vec![context.test_run_label(), context.test_case_label()]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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<ManifestPath, ProjectSettingsPathError>,
|
||||
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<EnvironmentLayer>,
|
||||
pub(crate) server_mcp_catalog: HashMap<String, McpServerSettings>,
|
||||
pub(crate) settings_input: RunCompilerSettingsInput,
|
||||
pub(crate) user_toml: Vec<String>,
|
||||
pub(crate) workflow_layer: Option<SettingsLayer>,
|
||||
pub(crate) run_overrides: Option<RunLayer>,
|
||||
pub(crate) cli_overrides: Option<CliLayer>,
|
||||
pub(crate) input_overrides: HashMap<String, toml::Value>,
|
||||
pub(crate) inline_goal_override: Option<String>,
|
||||
pub(crate) run_id: Option<RunId>,
|
||||
|
|
@ -97,55 +78,38 @@ pub(crate) struct RawRunCompilerInput {
|
|||
pub(crate) target: Option<RunTarget>,
|
||||
pub(crate) provenance: RunProvenance,
|
||||
pub(crate) web_url: Option<String>,
|
||||
pub(crate) submitted_manifest_bytes: Option<Vec<u8>>,
|
||||
pub(crate) automation: Option<AutomationRef>,
|
||||
}
|
||||
|
||||
/// 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<ProjectSettingsSource>,
|
||||
},
|
||||
Admitted {
|
||||
workflow_layer: Option<Box<SettingsLayer>>,
|
||||
},
|
||||
}
|
||||
|
||||
/// 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<SettingsLayer>,
|
||||
project_layers: Vec<SettingsLayer>,
|
||||
user_toml: Vec<String>,
|
||||
cwd: PathBuf,
|
||||
server_run_defaults: RunLayer,
|
||||
server_environment_defaults: MergeMap<EnvironmentLayer>,
|
||||
server_mcp_catalog: HashMap<String, McpServerSettings>,
|
||||
run_overrides: Option<RunLayer>,
|
||||
cli_overrides: Option<CliLayer>,
|
||||
input_overrides: HashMap<String, toml::Value>,
|
||||
inline_goal_override: Option<String>,
|
||||
metadata: RunMetadata,
|
||||
}
|
||||
|
||||
struct RunMetadata {
|
||||
run_id: Option<RunId>,
|
||||
storage_root: PathBuf,
|
||||
workflow_slug: Option<String>,
|
||||
run_id: Option<RunId>,
|
||||
storage_root: PathBuf,
|
||||
workflow_slug: Option<String>,
|
||||
workflow_version_id: Option<WorkflowVersionId>,
|
||||
target: Option<RunTarget>,
|
||||
submitted_manifest_bytes: Option<Vec<u8>>,
|
||||
title: Option<String>,
|
||||
automation: Option<AutomationRef>,
|
||||
git: Option<GitContext>,
|
||||
parent_id: Option<RunId>,
|
||||
provenance: RunProvenance,
|
||||
web_url: Option<String>,
|
||||
target: Option<RunTarget>,
|
||||
title: Option<String>,
|
||||
automation: Option<AutomationRef>,
|
||||
git: Option<GitContext>,
|
||||
parent_id: Option<RunId>,
|
||||
provenance: RunProvenance,
|
||||
web_url: Option<String>,
|
||||
}
|
||||
|
||||
/// 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<RunId>,
|
||||
parent_id: Option<RunId>,
|
||||
title: Option<String>,
|
||||
) -> 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<RunId> {
|
||||
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<T> = std::result::Result<T, RunCompilerError>;
|
||||
|
||||
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<NormalizedRun> {
|
||||
let RawRunCompilerInput {
|
||||
workflow_bundle,
|
||||
|
|
@ -330,10 +263,8 @@ pub(crate) fn normalize_source(input: RawRunCompilerInput) -> Result<NormalizedR
|
|||
server_run_defaults,
|
||||
server_environment_defaults,
|
||||
server_mcp_catalog,
|
||||
settings_input,
|
||||
user_toml,
|
||||
workflow_layer,
|
||||
run_overrides,
|
||||
cli_overrides,
|
||||
input_overrides,
|
||||
inline_goal_override,
|
||||
run_id,
|
||||
|
|
@ -346,7 +277,6 @@ pub(crate) fn normalize_source(input: RawRunCompilerInput) -> Result<NormalizedR
|
|||
target,
|
||||
provenance,
|
||||
web_url,
|
||||
submitted_manifest_bytes,
|
||||
automation,
|
||||
} = input;
|
||||
let mut workflow = workflow_bundle
|
||||
|
|
@ -357,54 +287,16 @@ pub(crate) fn normalize_source(input: RawRunCompilerInput) -> Result<NormalizedR
|
|||
})?;
|
||||
workflow.path = entrypoint.clone();
|
||||
|
||||
let (workflow_layer, project_layers) = match settings_input {
|
||||
RunCompilerSettingsInput::LegacyManifest { project_settings } => {
|
||||
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::<Result<Vec<_>>>()?;
|
||||
(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<NormalizedR
|
|||
workflow_slug,
|
||||
workflow_version_id,
|
||||
target,
|
||||
submitted_manifest_bytes,
|
||||
title,
|
||||
automation,
|
||||
git,
|
||||
|
|
@ -432,14 +323,11 @@ pub(crate) fn layer_settings(normalized: NormalizedRun) -> Result<LayeredRun> {
|
|||
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<LayeredRun> {
|
|||
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<PinnedRun
|
|||
}
|
||||
|
||||
/// Stage four: purely assemble the complete persistence input. Every durable
|
||||
/// field — run id, submitted source bytes, automation reference — is set here
|
||||
/// field — run id, captured definition, automation reference — is set here
|
||||
/// once; nothing mutates the result afterwards.
|
||||
pub(crate) fn assemble_run(pinned: PinnedRun) -> 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::<SettingsLayer>(
|
||||
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
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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::<RunIntent>(&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::<RunIntent>(&body) {
|
||||
Ok(intent) => intent,
|
||||
Err(error) => return create_run_parse_error(&body, &error),
|
||||
};
|
||||
let req = match serde_json::from_slice::<RunManifest>(&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::<serde_json::Value>(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::<IgnoredAny>(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<AppState>,
|
||||
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<u8>,
|
||||
/// Run ID preallocated by server-side automation code, never supplied by
|
||||
/// an HTTP create body.
|
||||
pub(crate) explicit_run_id: Option<RunId>,
|
||||
pub(crate) explicit_title_supplied: bool,
|
||||
pub(crate) actor: Principal,
|
||||
pub(crate) headers: HeaderMap,
|
||||
pub(crate) automation: Option<AutomationRef>,
|
||||
/// Trusted canonical target supplied by an internal manifest producer.
|
||||
/// Public legacy manifest requests always leave this absent.
|
||||
pub(crate) target: Option<RunTarget>,
|
||||
}
|
||||
|
||||
struct ManifestRunCompilerAdapter {
|
||||
workflow_bundle: WorkflowBundle,
|
||||
entrypoint: ManifestPath,
|
||||
cwd: PathBuf,
|
||||
project_settings: Vec<ProjectSettingsSource>,
|
||||
user_toml: Vec<String>,
|
||||
run_overrides: Option<RunLayer>,
|
||||
cli_overrides: Option<CliLayer>,
|
||||
input_overrides: HashMap<String, toml::Value>,
|
||||
inline_goal_override: Option<String>,
|
||||
}
|
||||
|
||||
fn adapt_manifest_source_for_run_compiler(
|
||||
manifest: &RunManifest,
|
||||
) -> anyhow::Result<ManifestRunCompilerAdapter> {
|
||||
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<ManifestPath, ProjectSettingsPathError> {
|
||||
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<RunId>,
|
||||
parent_id: Option<RunId>,
|
||||
title: Option<String>,
|
||||
}
|
||||
|
||||
fn manifest_run_identity(
|
||||
manifest: &RunManifest,
|
||||
explicit_run_id: Option<RunId>,
|
||||
) -> anyhow::Result<ManifestRunIdentity> {
|
||||
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::<RunId>)
|
||||
.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<AppState>,
|
||||
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<AppState>,
|
||||
run_id: RunId,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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<dyn ObjectStore>,
|
||||
|
|
@ -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<Body> {
|
||||
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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<Body> {
|
||||
|
|
@ -43,12 +43,15 @@ fn completion_request_with_model(stream: bool, model: &str) -> Request<Body> {
|
|||
|
||||
#[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]
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
|
|
|
|||
|
|
@ -872,7 +872,6 @@ fn projection_from_created(event: &EventEnvelope) -> Result<RunProjection> {
|
|||
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"]
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -43,8 +43,6 @@ pub enum Event {
|
|||
automation: Option<AutomationRef>,
|
||||
provenance: RunProvenance,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
manifest_blob: Option<BlobHash>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
spec_blob: Option<BlobHash>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
git: Option<GitContext>,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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<String, String>,
|
||||
pub cwd: PathBuf,
|
||||
pub workflow_slug: Option<String>,
|
||||
pub workflow_path: Option<ManifestPath>,
|
||||
pub workflow_bundle: Option<WorkflowBundle>,
|
||||
pub target: Option<RunTarget>,
|
||||
pub submitted_manifest_bytes: Option<Vec<u8>>,
|
||||
pub run_id: Option<RunId>,
|
||||
pub title: Option<String>,
|
||||
pub automation: Option<AutomationRef>,
|
||||
pub git: Option<GitContext>,
|
||||
pub fork_source_ref: Option<ForkSourceRef>,
|
||||
pub parent_id: Option<RunId>,
|
||||
pub provenance: RunProvenance,
|
||||
pub vars: HashMap<String, String>,
|
||||
pub cwd: PathBuf,
|
||||
pub workflow_slug: Option<String>,
|
||||
pub workflow_path: Option<ManifestPath>,
|
||||
pub workflow_bundle: Option<WorkflowBundle>,
|
||||
pub target: Option<RunTarget>,
|
||||
pub run_id: Option<RunId>,
|
||||
pub title: Option<String>,
|
||||
pub automation: Option<AutomationRef>,
|
||||
pub git: Option<GitContext>,
|
||||
pub fork_source_ref: Option<ForkSourceRef>,
|
||||
pub parent_id: Option<RunId>,
|
||||
pub provenance: RunProvenance,
|
||||
pub configured_providers: Vec<ProviderId>,
|
||||
/// 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<String>,
|
||||
pub web_url: Option<String>,
|
||||
}
|
||||
|
||||
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<String>,
|
||||
pub run_id: RunId,
|
||||
pub storage_root: PathBuf,
|
||||
pub workflow_slug: Option<String>,
|
||||
pub workflow_version_id: Option<WorkflowVersionId>,
|
||||
pub target: Option<RunTarget>,
|
||||
pub submitted_manifest_bytes: Option<Vec<u8>>,
|
||||
pub title: Option<String>,
|
||||
pub automation: Option<AutomationRef>,
|
||||
pub git: Option<GitContext>,
|
||||
pub fork_source_ref: Option<ForkSourceRef>,
|
||||
pub parent_id: Option<RunId>,
|
||||
pub provenance: RunProvenance,
|
||||
pub web_url: Option<String>,
|
||||
pub target: Option<RunTarget>,
|
||||
pub title: Option<String>,
|
||||
pub automation: Option<AutomationRef>,
|
||||
pub git: Option<GitContext>,
|
||||
pub fork_source_ref: Option<ForkSourceRef>,
|
||||
pub parent_id: Option<RunId>,
|
||||
pub provenance: RunProvenance,
|
||||
pub web_url: Option<String>,
|
||||
}
|
||||
|
||||
#[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<String>,
|
||||
materialized: MaterializedRun,
|
||||
run_id: RunId,
|
||||
run_dir: PathBuf,
|
||||
workflow_slug: Option<String>,
|
||||
workflow_version_id: Option<WorkflowVersionId>,
|
||||
target: Option<RunTarget>,
|
||||
submitted_manifest_bytes: Option<Vec<u8>>,
|
||||
title: Option<String>,
|
||||
automation: Option<AutomationRef>,
|
||||
git: Option<GitContext>,
|
||||
fork_source_ref: Option<ForkSourceRef>,
|
||||
parent_id: Option<RunId>,
|
||||
provenance: RunProvenance,
|
||||
web_url: Option<String>,
|
||||
target: Option<RunTarget>,
|
||||
title: Option<String>,
|
||||
automation: Option<AutomationRef>,
|
||||
git: Option<GitContext>,
|
||||
fork_source_ref: Option<ForkSourceRef>,
|
||||
parent_id: Option<RunId>,
|
||||
provenance: RunProvenance,
|
||||
web_url: Option<String>,
|
||||
}
|
||||
|
||||
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<String>,
|
||||
parent_id: Option<RunId>,
|
||||
|
|
@ -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(),
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
|
|
|
|||
|
|
@ -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<BlobHash>,
|
||||
fork_source_ref: Option<ForkSourceRef>,
|
||||
) {
|
||||
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<BlobHash>, Option<BlobHash>, ForkSourceRef) {
|
||||
) -> (Option<BlobHash>, 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,
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
|
|
|
|||
|
|
@ -698,10 +698,6 @@ impl Client {
|
|||
Ok(SessionEventStream::new(Box::pin(stream)))
|
||||
}
|
||||
|
||||
pub async fn create_run_from_manifest(&self, manifest: types::RunManifest) -> Result<RunId> {
|
||||
self.submit_create_run(manifest.into()).await
|
||||
}
|
||||
|
||||
/// Retrieves one canonical server-managed environment by ID.
|
||||
pub async fn retrieve_environment(&self, id: &str) -> Result<types::Environment> {
|
||||
let response = self
|
||||
|
|
@ -759,12 +755,8 @@ impl Client {
|
|||
}
|
||||
|
||||
pub async fn create_run_from_intent(&self, intent: types::RunIntent) -> Result<RunId> {
|
||||
self.submit_create_run(intent.into()).await
|
||||
}
|
||||
|
||||
async fn submit_create_run(&self, body: types::CreateRunRequest) -> Result<RunId> {
|
||||
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)
|
||||
|
|
|
|||
|
|
@ -79,8 +79,6 @@ pub struct RunSpec {
|
|||
pub labels: HashMap<String, String>,
|
||||
pub provenance: RunProvenance,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub manifest_blob: Option<BlobHash>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub definition_blob: Option<BlobHash>,
|
||||
/// Unredacted copy of this spec in the blob store. Stored events pass
|
||||
/// through secret redaction, so the spec folded from them is display
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
|
|
|
|||
|
|
@ -31,8 +31,6 @@ pub struct RunCreatedProps {
|
|||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub automation: Option<AutomationRef>,
|
||||
pub provenance: RunProvenance,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub manifest_blob: Option<BlobHash>,
|
||||
/// 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.
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
40
lib/packages/fabro-api-client/src/api/runs-api.ts
generated
40
lib/packages/fabro-api-client/src/api/runs-api.ts
generated
|
|
@ -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<RequestArgs> => {
|
||||
// verify required parameter 'createRunRequest' is not null or undefined
|
||||
assertParamExists('createRun', 'createRunRequest', createRunRequest)
|
||||
createRun: async (runIntent: RunIntent, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {
|
||||
// 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<Run>> {
|
||||
const localVarAxiosArgs = await localVarAxiosParamCreator.createRun(createRunRequest, options);
|
||||
async createRun(runIntent: RunIntent, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<Run>> {
|
||||
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<Run> {
|
||||
return localVarFp.createRun(createRunRequest, options).then((request) => request(axios, basePath));
|
||||
createRun(runIntent: RunIntent, options?: RawAxiosRequestConfig): AxiosPromise<Run> {
|
||||
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));
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
@ -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';
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue