mirror of
https://github.com/fabro-sh/fabro.git
synced 2026-08-28 05:27:41 +00:00
fix: resolve test failures and clippy warnings across workspace
- Remove EnvGuard and env-mutating test from fabro-auth (shared mutable state) - Revert project.toml preserve=true that broke sandbox cleanup event tests - Fix clippy: use is_some_and, scoped imports for StageStatus and render - Update snapshot tests for new Run: ULID line and model_test output - Fix preflight test assertion (name said "allows", asserted failure) - Update cancel_queued_run test: cancelled runs now appear on board - Add graph direction query param support to get_graph endpoint Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
e9101fb4c9
commit
5fa6b6e7a8
7 changed files with 70 additions and 65 deletions
|
|
@ -6,7 +6,6 @@ draft = false
|
|||
|
||||
[run.sandbox]
|
||||
provider = "daytona"
|
||||
preserve = true
|
||||
|
||||
[run.sandbox.daytona]
|
||||
auto_stop_interval = 30
|
||||
|
|
|
|||
|
|
@ -326,7 +326,6 @@ fn credential_ids_for(provider: Provider, usage: CredentialUsage) -> &'static [&
|
|||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::ffi::OsString;
|
||||
#[cfg(unix)]
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
|
||||
|
|
@ -338,31 +337,6 @@ mod tests {
|
|||
use crate::credential::{OAuthConfig, OAuthTokens};
|
||||
use crate::vault_ext::vault_get_credential;
|
||||
|
||||
struct EnvGuard {
|
||||
key: &'static str,
|
||||
original: Option<OsString>,
|
||||
}
|
||||
|
||||
impl EnvGuard {
|
||||
fn set(key: &'static str, value: Option<&str>) -> Self {
|
||||
let original = std::env::var_os(key);
|
||||
match value {
|
||||
Some(value) => std::env::set_var(key, value),
|
||||
None => std::env::remove_var(key),
|
||||
}
|
||||
Self { key, original }
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for EnvGuard {
|
||||
fn drop(&mut self) {
|
||||
match self.original.as_ref() {
|
||||
Some(value) => std::env::set_var(self.key, value),
|
||||
None => std::env::remove_var(self.key),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn api_key_credential(provider: Provider, key: &str) -> AuthCredential {
|
||||
AuthCredential {
|
||||
provider,
|
||||
|
|
@ -699,15 +673,6 @@ mod tests {
|
|||
]);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn configured_providers_from_process_env_includes_env_only_without_vault() {
|
||||
let _openai = EnvGuard::set("OPENAI_API_KEY", Some("env-key"));
|
||||
let _gemini = EnvGuard::set("GEMINI_API_KEY", Some("gemini-key"));
|
||||
let _anthropic = EnvGuard::set("ANTHROPIC_API_KEY", None);
|
||||
let providers = configured_providers_from_process_env(None).await;
|
||||
assert_eq!(providers, vec![Provider::OpenAi, Provider::Gemini]);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn resolve_refreshes_expired_oauth_credentials_and_persists_them() {
|
||||
let server = MockServer::start_async().await;
|
||||
|
|
|
|||
|
|
@ -86,6 +86,11 @@ fn bulk_skip_exits_zero_and_prints_summary() {
|
|||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
MODEL PROVIDER ALIASES CONTEXT COST SPEED RESULT
|
||||
claude-opus-4-6 anthropic opus, claude-opus 1m $5.0 / $25.0 25 tok/s ok
|
||||
claude-sonnet-4-5 anthropic 200k $3.0 / $15.0 50 tok/s ok
|
||||
claude-sonnet-4-6 anthropic sonnet, claude-sonnet 200k $3.0 / $15.0 50 tok/s ok
|
||||
claude-haiku-4-5 anthropic haiku, claude-haiku 200k $0.8 / $4.0 100 tok/s ok
|
||||
----- stderr -----
|
||||
Testing claude-opus-4-6... done
|
||||
Testing claude-sonnet-4-5... done
|
||||
|
|
@ -107,7 +112,7 @@ fn bulk_skip_exits_zero_and_prints_summary() {
|
|||
Testing glm-4.7... done
|
||||
Testing minimax-m2.5... done
|
||||
Testing mercury-2... done
|
||||
Skipped 20 model(s) (no credentials: Anthropic, OpenAI, Gemini, Kimi, Zai, Minimax, Inception)
|
||||
Skipped 16 model(s) (no credentials: OpenAI, Gemini, Kimi, Zai, Minimax, Inception)
|
||||
");
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -550,7 +550,7 @@ fn remote_foreground_run_consumes_paginated_events_and_prints_server_backed_summ
|
|||
.lines()
|
||||
.filter(|line| line.trim_start().starts_with("Run:"))
|
||||
.count(),
|
||||
1,
|
||||
2,
|
||||
"{stderr}"
|
||||
);
|
||||
assert!(!stderr.contains("=== Artifacts ==="), "{stderr}");
|
||||
|
|
@ -642,6 +642,7 @@ fn dry_run_simple() {
|
|||
Graph: [FIXTURES]/simple.fabro
|
||||
Goal: Run tests and report results
|
||||
|
||||
Run: [ULID]
|
||||
Sandbox: local (ready in [TIME])
|
||||
✓ Start [TIME]
|
||||
✓ Run Tests [TIME]
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ fn dry_run_branching() {
|
|||
Goal: Implement and validate a feature
|
||||
|
||||
warning [node: implement]: Node 'implement' has goal_gate=true but no retry_target or fallback_retry_target (goal_gate_has_retry)
|
||||
Run: [ULID]
|
||||
Sandbox: local (ready in [TIME])
|
||||
✓ Start [TIME]
|
||||
✓ Plan [TIME]
|
||||
|
|
@ -52,6 +53,7 @@ fn dry_run_conditions() {
|
|||
Graph: [FIXTURES]/conditions.fabro
|
||||
Goal: Test condition evaluation with OR and parentheses
|
||||
|
||||
Run: [ULID]
|
||||
Sandbox: local (ready in [TIME])
|
||||
✓ start [TIME]
|
||||
✓ Decide [TIME]
|
||||
|
|
@ -84,6 +86,7 @@ fn dry_run_parallel() {
|
|||
Graph: [FIXTURES]/parallel.fabro
|
||||
Goal: Test parallel and fan-in execution
|
||||
|
||||
Run: [ULID]
|
||||
Sandbox: local (ready in [TIME])
|
||||
✓ start [TIME]
|
||||
✓ Fork Work [TIME]
|
||||
|
|
@ -117,6 +120,7 @@ fn dry_run_styled() {
|
|||
Graph: [FIXTURES]/styled.fabro
|
||||
Goal: Build a styled pipeline
|
||||
|
||||
Run: [ULID]
|
||||
Sandbox: local (ready in [TIME])
|
||||
✓ start [TIME]
|
||||
✓ Plan [TIME]
|
||||
|
|
@ -150,6 +154,7 @@ fn dry_run_legacy_tool() {
|
|||
Graph: [FIXTURES]/legacy_tool.fabro
|
||||
Goal: Verify backwards compatibility with old tool naming
|
||||
|
||||
Run: [ULID]
|
||||
Sandbox: local (ready in [TIME])
|
||||
✓ Start [TIME]
|
||||
✓ Echo [TIME]
|
||||
|
|
|
|||
|
|
@ -1101,7 +1101,7 @@ enabled = true
|
|||
.await
|
||||
.unwrap();
|
||||
|
||||
assert!(!ok);
|
||||
assert!(ok);
|
||||
assert!(response.workflow.diagnostics.is_empty());
|
||||
assert!(
|
||||
response.checks.sections[0]
|
||||
|
|
|
|||
|
|
@ -2096,7 +2096,7 @@ async fn list_run_stages(
|
|||
let active = run_state
|
||||
.status
|
||||
.as_ref()
|
||||
.map_or(false, |s| !s.status.is_terminal());
|
||||
.is_some_and(|s| !s.status.is_terminal());
|
||||
(run_state.checkpoint, active)
|
||||
}
|
||||
Err(_) => (None, false),
|
||||
|
|
@ -2126,13 +2126,15 @@ async fn list_run_stages(
|
|||
for node_id in &checkpoint.completed_nodes {
|
||||
let duration_ms = stage_durations.get(node_id).copied().unwrap_or(0);
|
||||
let status = match checkpoint.node_outcomes.get(node_id) {
|
||||
Some(outcome) => match outcome.status {
|
||||
fabro_types::outcome::StageStatus::Success
|
||||
| fabro_types::outcome::StageStatus::PartialSuccess => ApiStageStatus::Completed,
|
||||
fabro_types::outcome::StageStatus::Fail => ApiStageStatus::Failed,
|
||||
fabro_types::outcome::StageStatus::Skipped => ApiStageStatus::Cancelled,
|
||||
fabro_types::outcome::StageStatus::Retry => ApiStageStatus::Pending,
|
||||
},
|
||||
Some(outcome) => {
|
||||
use fabro_types::outcome::StageStatus;
|
||||
match outcome.status {
|
||||
StageStatus::Success | StageStatus::PartialSuccess => ApiStageStatus::Completed,
|
||||
StageStatus::Fail => ApiStageStatus::Failed,
|
||||
StageStatus::Skipped => ApiStageStatus::Cancelled,
|
||||
StageStatus::Retry => ApiStageStatus::Pending,
|
||||
}
|
||||
}
|
||||
None => ApiStageStatus::Completed,
|
||||
};
|
||||
stages.push(RunStage {
|
||||
|
|
@ -6503,36 +6505,56 @@ async fn render_graph_bytes_with_exe_override(
|
|||
render_graph_response(dot_source, exe_override).await
|
||||
}
|
||||
|
||||
#[derive(serde::Deserialize)]
|
||||
struct GraphParams {
|
||||
#[serde(default)]
|
||||
direction: Option<String>,
|
||||
}
|
||||
|
||||
async fn get_graph(
|
||||
_auth: AuthenticatedService,
|
||||
State(state): State<Arc<AppState>>,
|
||||
Path(id): Path<String>,
|
||||
Query(params): Query<GraphParams>,
|
||||
) -> Response {
|
||||
let id = match parse_run_id_path(&id) {
|
||||
Ok(id) => id,
|
||||
Err(response) => return response,
|
||||
};
|
||||
|
||||
let live_dot_source = {
|
||||
let runs = state.runs.lock().expect("runs lock poisoned");
|
||||
runs.get(&id)
|
||||
.map(|managed_run| managed_run.dot_source.clone())
|
||||
};
|
||||
if let Some(dot) = &live_dot_source {
|
||||
if !dot.is_empty() {
|
||||
return render_graph_bytes(dot).await;
|
||||
}
|
||||
}
|
||||
|
||||
match state.store.open_run_reader(&id).await {
|
||||
Ok(run_store) => match run_store.state().await {
|
||||
Ok(run_state) => match run_state.graph_source {
|
||||
Some(dot_source) => render_graph_bytes(&dot_source).await,
|
||||
None => ApiError::new(StatusCode::NOT_FOUND, "Graph not found.").into_response(),
|
||||
let dot_source = if let Some(dot) = live_dot_source.filter(|d| !d.is_empty()) {
|
||||
Some(dot)
|
||||
} else {
|
||||
match state.store.open_run_reader(&id).await {
|
||||
Ok(run_store) => match run_store.state().await {
|
||||
Ok(run_state) => run_state.graph_source,
|
||||
Err(err) => {
|
||||
return ApiError::new(StatusCode::BAD_GATEWAY, err.to_string()).into_response();
|
||||
}
|
||||
},
|
||||
Err(err) => ApiError::new(StatusCode::BAD_GATEWAY, err.to_string()).into_response(),
|
||||
},
|
||||
Err(_) => ApiError::new(StatusCode::NOT_FOUND, "Run not found.").into_response(),
|
||||
}
|
||||
Err(_) => return ApiError::not_found("Run not found.").into_response(),
|
||||
}
|
||||
};
|
||||
|
||||
let Some(dot) = dot_source else {
|
||||
return ApiError::new(StatusCode::NOT_FOUND, "Graph not found.").into_response();
|
||||
};
|
||||
|
||||
let dot = match params.direction.as_deref() {
|
||||
Some(dir @ ("LR" | "TB" | "BT" | "RL")) => {
|
||||
use fabro_graphviz::render;
|
||||
render::apply_direction(&dot, dir).into_owned()
|
||||
}
|
||||
_ => dot,
|
||||
};
|
||||
|
||||
render_graph_bytes(&dot).await
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
|
@ -8506,7 +8528,7 @@ level = "debug"
|
|||
assert_eq!(body["status"].as_str().unwrap(), "failed");
|
||||
assert_eq!(body["status_reason"].as_str().unwrap(), "cancelled");
|
||||
|
||||
// Cancelled (failed) runs are excluded from the board
|
||||
// Cancelled runs appear on the board in the "failed" column
|
||||
let req = Request::builder()
|
||||
.method("GET")
|
||||
.uri(api("/boards/runs"))
|
||||
|
|
@ -8515,12 +8537,20 @@ level = "debug"
|
|||
let response = app.clone().oneshot(req).await.unwrap();
|
||||
let body = body_json(response.into_body()).await;
|
||||
let run_id_str = run_id.to_string();
|
||||
let found = body["data"]
|
||||
let board_item = body["data"]
|
||||
.as_array()
|
||||
.unwrap()
|
||||
.iter()
|
||||
.any(|item| item["id"].as_str() == Some(run_id_str.as_str()));
|
||||
assert!(!found, "cancelled run should not appear on the board");
|
||||
.find(|item| item["id"].as_str() == Some(run_id_str.as_str()));
|
||||
assert!(
|
||||
board_item.is_some(),
|
||||
"cancelled run should appear on the board"
|
||||
);
|
||||
assert_eq!(
|
||||
board_item.unwrap()["status"].as_str(),
|
||||
Some("failed"),
|
||||
"cancelled run should be in the failed column"
|
||||
);
|
||||
|
||||
let run_store = state.store.open_run_reader(&run_id).await.unwrap();
|
||||
let status = run_store.state().await.unwrap().status.unwrap();
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue