mirror of
https://github.com/fabro-sh/fabro.git
synced 2026-10-09 03:20:56 +00:00
test(cli): prune slow integration outliers
Some checks are pending
Rust / Format (push) Waiting to run
Rust / Clippy (push) Waiting to run
Rust / Generated Docs (push) Waiting to run
Rust / Test (Linux) (push) Waiting to run
Rust / Test (macOS) (push) Waiting to run
TypeScript / Typecheck (push) Waiting to run
TypeScript / Test (push) Waiting to run
TypeScript / Build (push) Waiting to run
Some checks are pending
Rust / Format (push) Waiting to run
Rust / Clippy (push) Waiting to run
Rust / Generated Docs (push) Waiting to run
Rust / Test (Linux) (push) Waiting to run
Rust / Test (macOS) (push) Waiting to run
TypeScript / Typecheck (push) Waiting to run
TypeScript / Test (push) Waiting to run
TypeScript / Build (push) Waiting to run
Replace remaining expensive CLI lifecycle checks with seeded fixtures or focused unit coverage so the concurrent suite spends less time on duplicate full-process setup.
This commit is contained in:
parent
4ce07aac17
commit
788b389e4c
10 changed files with 294 additions and 537 deletions
|
|
@ -748,6 +748,8 @@ pub async fn run() -> anyhow::Result<()> {
|
|||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::collections::HashMap;
|
||||
|
||||
use fabro_model::Provider;
|
||||
use serde_json::json;
|
||||
|
||||
|
|
@ -861,6 +863,16 @@ mod tests {
|
|||
assert_eq!(profile.provider(), Provider::OpenAi);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ensure_provider_registered_reports_missing_credentials() {
|
||||
let client = Client::new(HashMap::new(), None, vec![]);
|
||||
let error = ensure_provider_registered(&client, Provider::Anthropic).unwrap_err();
|
||||
assert_eq!(
|
||||
error.to_string(),
|
||||
"LLM credentials not configured for provider 'anthropic'"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn build_profile_gemini() {
|
||||
let profile = build_profile(Provider::Gemini, "model", None);
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ use fabro_config::user::active_settings_path;
|
|||
use fabro_util::terminal::Styles;
|
||||
use tracing::debug;
|
||||
|
||||
use crate::args::{GraphArgs, GraphDirection};
|
||||
use crate::args::{GraphArgs, GraphDirection, GraphOutputFormat};
|
||||
use crate::command_context::CommandContext;
|
||||
use crate::commands::run::output::api_diagnostics_to_local;
|
||||
use crate::manifest_builder::{ManifestBuildInput, build_run_manifest};
|
||||
|
|
@ -68,10 +68,7 @@ pub(crate) async fn run(
|
|||
std::fs::write(output_path, &rendered)
|
||||
.with_context(|| format!("writing rendered graph to {}", output_path.display()))?;
|
||||
if ctx.json_output() {
|
||||
print_json_pretty(&serde_json::json!({
|
||||
"path": absolute_or_current(output_path),
|
||||
"format": args.format.to_string(),
|
||||
}))?;
|
||||
print_json_pretty(&output_file_json(output_path, args.format))?;
|
||||
}
|
||||
} else {
|
||||
std::io::stdout().write_all(&rendered)?;
|
||||
|
|
@ -85,3 +82,29 @@ pub(crate) async fn run(
|
|||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn output_file_json(output_path: &std::path::Path, format: GraphOutputFormat) -> serde_json::Value {
|
||||
serde_json::json!({
|
||||
"path": absolute_or_current(output_path),
|
||||
"format": format.to_string(),
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn output_file_json_reports_absolute_path_and_format() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let path = dir.path().join("graph.svg");
|
||||
|
||||
assert_eq!(
|
||||
output_file_json(&path, GraphOutputFormat::Svg),
|
||||
serde_json::json!({
|
||||
"path": path,
|
||||
"format": "svg",
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
use fabro_test::{fabro_snapshot, test_context};
|
||||
|
||||
use super::support::{git_filters, setup_git_backed_noop_run, setup_seeded_git_backed_changed_run};
|
||||
use super::support::{
|
||||
git_filters, setup_seeded_git_backed_changed_run, setup_seeded_git_backed_noop_run,
|
||||
};
|
||||
|
||||
#[test]
|
||||
fn help() {
|
||||
|
|
@ -34,9 +36,9 @@ fn help() {
|
|||
#[test]
|
||||
fn diff_completed_run_without_changes_reports_no_patch() {
|
||||
let context = test_context!();
|
||||
let setup = setup_git_backed_noop_run(&context);
|
||||
let run = setup_seeded_git_backed_noop_run(&context);
|
||||
let mut cmd = context.command();
|
||||
cmd.args(["diff", &setup.run.run_id]);
|
||||
cmd.args(["diff", &run.run_id]);
|
||||
|
||||
fabro_snapshot!(git_filters(&context), cmd, @"
|
||||
success: false
|
||||
|
|
|
|||
|
|
@ -90,23 +90,6 @@ fn no_prompt() {
|
|||
");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn exec_missing_api_key_exits_with_error() {
|
||||
let context = test_context!();
|
||||
let mut cmd = context.exec_cmd();
|
||||
cmd.arg("test prompt");
|
||||
cmd.env_clear();
|
||||
preserve_coverage_env!(cmd);
|
||||
cmd.env("HOME", &context.home_dir);
|
||||
fabro_snapshot!(context.filters(), cmd, @"
|
||||
success: false
|
||||
exit_code: 1
|
||||
----- stdout -----
|
||||
----- stderr -----
|
||||
× LLM credentials not configured for provider 'anthropic'
|
||||
");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn exec_uses_user_config_defaults() {
|
||||
let context = test_context!();
|
||||
|
|
|
|||
|
|
@ -205,37 +205,6 @@ fn graph_json_without_output_is_rejected() {
|
|||
assert!(stderr.contains("--json is not supported for this command"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn graph_json_with_output_reports_file() {
|
||||
let context = test_context!();
|
||||
context.ensure_home_server_auth_methods();
|
||||
let output_path = context.temp_dir.join("graph.svg");
|
||||
let workflow = fixture("simple.fabro");
|
||||
|
||||
let output = context
|
||||
.command()
|
||||
.args([
|
||||
"--json",
|
||||
"graph",
|
||||
workflow.to_str().unwrap(),
|
||||
"--output",
|
||||
output_path.to_str().unwrap(),
|
||||
])
|
||||
.output()
|
||||
.expect("command should run");
|
||||
|
||||
assert!(
|
||||
output.status.success(),
|
||||
"stdout:\n{}\nstderr:\n{}",
|
||||
output_stdout(&output),
|
||||
output_stderr(&output)
|
||||
);
|
||||
let value: Value = serde_json::from_slice(&output.stdout).expect("graph JSON should parse");
|
||||
assert_eq!(value["format"], "svg");
|
||||
assert_eq!(value["path"], output_path.to_string_lossy().to_string());
|
||||
assert!(output_path.exists());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn secret_list_json_missing_env_outputs_json_array() {
|
||||
let context = test_context!();
|
||||
|
|
|
|||
|
|
@ -1,12 +1,5 @@
|
|||
#![expect(
|
||||
clippy::disallowed_methods,
|
||||
reason = "integration tests stage fixtures with sync std::fs; test infrastructure, not Tokio-hot path"
|
||||
)]
|
||||
|
||||
use fabro_test::{fabro_snapshot, test_context};
|
||||
|
||||
use super::support::{git_stdout, output_stderr, run_state_by_id, setup_git_backed_changed_run};
|
||||
|
||||
#[test]
|
||||
fn help() {
|
||||
let context = test_context!();
|
||||
|
|
@ -54,74 +47,3 @@ fn resume_requires_run_arg() {
|
|||
For more information, try '--help'.
|
||||
");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resume_rewound_run_succeeds() {
|
||||
let context = test_context!();
|
||||
let setup = setup_git_backed_changed_run(&context);
|
||||
|
||||
let new_run_id = rewind_replacement_run_id(&context, &setup);
|
||||
let rewound_head = run_state_by_id(&context, &new_run_id)
|
||||
.checkpoint
|
||||
.and_then(|checkpoint| checkpoint.git_commit_sha)
|
||||
.expect("rewound run should record checkpoint sha");
|
||||
|
||||
let mut resume_cmd = context.command();
|
||||
resume_cmd.current_dir(&setup.repo_dir);
|
||||
resume_cmd.env("OPENAI_API_KEY", "test");
|
||||
resume_cmd.args(["resume", &new_run_id]);
|
||||
let resume_output = resume_cmd.output().expect("resume should execute");
|
||||
assert!(
|
||||
resume_output.status.success(),
|
||||
"resume should succeed\nstdout:\n{}\nstderr:\n{}",
|
||||
String::from_utf8_lossy(&resume_output.stdout),
|
||||
output_stderr(&resume_output)
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
git_stdout(&setup.repo_dir, &[
|
||||
"show",
|
||||
&format!("fabro/run/{new_run_id}:story.txt")
|
||||
]),
|
||||
"line 1\nline 2\nline 3\n"
|
||||
);
|
||||
assert_eq!(
|
||||
std::fs::read_to_string(setup.repo_dir.join("story.txt")).unwrap(),
|
||||
"line 1\n"
|
||||
);
|
||||
let resumed_head = git_stdout(&setup.repo_dir, &[
|
||||
"rev-parse",
|
||||
&format!("fabro/run/{new_run_id}"),
|
||||
]);
|
||||
assert_ne!(resumed_head.trim(), rewound_head);
|
||||
}
|
||||
|
||||
fn rewind_replacement_run_id(
|
||||
context: &fabro_test::TestContext,
|
||||
setup: &super::support::GitRunSetup,
|
||||
) -> String {
|
||||
let rewind = context
|
||||
.command()
|
||||
.current_dir(&setup.repo_dir)
|
||||
.args(["rewind", &setup.run.run_id, "@2", "--json"])
|
||||
.output()
|
||||
.expect("rewind should execute");
|
||||
assert!(
|
||||
rewind.status.success(),
|
||||
"rewind should succeed\nstdout:\n{}\nstderr:\n{}",
|
||||
String::from_utf8_lossy(&rewind.stdout),
|
||||
output_stderr(&rewind)
|
||||
);
|
||||
|
||||
let response: serde_json::Value =
|
||||
serde_json::from_slice(&rewind.stdout).expect("rewind json should parse");
|
||||
assert_eq!(
|
||||
response["source_run_id"].as_str(),
|
||||
Some(setup.run.run_id.as_str())
|
||||
);
|
||||
assert_eq!(response["archived"].as_bool(), Some(true));
|
||||
response["new_run_id"]
|
||||
.as_str()
|
||||
.expect("rewind response should include new_run_id")
|
||||
.to_string()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -44,11 +44,6 @@ pub(crate) struct RunSetup {
|
|||
pub(crate) run_dir: PathBuf,
|
||||
}
|
||||
|
||||
pub(crate) struct GitRunSetup {
|
||||
pub(crate) run: RunSetup,
|
||||
pub(crate) repo_dir: PathBuf,
|
||||
}
|
||||
|
||||
pub(crate) struct SeededGitRunSetup {
|
||||
pub(crate) run: RunSetup,
|
||||
pub(crate) step_one_sha: String,
|
||||
|
|
@ -68,12 +63,6 @@ pub(crate) struct WorkflowGate {
|
|||
gate_path: PathBuf,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
enum GitWorkflowKind {
|
||||
Changed,
|
||||
Noop,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
enum SeededRunState {
|
||||
Submitted,
|
||||
|
|
@ -295,18 +284,18 @@ pub(crate) fn setup_detached_dry_run(context: &TestContext) -> RunSetup {
|
|||
run
|
||||
}
|
||||
|
||||
pub(crate) fn setup_git_backed_changed_run(context: &TestContext) -> GitRunSetup {
|
||||
setup_git_backed_run(context, GitWorkflowKind::Changed)
|
||||
}
|
||||
|
||||
pub(crate) fn setup_git_backed_noop_run(context: &TestContext) -> GitRunSetup {
|
||||
setup_git_backed_run(context, GitWorkflowKind::Noop)
|
||||
}
|
||||
|
||||
pub(crate) fn setup_seeded_git_backed_changed_run(context: &TestContext) -> SeededGitRunSetup {
|
||||
block_on(seed_git_backed_changed_run(context))
|
||||
}
|
||||
|
||||
pub(crate) fn setup_seeded_git_backed_noop_run(context: &TestContext) -> RunSetup {
|
||||
block_on(seed_git_backed_noop_run(context))
|
||||
}
|
||||
|
||||
pub(crate) fn setup_seeded_artifact_run(context: &TestContext) -> RunSetup {
|
||||
block_on(seed_artifact_run(context))
|
||||
}
|
||||
|
||||
pub(crate) fn setup_project_fixture(context: &TestContext) -> ProjectFixture {
|
||||
let project_dir = context.temp_dir.join("project");
|
||||
let fabro_root = project_dir.join(".fabro");
|
||||
|
|
@ -325,51 +314,6 @@ impl WorkflowGate {
|
|||
}
|
||||
}
|
||||
|
||||
pub(crate) fn setup_artifact_run(context: &TestContext) -> WorkspaceRunSetup {
|
||||
let workspace_dir = context.temp_dir.join("artifact-run");
|
||||
std::fs::create_dir_all(&workspace_dir)
|
||||
.unwrap_or_else(|err| panic!("failed to create {}: {err}", workspace_dir.display()));
|
||||
|
||||
write_text_file(
|
||||
&workspace_dir.join("artifact_run.fabro"),
|
||||
r#"digraph ArtifactRun {
|
||||
graph [goal="Exercise artifact commands", default_max_retries=0]
|
||||
start [shape=Mdiamond]
|
||||
exit [shape=Msquare]
|
||||
create_assets [shape=parallelogram, script="mkdir -p assets/shared assets/node_a && printf one > assets/shared/report.txt && printf alpha > assets/node_a/summary.txt", max_retries=0]
|
||||
retry_assets [shape=parallelogram, script="mkdir -p assets/retry && touch -c -t 200001010000 assets/shared/report.txt assets/node_a/summary.txt && if [ ! -f .retry-sentinel ]; then printf first > assets/retry/report.txt && touch .retry-sentinel && sleep 1; else printf second > assets/retry/report.txt; fi", retry_policy="linear", timeout="500ms"]
|
||||
create_colliding [shape=parallelogram, script="mkdir -p assets/other assets/retry && touch -c -t 200001010000 assets/shared/report.txt assets/node_a/summary.txt assets/retry/report.txt && printf beta > assets/other/summary.txt && printf second > assets/retry/report.txt", max_retries=0]
|
||||
start -> create_assets -> retry_assets -> create_colliding -> exit
|
||||
}
|
||||
"#,
|
||||
);
|
||||
write_text_file(
|
||||
&workspace_dir.join("run.toml"),
|
||||
r#"_version = 1
|
||||
|
||||
[workflow]
|
||||
graph = "artifact_run.fabro"
|
||||
|
||||
[run]
|
||||
goal = "Exercise artifact commands"
|
||||
|
||||
[run.sandbox]
|
||||
provider = "local"
|
||||
preserve = true
|
||||
|
||||
[run.sandbox.local]
|
||||
worktree_mode = "never"
|
||||
|
||||
[run.artifacts]
|
||||
include = ["assets/**"]
|
||||
"#,
|
||||
);
|
||||
|
||||
let run = run_local_workflow(context, &workspace_dir, "run.toml");
|
||||
|
||||
WorkspaceRunSetup { run, workspace_dir }
|
||||
}
|
||||
|
||||
pub(crate) fn setup_local_sandbox_run(context: &TestContext) -> WorkspaceRunSetup {
|
||||
let workspace_dir = context.temp_dir.join("local-sandbox");
|
||||
std::fs::create_dir_all(&workspace_dir)
|
||||
|
|
@ -853,6 +797,69 @@ 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",
|
||||
"no_retro": true,
|
||||
"label": test_labels(context),
|
||||
}),
|
||||
Some(serde_json::json!({
|
||||
"origin_url": "https://github.com/fabro-sh/seeded-fixture.git",
|
||||
"branch": "main",
|
||||
"sha": base_sha,
|
||||
"dirty": "clean",
|
||||
"push_outcome": {
|
||||
"type": "succeeded",
|
||||
"remote": "origin",
|
||||
"branch": "main",
|
||||
},
|
||||
})),
|
||||
)
|
||||
.await;
|
||||
|
||||
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;
|
||||
run
|
||||
}
|
||||
|
||||
async fn seed_artifact_run(context: &TestContext) -> RunSetup {
|
||||
let run = create_seeded_run(
|
||||
context,
|
||||
"artifact_run.fabro",
|
||||
artifact_workflow_source(),
|
||||
serde_json::json!({
|
||||
"sandbox": "local",
|
||||
"no_retro": true,
|
||||
"label": test_labels(context),
|
||||
}),
|
||||
None,
|
||||
)
|
||||
.await;
|
||||
|
||||
let (client, base_url) = server_endpoint(&context.storage_dir)
|
||||
.expect("test server endpoint should be available for seeded artifacts");
|
||||
append_seeded_artifact_run_events(&client, &base_url, &run, context).await;
|
||||
for (stage_id, path, contents) in [
|
||||
("create_assets@1", "assets/node_a/summary.txt", "alpha"),
|
||||
("create_assets@1", "assets/shared/report.txt", "one"),
|
||||
("create_colliding@1", "assets/other/summary.txt", "beta"),
|
||||
("create_colliding@1", "assets/retry/report.txt", "second"),
|
||||
("retry_assets@1", "assets/retry/report.txt", "first"),
|
||||
("retry_assets@2", "assets/retry/report.txt", "second"),
|
||||
] {
|
||||
upload_seeded_artifact(&client, &base_url, &run.run_id, stage_id, path, contents).await;
|
||||
}
|
||||
|
||||
run
|
||||
}
|
||||
|
||||
async fn create_seeded_run(
|
||||
context: &TestContext,
|
||||
target_path: &str,
|
||||
|
|
@ -1205,6 +1212,152 @@ async fn append_seeded_git_completion_events(
|
|||
.await;
|
||||
}
|
||||
|
||||
async fn append_seeded_git_noop_events(
|
||||
client: &fabro_http::HttpClient,
|
||||
base_url: &str,
|
||||
run: &RunSetup,
|
||||
context: &TestContext,
|
||||
base_sha: &str,
|
||||
) {
|
||||
append_run_event(
|
||||
client,
|
||||
base_url,
|
||||
&run.run_id,
|
||||
None,
|
||||
"run.started",
|
||||
serde_json::json!({
|
||||
"name": "Flow",
|
||||
"base_branch": "main",
|
||||
"base_sha": base_sha,
|
||||
"run_branch": format!("fabro/run/{}", run.run_id),
|
||||
"worktree_dir": context.temp_dir.display().to_string(),
|
||||
"goal": "Leave tracked files unchanged",
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
append_run_event(
|
||||
client,
|
||||
base_url,
|
||||
&run.run_id,
|
||||
None,
|
||||
"run.starting",
|
||||
serde_json::json!({}),
|
||||
)
|
||||
.await;
|
||||
append_run_event(
|
||||
client,
|
||||
base_url,
|
||||
&run.run_id,
|
||||
None,
|
||||
"run.running",
|
||||
serde_json::json!({}),
|
||||
)
|
||||
.await;
|
||||
append_run_event(
|
||||
client,
|
||||
base_url,
|
||||
&run.run_id,
|
||||
None,
|
||||
"run.completed",
|
||||
serde_json::json!({
|
||||
"duration_ms": 123,
|
||||
"artifact_count": 0,
|
||||
"status": "success",
|
||||
"reason": "completed",
|
||||
"total_usd_micros": null,
|
||||
"final_git_commit_sha": base_sha,
|
||||
"final_patch": null,
|
||||
"billing": null,
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
}
|
||||
|
||||
async fn append_seeded_artifact_run_events(
|
||||
client: &fabro_http::HttpClient,
|
||||
base_url: &str,
|
||||
run: &RunSetup,
|
||||
context: &TestContext,
|
||||
) {
|
||||
append_run_event(
|
||||
client,
|
||||
base_url,
|
||||
&run.run_id,
|
||||
None,
|
||||
"run.started",
|
||||
serde_json::json!({
|
||||
"name": "ArtifactRun",
|
||||
"base_branch": null,
|
||||
"base_sha": null,
|
||||
"run_branch": null,
|
||||
"worktree_dir": context.temp_dir.display().to_string(),
|
||||
"goal": "Exercise artifact commands",
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
append_run_event(
|
||||
client,
|
||||
base_url,
|
||||
&run.run_id,
|
||||
None,
|
||||
"run.starting",
|
||||
serde_json::json!({}),
|
||||
)
|
||||
.await;
|
||||
append_run_event(
|
||||
client,
|
||||
base_url,
|
||||
&run.run_id,
|
||||
None,
|
||||
"run.running",
|
||||
serde_json::json!({}),
|
||||
)
|
||||
.await;
|
||||
append_run_event(
|
||||
client,
|
||||
base_url,
|
||||
&run.run_id,
|
||||
None,
|
||||
"run.completed",
|
||||
serde_json::json!({
|
||||
"duration_ms": 123,
|
||||
"artifact_count": 6,
|
||||
"status": "success",
|
||||
"reason": "completed",
|
||||
"total_usd_micros": null,
|
||||
"final_git_commit_sha": null,
|
||||
"final_patch": null,
|
||||
"billing": null,
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
}
|
||||
|
||||
async fn upload_seeded_artifact(
|
||||
client: &fabro_http::HttpClient,
|
||||
base_url: &str,
|
||||
run_id: &str,
|
||||
stage_id: &str,
|
||||
path: &str,
|
||||
contents: &str,
|
||||
) {
|
||||
let response = client
|
||||
.post(format!(
|
||||
"{base_url}/api/v1/runs/{run_id}/stages/{stage_id}/artifacts?filename={path}"
|
||||
))
|
||||
.header(fabro_http::header::CONTENT_TYPE, "application/octet-stream")
|
||||
.body(contents.to_string())
|
||||
.send()
|
||||
.await
|
||||
.unwrap_or_else(|err| panic!("seeded artifact upload should execute: {err}"));
|
||||
expect_reqwest_status(
|
||||
response,
|
||||
fabro_http::StatusCode::NO_CONTENT,
|
||||
format!("POST /api/v1/runs/{run_id}/stages/{stage_id}/artifacts ({path})"),
|
||||
)
|
||||
.await;
|
||||
}
|
||||
|
||||
async fn append_seeded_stage(
|
||||
client: &fabro_http::HttpClient,
|
||||
base_url: &str,
|
||||
|
|
@ -1402,6 +1555,30 @@ fn changed_git_workflow_source() -> &'static str {
|
|||
"#
|
||||
}
|
||||
|
||||
fn noop_git_workflow_source() -> &'static str {
|
||||
r#"digraph Flow {
|
||||
graph [goal="Leave tracked files unchanged"];
|
||||
start [shape=Mdiamond];
|
||||
exit [shape=Msquare];
|
||||
check [shape=parallelogram, script="test -f story.txt"];
|
||||
start -> check -> exit;
|
||||
}
|
||||
"#
|
||||
}
|
||||
|
||||
fn artifact_workflow_source() -> &'static str {
|
||||
r#"digraph ArtifactRun {
|
||||
graph [goal="Exercise artifact commands", default_max_retries=0]
|
||||
start [shape=Mdiamond]
|
||||
exit [shape=Msquare]
|
||||
create_assets [shape=parallelogram, script="true", max_retries=0]
|
||||
retry_assets [shape=parallelogram, script="true", retry_policy="linear", timeout="500ms"]
|
||||
create_colliding [shape=parallelogram, script="true", max_retries=0]
|
||||
start -> create_assets -> retry_assets -> create_colliding -> exit
|
||||
}
|
||||
"#
|
||||
}
|
||||
|
||||
fn step_one_patch() -> &'static str {
|
||||
"diff --git a/story.txt b/story.txt\nindex 1111111..2222222 100644\n--- a/story.txt\n+++ b/story.txt\n@@ -1 +1,2 @@\n line 1\n+line 2\n"
|
||||
}
|
||||
|
|
@ -1414,10 +1591,6 @@ fn final_story_patch() -> &'static str {
|
|||
"diff --git a/story.txt b/story.txt\nindex 1111111..3333333 100644\n--- a/story.txt\n+++ b/story.txt\n@@ -1 +1,3 @@\n line 1\n+line 2\n+line 3\n"
|
||||
}
|
||||
|
||||
pub(crate) fn git_stdout(repo_dir: &Path, args: &[&str]) -> String {
|
||||
stdout(&git_success(repo_dir, args))
|
||||
}
|
||||
|
||||
pub(crate) fn text_tree(root: &Path) -> Vec<String> {
|
||||
fn visit(root: &Path, dir: &Path, entries: &mut Vec<String>) {
|
||||
let mut children: Vec<_> = std::fs::read_dir(dir)
|
||||
|
|
@ -1579,146 +1752,6 @@ pub(crate) fn compact_git_inspect(output: &Output) -> Value {
|
|||
)
|
||||
}
|
||||
|
||||
fn setup_git_backed_run(context: &TestContext, workflow: GitWorkflowKind) -> GitRunSetup {
|
||||
let repo_dir = context.temp_dir.join(match workflow {
|
||||
GitWorkflowKind::Changed => "git-changed",
|
||||
GitWorkflowKind::Noop => "git-noop",
|
||||
});
|
||||
std::fs::create_dir_all(&repo_dir)
|
||||
.unwrap_or_else(|err| panic!("failed to create {}: {err}", repo_dir.display()));
|
||||
|
||||
git_success(&repo_dir, &["init", "-q"]);
|
||||
git_success(&repo_dir, &["config", "user.name", "Fabro Test"]);
|
||||
git_success(&repo_dir, &["config", "user.email", "test@example.com"]);
|
||||
|
||||
write_text_file(&repo_dir.join("story.txt"), "line 1\n");
|
||||
write_text_file(&repo_dir.join("flow.fabro"), match workflow {
|
||||
GitWorkflowKind::Changed => {
|
||||
r#"digraph Flow {
|
||||
graph [goal="Edit a tracked file"];
|
||||
start [shape=Mdiamond];
|
||||
exit [shape=Msquare];
|
||||
step_one [shape=parallelogram, script="printf 'line 1\nline 2\n' > story.txt"];
|
||||
step_two [shape=parallelogram, script="printf 'line 1\nline 2\nline 3\n' > story.txt"];
|
||||
start -> step_one -> step_two -> exit;
|
||||
}
|
||||
"#
|
||||
}
|
||||
GitWorkflowKind::Noop => {
|
||||
r#"digraph Flow {
|
||||
graph [goal="Leave tracked files unchanged"];
|
||||
start [shape=Mdiamond];
|
||||
exit [shape=Msquare];
|
||||
check [shape=parallelogram, script="test -f story.txt"];
|
||||
start -> check -> exit;
|
||||
}
|
||||
"#
|
||||
}
|
||||
});
|
||||
|
||||
git_success(&repo_dir, &["add", "story.txt", "flow.fabro"]);
|
||||
git_success(&repo_dir, &["commit", "-qm", "init"]);
|
||||
let remote_dir = context.temp_dir.join(match workflow {
|
||||
GitWorkflowKind::Changed => "git-changed-remote.git",
|
||||
GitWorkflowKind::Noop => "git-noop-remote.git",
|
||||
});
|
||||
let remote_dir_str = remote_dir.display().to_string();
|
||||
git_success(&context.temp_dir, &[
|
||||
"init",
|
||||
"--bare",
|
||||
"-q",
|
||||
&remote_dir_str,
|
||||
]);
|
||||
git_success(&repo_dir, &["remote", "add", "origin", &remote_dir_str]);
|
||||
git_success(&repo_dir, &["push", "-u", "origin", "HEAD:main"]);
|
||||
let base_sha = git_stdout(&repo_dir, &["rev-parse", "HEAD"])
|
||||
.trim()
|
||||
.to_string();
|
||||
let run_id = unique_run_id();
|
||||
|
||||
let mut cmd = context.run_cmd();
|
||||
cmd.current_dir(&repo_dir);
|
||||
cmd.env("OPENAI_API_KEY", "test");
|
||||
cmd.args([
|
||||
"--run-id",
|
||||
run_id.as_str(),
|
||||
"--sandbox",
|
||||
"local",
|
||||
"--no-retro",
|
||||
"--provider",
|
||||
"openai",
|
||||
"flow.fabro",
|
||||
]);
|
||||
let output = cmd.output().expect("command should execute");
|
||||
if !output.status.success() {
|
||||
panic!(
|
||||
"command failed: fabro run --sandbox local --no-retro --provider openai flow.fabro\nstdout:\n{}\nstderr:\n{}",
|
||||
stdout(&output),
|
||||
stderr(&output)
|
||||
);
|
||||
}
|
||||
|
||||
let run = RunSetup {
|
||||
run_dir: context.find_run_dir(&run_id),
|
||||
run_id,
|
||||
};
|
||||
let start = serde_json::to_value(
|
||||
run_state(&run.run_dir)
|
||||
.start
|
||||
.expect("start record should exist"),
|
||||
)
|
||||
.expect("start record should serialize to JSON");
|
||||
assert_eq!(
|
||||
start["run_branch"].as_str(),
|
||||
Some(format!("fabro/run/{}", run.run_id).as_str())
|
||||
);
|
||||
assert_eq!(start["base_sha"].as_str(), Some(base_sha.as_str()));
|
||||
match workflow {
|
||||
GitWorkflowKind::Changed => {
|
||||
assert!(
|
||||
run_state(&run.run_dir).final_patch.is_some(),
|
||||
"changed git-backed run should persist final patch in store"
|
||||
);
|
||||
let state = run_state(&run.run_dir);
|
||||
assert!(
|
||||
state
|
||||
.iter_nodes()
|
||||
.any(|(node, state)| node.node_id() == "step_one" && state.diff.is_some())
|
||||
);
|
||||
assert!(
|
||||
state
|
||||
.iter_nodes()
|
||||
.any(|(node, state)| node.node_id() == "step_two" && state.diff.is_some())
|
||||
);
|
||||
}
|
||||
GitWorkflowKind::Noop => {
|
||||
assert!(
|
||||
run_state(&run.run_dir).final_patch.is_none(),
|
||||
"no-op git-backed run should not persist final.patch"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
GitRunSetup { run, repo_dir }
|
||||
}
|
||||
|
||||
fn git_success(repo_dir: &Path, args: &[&str]) -> Output {
|
||||
let output = std::process::Command::new("git")
|
||||
.current_dir(repo_dir)
|
||||
.args(args)
|
||||
.output()
|
||||
.expect("git command should execute");
|
||||
if !output.status.success() {
|
||||
panic!(
|
||||
"git command failed: git {}\nstdout:\n{}\nstderr:\n{}",
|
||||
args.join(" "),
|
||||
stdout(&output),
|
||||
stderr(&output)
|
||||
);
|
||||
}
|
||||
output
|
||||
}
|
||||
|
||||
fn write_text_file(path: &Path, content: &str) {
|
||||
if let Some(parent) = path.parent() {
|
||||
std::fs::create_dir_all(parent)
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ use std::time::Duration;
|
|||
|
||||
use fabro_test::{fabro_snapshot, test_context};
|
||||
|
||||
use crate::cmd::support::{read_text, setup_artifact_run, text_tree};
|
||||
use crate::cmd::support::{read_text, setup_seeded_artifact_run, text_tree};
|
||||
|
||||
fn artifact_filters(context: &fabro_test::TestContext) -> Vec<(String, String)> {
|
||||
let mut filters = context.filters();
|
||||
|
|
@ -16,11 +16,11 @@ fn artifact_filters(context: &fabro_test::TestContext) -> Vec<(String, String)>
|
|||
#[test]
|
||||
fn artifact_commands_share_populated_run_fixture() {
|
||||
let context = test_context!();
|
||||
let setup = setup_artifact_run(&context);
|
||||
let run = setup_seeded_artifact_run(&context);
|
||||
let filters = artifact_filters(&context);
|
||||
|
||||
let mut list_json = context.command();
|
||||
list_json.args(["artifact", "list", &setup.run.run_id, "--json"]);
|
||||
list_json.args(["artifact", "list", &run.run_id, "--json"]);
|
||||
fabro_snapshot!(filters.clone(), list_json, @r#"
|
||||
success: true
|
||||
exit_code: 0
|
||||
|
|
@ -70,7 +70,7 @@ fn artifact_commands_share_populated_run_fixture() {
|
|||
list_filtered.args([
|
||||
"artifact",
|
||||
"list",
|
||||
&setup.run.run_id,
|
||||
&run.run_id,
|
||||
"--node",
|
||||
"retry_assets",
|
||||
"--retry",
|
||||
|
|
@ -97,7 +97,7 @@ fn artifact_commands_share_populated_run_fixture() {
|
|||
cp_single.args([
|
||||
"artifact",
|
||||
"cp",
|
||||
&format!("{}:assets/shared/report.txt", setup.run.run_id),
|
||||
&format!("{}:assets/shared/report.txt", run.run_id),
|
||||
single_dest.to_str().unwrap(),
|
||||
"--node",
|
||||
"create_assets",
|
||||
|
|
@ -116,7 +116,7 @@ fn artifact_commands_share_populated_run_fixture() {
|
|||
cp_tree.args([
|
||||
"artifact",
|
||||
"cp",
|
||||
&setup.run.run_id,
|
||||
&run.run_id,
|
||||
tree_dest.to_str().unwrap(),
|
||||
"--tree",
|
||||
]);
|
||||
|
|
@ -145,7 +145,7 @@ fn artifact_commands_share_populated_run_fixture() {
|
|||
cp_ambiguous.args([
|
||||
"artifact",
|
||||
"cp",
|
||||
&format!("{}:assets/retry/report.txt", setup.run.run_id),
|
||||
&format!("{}:assets/retry/report.txt", run.run_id),
|
||||
ambiguous_dest.to_str().unwrap(),
|
||||
]);
|
||||
fabro_snapshot!(context.filters(), cp_ambiguous, @"
|
||||
|
|
@ -158,12 +158,7 @@ fn artifact_commands_share_populated_run_fixture() {
|
|||
|
||||
let flat_dest = context.temp_dir.join("artifact-flat");
|
||||
let mut cp_flat = context.command();
|
||||
cp_flat.args([
|
||||
"artifact",
|
||||
"cp",
|
||||
&setup.run.run_id,
|
||||
flat_dest.to_str().unwrap(),
|
||||
]);
|
||||
cp_flat.args(["artifact", "cp", &run.run_id, flat_dest.to_str().unwrap()]);
|
||||
fabro_snapshot!(context.filters(), cp_flat, @"
|
||||
success: false
|
||||
exit_code: 1
|
||||
|
|
|
|||
|
|
@ -8,7 +8,6 @@ mod artifacts;
|
|||
mod auth;
|
||||
mod exec;
|
||||
mod lifecycle;
|
||||
mod recovery;
|
||||
mod server_lifecycle;
|
||||
mod smoke;
|
||||
|
||||
|
|
|
|||
|
|
@ -1,181 +0,0 @@
|
|||
#![expect(
|
||||
clippy::disallowed_methods,
|
||||
reason = "This recovery scenario test uses the real git CLI to set up repository history for end-to-end assertions."
|
||||
)]
|
||||
|
||||
use std::collections::BTreeSet;
|
||||
use std::path::Path;
|
||||
|
||||
use fabro_test::{fabro_snapshot, test_context};
|
||||
use git2::Repository;
|
||||
|
||||
use crate::support::unique_run_id;
|
||||
|
||||
fn list_metadata_run_ids(repo_dir: &Path) -> BTreeSet<String> {
|
||||
let repo = Repository::discover(repo_dir).expect("recovery fixture should be a git repo");
|
||||
repo.references()
|
||||
.expect("recovery fixture should list git references")
|
||||
.flatten()
|
||||
.filter_map(|reference| reference.name().map(ToOwned::to_owned))
|
||||
.filter_map(|name| {
|
||||
name.strip_prefix("refs/heads/fabro/meta/")
|
||||
.map(ToOwned::to_owned)
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[expect(
|
||||
clippy::disallowed_methods,
|
||||
reason = "This sync git integration helper retries metadata branch deletion until libgit2 releases its lock."
|
||||
)]
|
||||
fn delete_metadata_branch_when_ready(repo_dir: &Path, run_id: &str) {
|
||||
let deadline = std::time::Instant::now() + std::time::Duration::from_secs(5);
|
||||
loop {
|
||||
let repo = Repository::discover(repo_dir).expect("recovery fixture should stay a git repo");
|
||||
let mut reference = repo
|
||||
.find_reference(&format!("refs/heads/fabro/meta/{run_id}"))
|
||||
.expect("metadata branch should exist");
|
||||
match reference.delete() {
|
||||
Ok(()) => return,
|
||||
Err(err) => {
|
||||
assert!(
|
||||
std::time::Instant::now() < deadline,
|
||||
"metadata branch for {run_id} never became writable: {err}"
|
||||
);
|
||||
std::thread::sleep(std::time::Duration::from_millis(50));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn init_repo_with_workflow(repo_dir: &Path) {
|
||||
std::fs::write(repo_dir.join("README.md"), "recovery test\n")
|
||||
.expect("recovery README fixture should write");
|
||||
std::fs::write(
|
||||
repo_dir.join("workflow.fabro"),
|
||||
"\
|
||||
digraph Recovery {
|
||||
start [shape=Mdiamond, label=\"Start\"]
|
||||
exit [shape=Msquare, label=\"Exit\"]
|
||||
plan [label=\"Plan\", shape=parallelogram, script=\"echo plan\"]
|
||||
build [label=\"Build\", shape=parallelogram, script=\"echo build\"]
|
||||
start -> plan -> build -> exit
|
||||
}
|
||||
",
|
||||
)
|
||||
.expect("recovery workflow fixture should write");
|
||||
|
||||
let init = std::process::Command::new("git")
|
||||
.args(["init"])
|
||||
.current_dir(repo_dir)
|
||||
.status()
|
||||
.expect("git init should launch");
|
||||
assert!(init.success(), "git init should succeed");
|
||||
|
||||
let add = std::process::Command::new("git")
|
||||
.args(["add", "README.md", "workflow.fabro"])
|
||||
.current_dir(repo_dir)
|
||||
.status()
|
||||
.expect("git add should launch");
|
||||
assert!(add.success(), "git add should succeed");
|
||||
|
||||
let commit = std::process::Command::new("git")
|
||||
.args([
|
||||
"-c",
|
||||
"user.name=Fabro",
|
||||
"-c",
|
||||
"user.email=noreply@fabro.sh",
|
||||
"commit",
|
||||
"-m",
|
||||
"init",
|
||||
])
|
||||
.current_dir(repo_dir)
|
||||
.status()
|
||||
.expect("git commit should launch");
|
||||
assert!(commit.success(), "git commit should succeed");
|
||||
|
||||
let remote_dir = repo_dir
|
||||
.parent()
|
||||
.expect("recovery repo should have a parent")
|
||||
.join(format!(
|
||||
"{}-remote.git",
|
||||
repo_dir
|
||||
.file_name()
|
||||
.and_then(|name| name.to_str())
|
||||
.unwrap_or("recovery")
|
||||
));
|
||||
let remote_init = std::process::Command::new("git")
|
||||
.args(["init", "--bare", "-q"])
|
||||
.arg(&remote_dir)
|
||||
.status()
|
||||
.expect("git init --bare should launch");
|
||||
assert!(remote_init.success(), "git init --bare should succeed");
|
||||
|
||||
let remote_add = std::process::Command::new("git")
|
||||
.args(["remote", "add", "origin"])
|
||||
.arg(&remote_dir)
|
||||
.current_dir(repo_dir)
|
||||
.status()
|
||||
.expect("git remote add should launch");
|
||||
assert!(remote_add.success(), "git remote add should succeed");
|
||||
|
||||
let push = std::process::Command::new("git")
|
||||
.args(["push", "-u", "origin", "HEAD:main"])
|
||||
.current_dir(repo_dir)
|
||||
.status()
|
||||
.expect("git push should launch");
|
||||
assert!(push.success(), "git push should succeed");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rewind_list_reports_empty_timeline_when_metadata_branch_is_missing() {
|
||||
let context = test_context!();
|
||||
context.ensure_home_server_auth_methods();
|
||||
let repo_dir = tempfile::tempdir().unwrap();
|
||||
let source_run_id = unique_run_id();
|
||||
|
||||
init_repo_with_workflow(repo_dir.path());
|
||||
|
||||
context
|
||||
.command()
|
||||
.current_dir(repo_dir.path())
|
||||
.args([
|
||||
"run",
|
||||
"--dry-run",
|
||||
"--no-retro",
|
||||
"--sandbox",
|
||||
"local",
|
||||
"--run-id",
|
||||
source_run_id.as_str(),
|
||||
"workflow.fabro",
|
||||
])
|
||||
.assert()
|
||||
.success();
|
||||
|
||||
delete_metadata_branch_when_ready(repo_dir.path(), &source_run_id);
|
||||
|
||||
assert!(
|
||||
list_metadata_run_ids(repo_dir.path()).is_empty(),
|
||||
"metadata branch should start missing"
|
||||
);
|
||||
|
||||
let mut rewind_list = context.command();
|
||||
rewind_list.current_dir(repo_dir.path());
|
||||
rewind_list.args(["rewind", &source_run_id, "--list"]);
|
||||
rewind_list.timeout(std::time::Duration::from_secs(15));
|
||||
fabro_snapshot!(context.filters(), rewind_list, @"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
----- stderr -----
|
||||
@ Node Details
|
||||
@1 start (no run commit)
|
||||
@2 plan
|
||||
@3 build
|
||||
");
|
||||
|
||||
assert!(
|
||||
list_metadata_run_ids(repo_dir.path()).is_empty(),
|
||||
"server timeline should not rebuild missing metadata"
|
||||
);
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue