mirror of
https://github.com/fabro-sh/fabro.git
synced 2026-08-28 05:27:41 +00:00
feat(cli): render fatal errors with miette
Wrap root CLI errors at the main boundary so fatal diagnostics use miette's styled renderer while preserving existing telemetry, exit codes, and auth help hints.
This commit is contained in:
parent
89bf987457
commit
7e64bf8b69
36 changed files with 259 additions and 96 deletions
99
Cargo.lock
generated
99
Cargo.lock
generated
|
|
@ -478,6 +478,15 @@ dependencies = [
|
|||
"windows-link 0.2.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "backtrace-ext"
|
||||
version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "537beee3be4a18fb023b570f80e3ae28003db9167a751266b259926e25539d50"
|
||||
dependencies = [
|
||||
"backtrace",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "base64"
|
||||
version = "0.22.1"
|
||||
|
|
@ -1663,6 +1672,7 @@ dependencies = [
|
|||
"insta",
|
||||
"jsonwebtoken",
|
||||
"libc",
|
||||
"miette",
|
||||
"object_store",
|
||||
"openssl",
|
||||
"paste",
|
||||
|
|
@ -3520,6 +3530,12 @@ dependencies = [
|
|||
"once_cell",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "is_ci"
|
||||
version = "1.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7655c9839580ee829dfacba1d1278c2b7883e50a277ff7541299489d6bdfdc45"
|
||||
|
||||
[[package]]
|
||||
name = "is_terminal_polyfill"
|
||||
version = "1.70.2"
|
||||
|
|
@ -3964,6 +3980,36 @@ dependencies = [
|
|||
"autocfg",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "miette"
|
||||
version = "7.6.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5f98efec8807c63c752b5bd61f862c165c115b0a35685bdcfd9238c7aeb592b7"
|
||||
dependencies = [
|
||||
"backtrace",
|
||||
"backtrace-ext",
|
||||
"cfg-if",
|
||||
"miette-derive",
|
||||
"owo-colors",
|
||||
"supports-color",
|
||||
"supports-hyperlinks",
|
||||
"supports-unicode",
|
||||
"terminal_size",
|
||||
"textwrap",
|
||||
"unicode-width 0.1.14",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "miette-derive"
|
||||
version = "7.6.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "db5b29714e950dbb20d5e6f74f9dcec4edbcc1067bb7f8ed198c097b8c1a818b"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.117",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "mime"
|
||||
version = "0.3.17"
|
||||
|
|
@ -4625,6 +4671,12 @@ version = "0.5.2"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1a80800c0488c3a21695ea981a54918fbb37abf04f4d0720c453632255e2ff0e"
|
||||
|
||||
[[package]]
|
||||
name = "owo-colors"
|
||||
version = "4.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d211803b9b6b570f68772237e415a029d5a50c65d382910b879fb19d3271f94d"
|
||||
|
||||
[[package]]
|
||||
name = "panic-message"
|
||||
version = "0.3.0"
|
||||
|
|
@ -6379,6 +6431,27 @@ version = "2.6.1"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292"
|
||||
|
||||
[[package]]
|
||||
name = "supports-color"
|
||||
version = "3.0.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c64fc7232dd8d2e4ac5ce4ef302b1d81e0b80d055b9d77c7c4f51f6aa4c867d6"
|
||||
dependencies = [
|
||||
"is_ci",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "supports-hyperlinks"
|
||||
version = "3.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e396b6523b11ccb83120b115a0b7366de372751aa6edf19844dfb13a6af97e91"
|
||||
|
||||
[[package]]
|
||||
name = "supports-unicode"
|
||||
version = "3.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b7401a30af6cb5818bb64852270bb722533397edcfc7344954a38f420819ece2"
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "1.0.109"
|
||||
|
|
@ -6534,12 +6607,32 @@ dependencies = [
|
|||
"unicode-width 0.1.14",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "terminal_size"
|
||||
version = "0.4.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "230a1b821ccbd75b185820a1f1ff7b14d21da1e442e22c0863ea5f08771a8874"
|
||||
dependencies = [
|
||||
"rustix",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "termtree"
|
||||
version = "0.5.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8f50febec83f5ee1df3015341d8bd429f2d1cc62bcba7ea2076759d315084683"
|
||||
|
||||
[[package]]
|
||||
name = "textwrap"
|
||||
version = "0.16.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c13547615a44dc9c452a8a534638acdf07120d4b6847c8178705da06306a3057"
|
||||
dependencies = [
|
||||
"unicode-linebreak",
|
||||
"unicode-width 0.2.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "thiserror"
|
||||
version = "1.0.69"
|
||||
|
|
@ -7109,6 +7202,12 @@ version = "1.0.24"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
|
||||
|
||||
[[package]]
|
||||
name = "unicode-linebreak"
|
||||
version = "0.1.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3b09c83c3c29d37506a3e260c08c03743a6bb66a9cd432c6934ab501a190571f"
|
||||
|
||||
[[package]]
|
||||
name = "unicode-segmentation"
|
||||
version = "1.12.0"
|
||||
|
|
|
|||
|
|
@ -78,6 +78,7 @@ object_store = { version = "0.12.5", features = ["aws"] }
|
|||
rust-embed = "8"
|
||||
percent-encoding = "2"
|
||||
minijinja = "2"
|
||||
miette = { version = "7.6", features = ["fancy"] }
|
||||
fabro-http = { path = "lib/crates/fabro-http" }
|
||||
fabro-redact = { path = "lib/crates/fabro-redact" }
|
||||
fabro-static = { path = "lib/crates/fabro-static" }
|
||||
|
|
|
|||
|
|
@ -510,7 +510,7 @@ Solid arrows are real dependencies. Dashed lines show phases that are independen
|
|||
|
||||
### Phase 4 — miette CLI diagnostics wrapper
|
||||
|
||||
- [ ] **Unit 4.1: Wrap `fabro-cli` root error with `miette::Diagnostic`**
|
||||
- [x] **Unit 4.1: Wrap `fabro-cli` root error with `miette::Diagnostic`**
|
||||
|
||||
**Goal:** Render CLI errors through `miette` at the `main` boundary, preserving existing exit-class hint behavior while gaining styled chained errors and `help:` footer rendering. Source-highlighted output is out of scope until fabro has errors that carry span/source context.
|
||||
|
||||
|
|
|
|||
|
|
@ -56,6 +56,7 @@ console.workspace = true
|
|||
indicatif.workspace = true
|
||||
daytona-sdk.workspace = true
|
||||
anyhow.workspace = true
|
||||
miette.workspace = true
|
||||
dotenvy.workspace = true
|
||||
tokio.workspace = true
|
||||
tracing.workspace = true
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ mod user_config;
|
|||
|
||||
#[cfg(test)]
|
||||
use std::ffi::OsString;
|
||||
use std::fmt::{self, Debug, Display};
|
||||
|
||||
use anyhow::Result;
|
||||
use args::{
|
||||
|
|
@ -94,6 +95,8 @@ async fn main() {
|
|||
None
|
||||
};
|
||||
|
||||
install_miette_hook();
|
||||
|
||||
tel_panic::install_panic_hook();
|
||||
fabro_telemetry::init_cli();
|
||||
|
||||
|
|
@ -135,38 +138,69 @@ async fn main() {
|
|||
fabro_telemetry::shutdown();
|
||||
|
||||
if let Err(err) = result {
|
||||
let style = console::Style::new().red().bold();
|
||||
for (i, cause) in err.chain().enumerate() {
|
||||
let text = cause.to_string();
|
||||
if i == 0 {
|
||||
for (j, line) in text.lines().enumerate() {
|
||||
if j == 0 {
|
||||
eprintln!("{} {line}", style.apply_to("error:"));
|
||||
} else {
|
||||
eprintln!(" {line}");
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for line in text.lines() {
|
||||
eprintln!(" > {line}");
|
||||
}
|
||||
}
|
||||
}
|
||||
let json_mode = raw_args.iter().any(|a| a == "--json");
|
||||
if !json_mode && exit::exit_class_for(&err) == Some(ExitClass::AuthRequired) {
|
||||
let hint_style = console::Style::new().cyan().bold();
|
||||
let cmd_style = console::Style::new().bold();
|
||||
eprintln!();
|
||||
eprintln!(
|
||||
"{} Run {} to authenticate.",
|
||||
hint_style.apply_to("hint:"),
|
||||
cmd_style.apply_to("`fabro auth login`"),
|
||||
);
|
||||
}
|
||||
eprintln!(
|
||||
"{:?}",
|
||||
miette::Report::new(CliDiagnostic::new(err, !json_mode))
|
||||
);
|
||||
std::process::exit(exit_code);
|
||||
}
|
||||
}
|
||||
|
||||
fn install_miette_hook() {
|
||||
let _ = miette::set_hook(Box::new(|_| {
|
||||
Box::new(
|
||||
miette::MietteHandlerOpts::new()
|
||||
.with_cause_chain()
|
||||
.wrap_lines(false)
|
||||
.break_words(false)
|
||||
.build(),
|
||||
)
|
||||
}));
|
||||
}
|
||||
|
||||
struct CliDiagnostic {
|
||||
err: anyhow::Error,
|
||||
show_auth_hint: bool,
|
||||
}
|
||||
|
||||
impl CliDiagnostic {
|
||||
fn new(err: anyhow::Error, show_auth_hint: bool) -> Self {
|
||||
Self {
|
||||
err,
|
||||
show_auth_hint,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for CliDiagnostic {
|
||||
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
Display::fmt(&self.err, formatter)
|
||||
}
|
||||
}
|
||||
|
||||
impl Debug for CliDiagnostic {
|
||||
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
Debug::fmt(&self.err, formatter)
|
||||
}
|
||||
}
|
||||
|
||||
impl std::error::Error for CliDiagnostic {
|
||||
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
|
||||
self.err.source()
|
||||
}
|
||||
}
|
||||
|
||||
impl miette::Diagnostic for CliDiagnostic {
|
||||
fn help<'a>(&'a self) -> Option<Box<dyn Display + 'a>> {
|
||||
if self.show_auth_hint && exit::exit_class_for(&self.err) == Some(ExitClass::AuthRequired) {
|
||||
Some(Box::new("Run `fabro auth login` to authenticate."))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[expect(
|
||||
clippy::disallowed_methods,
|
||||
reason = "CLI main reads documented process-env controls before telemetry and worker dispatch."
|
||||
|
|
|
|||
|
|
@ -15,6 +15,6 @@ fn artifact_cp_empty_run_reports_no_artifacts() {
|
|||
exit_code: 1
|
||||
----- stdout -----
|
||||
----- stderr -----
|
||||
error: No artifacts found for this run
|
||||
× No artifacts found for this run
|
||||
");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -462,7 +462,7 @@ fn create_invalid_workflow_fails_without_creating_run() {
|
|||
exit_code: 1
|
||||
----- stdout -----
|
||||
----- stderr -----
|
||||
error: Validation failed
|
||||
× Validation failed
|
||||
");
|
||||
|
||||
let run_count = run_count_for_test_case(&context);
|
||||
|
|
|
|||
|
|
@ -43,7 +43,7 @@ fn diff_completed_run_without_changes_reports_no_patch() {
|
|||
exit_code: 1
|
||||
----- stdout -----
|
||||
----- stderr -----
|
||||
error: Run completed but no stored diff exists — the run may not have produced any changes
|
||||
× Run completed but no stored diff exists — the run may not have produced any changes
|
||||
");
|
||||
}
|
||||
|
||||
|
|
@ -59,7 +59,7 @@ fn diff_missing_node_diff_reports_helpful_error() {
|
|||
exit_code: 1
|
||||
----- stdout -----
|
||||
----- stderr -----
|
||||
error: No diff found for node 'missing' — check the node ID and try again
|
||||
× No diff found for node 'missing' — check the node ID and try again
|
||||
");
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -303,7 +303,7 @@ fn dump_rejects_non_empty_output_dir() {
|
|||
exit_code: 1
|
||||
----- stdout -----
|
||||
----- stderr -----
|
||||
error: output path [TEMP_DIR]/nonempty already exists and is not an empty directory; remove it first or choose a different path
|
||||
× output path [TEMP_DIR]/nonempty already exists and is not an empty directory; remove it first or choose a different path
|
||||
");
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -103,7 +103,7 @@ fn exec_missing_api_key_exits_with_error() {
|
|||
exit_code: 1
|
||||
----- stdout -----
|
||||
----- stderr -----
|
||||
error: LLM credentials not configured for provider 'anthropic'
|
||||
× LLM credentials not configured for provider 'anthropic'
|
||||
");
|
||||
}
|
||||
|
||||
|
|
@ -129,7 +129,7 @@ fn exec_uses_user_config_defaults() {
|
|||
exit_code: 1
|
||||
----- stdout -----
|
||||
----- stderr -----
|
||||
error: LLM credentials not configured for provider 'openai'
|
||||
× LLM credentials not configured for provider 'openai'
|
||||
");
|
||||
}
|
||||
|
||||
|
|
@ -350,6 +350,12 @@ fn exec_server_target_auth_failure_exits_with_4() {
|
|||
fatal_error_line(&output.stderr),
|
||||
"LLM error: Authentication error for openai: Authentication required."
|
||||
);
|
||||
let stderr = String::from_utf8_lossy(&output.stderr);
|
||||
let stderr = console::strip_ansi_codes(&stderr);
|
||||
assert!(
|
||||
stderr.contains("Run `fabro auth login` to authenticate."),
|
||||
"auth failures should retain the login help footer:\n{stderr}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
|
|||
|
|
@ -48,7 +48,7 @@ fn fork_outside_git_repo_errors() {
|
|||
exit_code: 1
|
||||
----- stdout -----
|
||||
----- stderr -----
|
||||
error: No run found matching '[ULID]' (tried run ID prefix and workflow name)
|
||||
× No run found matching '[ULID]' (tried run ID prefix and workflow name)
|
||||
");
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -86,6 +86,6 @@ fn graph_invalid_workflow_fails_after_diagnostics() {
|
|||
----- stderr -----
|
||||
error: Pipeline must have exactly one start node (shape=Mdiamond or id start/Start) (start_node)
|
||||
error [node: exit]: Exit node 'exit' has 1 outgoing edge(s) but must have none (exit_no_outgoing)
|
||||
error: Validation failed
|
||||
× Validation failed
|
||||
");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -181,7 +181,7 @@ fn list_invalid_provider_errors() {
|
|||
exit_code: 1
|
||||
----- stdout -----
|
||||
----- stderr -----
|
||||
error: unknown provider: not-a-provider
|
||||
× unknown provider: not-a-provider
|
||||
");
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -52,7 +52,7 @@ fn model_test_unknown_model_errors() {
|
|||
----- stdout -----
|
||||
----- stderr -----
|
||||
Testing nonexistent-model-xyz... done
|
||||
error: Unknown model: nonexistent-model-xyz
|
||||
× Unknown model: nonexistent-model-xyz
|
||||
");
|
||||
}
|
||||
|
||||
|
|
@ -71,7 +71,7 @@ fn single_model_skip_exits_nonzero() {
|
|||
gemini-3.1-pro-preview gemini gemini-pro 1m $2.0 / $12.0 85 tok/s not configured
|
||||
----- stderr -----
|
||||
Testing gemini-3.1-pro-preview... done
|
||||
error: 1 model(s) failed
|
||||
× 1 model(s) failed
|
||||
");
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -125,11 +125,11 @@ fn parse_invalid_dot_fails_cleanly() {
|
|||
let mut cmd = context.command();
|
||||
cmd.args(["parse", "bad.fabro"]);
|
||||
|
||||
fabro_snapshot!(context.filters(), cmd, @"
|
||||
fabro_snapshot!(context.filters(), cmd, @r#"
|
||||
success: false
|
||||
exit_code: 1
|
||||
----- stdout -----
|
||||
----- stderr -----
|
||||
error: Parse error: grammar error: Parsing Error: Error { input: \"\", code: Char }
|
||||
");
|
||||
× Parse error: grammar error: Parsing Error: Error { input: "", code: Char }
|
||||
"#);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -52,7 +52,7 @@ fn pr_create_nongit_run_reports_missing_repo_origin() {
|
|||
exit_code: 1
|
||||
----- stdout -----
|
||||
----- stderr -----
|
||||
error: Run has no repo origin URL — pull request creation requires git metadata.
|
||||
× Run has no repo origin URL — pull request creation requires git metadata.
|
||||
");
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -51,7 +51,7 @@ fn pr_view_missing_pull_request_json_errors() {
|
|||
exit_code: 1
|
||||
----- stdout -----
|
||||
----- stderr -----
|
||||
error: No pull request found in store. Create one first with: fabro pr create [ULID]
|
||||
× No pull request found in store. Create one first with: fabro pr create [ULID]
|
||||
");
|
||||
}
|
||||
|
||||
|
|
@ -107,7 +107,7 @@ fn pr_view_reads_pull_request_from_store_without_pull_request_json() {
|
|||
exit_code: 1
|
||||
----- stdout -----
|
||||
----- stderr -----
|
||||
error: GitHub integration unavailable on server.
|
||||
× GitHub integration unavailable on server.
|
||||
");
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -52,7 +52,7 @@ fn preflight_invalid_workflow_fails_with_validation_output() {
|
|||
Graph: [FIXTURES]/invalid.fabro
|
||||
error: Pipeline must have exactly one start node (shape=Mdiamond or id start/Start) (start_node)
|
||||
error [node: exit]: Exit node 'exit' has 1 outgoing edge(s) but must have none (exit_no_outgoing)
|
||||
error: Validation failed
|
||||
× Validation failed
|
||||
");
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -69,6 +69,6 @@ fn test_repo_deinit_fails_when_not_initialized() {
|
|||
exit_code: 1
|
||||
----- stdout -----
|
||||
----- stderr -----
|
||||
error: not initialized — .fabro/project.toml not found
|
||||
× not initialized — .fabro/project.toml not found
|
||||
");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -122,7 +122,7 @@ fn repo_init_rejects_already_initialized_repo() {
|
|||
exit_code: 1
|
||||
----- stdout -----
|
||||
----- stderr -----
|
||||
error: already initialized — .fabro/project.toml exists at [TEMP_DIR]/.fabro/project.toml
|
||||
× already initialized — .fabro/project.toml exists at [TEMP_DIR]/.fabro/project.toml
|
||||
");
|
||||
}
|
||||
|
||||
|
|
@ -137,6 +137,6 @@ fn repo_init_errors_outside_git_repo() {
|
|||
exit_code: 1
|
||||
----- stdout -----
|
||||
----- stderr -----
|
||||
error: not a git repository — run `git init` first
|
||||
× not a git repository — run `git init` first
|
||||
");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -48,7 +48,7 @@ fn rewind_outside_git_repo_errors() {
|
|||
exit_code: 1
|
||||
----- stdout -----
|
||||
----- stderr -----
|
||||
error: No run found matching '[ULID]' (tried run ID prefix and workflow name)
|
||||
× No run found matching '[ULID]' (tried run ID prefix and workflow name)
|
||||
");
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -85,7 +85,7 @@ fn rm_rejects_submitted_run_without_force() {
|
|||
----- stdout -----
|
||||
----- stderr -----
|
||||
cannot remove active run [ULID] (status: submitted, use force=true or --force to force)
|
||||
error: some runs could not be removed
|
||||
× some runs could not be removed
|
||||
");
|
||||
}
|
||||
|
||||
|
|
@ -286,7 +286,7 @@ fn rm_without_force_uses_resolve_then_surfaces_server_conflict() {
|
|||
----- stdout -----
|
||||
----- stderr -----
|
||||
cannot remove active run [ULID] (status: running, use force=true or --force to force)
|
||||
error: some runs could not be removed
|
||||
× some runs could not be removed
|
||||
");
|
||||
resolve_mock.assert();
|
||||
delete_mock.assert();
|
||||
|
|
@ -310,7 +310,7 @@ fn rm_partial_failure_reports_which_identifiers_failed() {
|
|||
----- stderr -----
|
||||
[ULID]
|
||||
error: does-not-exist: No run found matching 'does-not-exist' (tried run ID prefix and workflow name)
|
||||
error: some runs could not be removed
|
||||
× some runs could not be removed
|
||||
");
|
||||
assert!(
|
||||
!run.run_dir.exists(),
|
||||
|
|
|
|||
|
|
@ -653,7 +653,7 @@ digraph Test {
|
|||
exit_code: 1
|
||||
----- stdout -----
|
||||
----- stderr -----
|
||||
error: Precondition failed: run already finished successfully — nothing to resume
|
||||
× Precondition failed: run already finished successfully — nothing to resume
|
||||
");
|
||||
|
||||
let inspect_after = context
|
||||
|
|
|
|||
|
|
@ -50,7 +50,7 @@ fn sandbox_cp_run_without_sandbox_json_errors_cleanly() {
|
|||
exit_code: 1
|
||||
----- stdout -----
|
||||
----- stderr -----
|
||||
error: Run has no active sandbox.
|
||||
× Run has no active sandbox.
|
||||
");
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -71,7 +71,7 @@ fn sandbox_preview_rejects_non_daytona_run() {
|
|||
exit_code: 1
|
||||
----- stdout -----
|
||||
----- stderr -----
|
||||
error: Sandbox provider does not support this capability.
|
||||
× Sandbox provider does not support this capability.
|
||||
");
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -44,7 +44,7 @@ fn sandbox_ssh_rejects_non_daytona_run() {
|
|||
exit_code: 1
|
||||
----- stdout -----
|
||||
----- stderr -----
|
||||
error: Sandbox provider does not support this capability.
|
||||
× Sandbox provider does not support this capability.
|
||||
");
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -109,7 +109,7 @@ fn test_secret_rm_missing_key() {
|
|||
exit_code: 1
|
||||
----- stdout -----
|
||||
----- stderr -----
|
||||
error: secret not found: NOPE
|
||||
× secret not found: NOPE
|
||||
");
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -19,6 +19,8 @@ use fabro_test::{
|
|||
};
|
||||
use fabro_util::dev_token;
|
||||
|
||||
use crate::support::fatal_error_line;
|
||||
|
||||
const TEST_DEV_TOKEN: &str =
|
||||
"fabro_dev_abababababababababababababababababababababababababababababababab";
|
||||
const TEST_SESSION_SECRET: &str =
|
||||
|
|
@ -120,13 +122,28 @@ fn run_startup_failure(context: &TestContext, mode: ServerStartMode, case: &Star
|
|||
mode.name(),
|
||||
String::from_utf8_lossy(&output.stdout)
|
||||
);
|
||||
let stderr = String::from_utf8_lossy(&output.stderr);
|
||||
let stderr = console::strip_ansi_codes(&stderr);
|
||||
let mut expected_lines = case.expected_error.lines();
|
||||
let expected_fatal_line = expected_lines.next().unwrap_or(case.expected_error);
|
||||
assert_eq!(
|
||||
String::from_utf8_lossy(&output.stderr),
|
||||
format!("error: {}\n", case.expected_error),
|
||||
"unexpected stderr for {} in {} mode",
|
||||
fatal_error_line(&output.stderr),
|
||||
expected_fatal_line,
|
||||
"unexpected fatal line for {} in {} mode\nstderr:\n{}",
|
||||
case.name,
|
||||
mode.name()
|
||||
mode.name(),
|
||||
stderr
|
||||
);
|
||||
for expected_line in expected_lines {
|
||||
let expected_line = expected_line.trim_start();
|
||||
assert!(
|
||||
stderr.contains(expected_line),
|
||||
"missing expected stderr detail for {} in {} mode: {expected_line}\nstderr:\n{}",
|
||||
case.name,
|
||||
mode.name(),
|
||||
stderr
|
||||
);
|
||||
}
|
||||
|
||||
let log_path = storage_dir.join("logs/server.log");
|
||||
match mode {
|
||||
|
|
@ -348,7 +365,7 @@ fn start_already_running_exits_with_error() {
|
|||
exit_code: 1
|
||||
----- stdout -----
|
||||
----- stderr -----
|
||||
error: Server already running (pid [PID]) on [SOCKET_PATH]
|
||||
× Server already running (pid [PID]) on [SOCKET_PATH]
|
||||
");
|
||||
|
||||
context
|
||||
|
|
@ -765,8 +782,8 @@ fn start_with_missing_explicit_flag_config_errors_without_entering_install_mode(
|
|||
exit_code: 1
|
||||
----- stdout -----
|
||||
----- stderr -----
|
||||
error: reading config file [MISSING_CONFIG]: No such file or directory (os error 2)
|
||||
> No such file or directory (os error 2)
|
||||
× reading config file [MISSING_CONFIG]: No such file or directory (os error 2)
|
||||
╰─▶ No such file or directory (os error 2)
|
||||
");
|
||||
}
|
||||
|
||||
|
|
@ -793,8 +810,8 @@ fn start_with_missing_env_config_errors_without_entering_install_mode() {
|
|||
exit_code: 1
|
||||
----- stdout -----
|
||||
----- stderr -----
|
||||
error: reading config file [MISSING_CONFIG]: No such file or directory (os error 2)
|
||||
> No such file or directory (os error 2)
|
||||
× reading config file [MISSING_CONFIG]: No such file or directory (os error 2)
|
||||
╰─▶ No such file or directory (os error 2)
|
||||
");
|
||||
}
|
||||
|
||||
|
|
@ -819,18 +836,19 @@ fn start_with_malformed_default_settings_errors_without_entering_install_mode()
|
|||
exit_code: 1
|
||||
----- stdout -----
|
||||
----- stderr -----
|
||||
error: Failed to parse settings file at [HOME_DIR]/.fabro/settings.toml: settings file is not valid TOML: TOML parse error at line 1, column 15
|
||||
|
|
||||
1 | [server.listen
|
||||
| ^
|
||||
invalid table header
|
||||
expected `.`, `]`
|
||||
> settings file is not valid TOML: TOML parse error at line 1, column 15
|
||||
> |
|
||||
> 1 | [server.listen
|
||||
> | ^
|
||||
> invalid table header
|
||||
> expected `.`, `]`
|
||||
× Failed to parse settings file at [HOME_DIR]/.fabro/settings.toml: settings file is not valid TOML: TOML parse error at line 1, column 15
|
||||
│ |
|
||||
│ 1 | [server.listen
|
||||
│ | ^
|
||||
│ invalid table header
|
||||
│ expected `.`, `]`
|
||||
|
||||
╰─▶ settings file is not valid TOML: TOML parse error at line 1, column 15
|
||||
|
|
||||
1 | [server.listen
|
||||
| ^
|
||||
invalid table header
|
||||
expected `.`, `]`
|
||||
");
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -172,7 +172,7 @@ fn start_rejects_already_active_or_completed_run() {
|
|||
exit_code: 1
|
||||
----- stdout -----
|
||||
----- stderr -----
|
||||
error: an engine process is still running for this run — cannot start
|
||||
× an engine process is still running for this run — cannot start
|
||||
");
|
||||
|
||||
gate.release();
|
||||
|
|
@ -185,7 +185,7 @@ fn start_rejects_already_active_or_completed_run() {
|
|||
exit_code: 1
|
||||
----- stdout -----
|
||||
----- stderr -----
|
||||
error: cannot start run: status is succeeded(completed), expected submitted
|
||||
× cannot start run: status is succeeded(completed), expected submitted
|
||||
");
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -97,8 +97,8 @@ fn upgrade_invalid_version_errors() {
|
|||
exit_code: 1
|
||||
----- stdout -----
|
||||
----- stderr -----
|
||||
error: invalid version: not-a-semver
|
||||
> unexpected character 'n' while parsing major version number
|
||||
× invalid version: not-a-semver
|
||||
╰─▶ unexpected character 'n' while parsing major version number
|
||||
");
|
||||
}
|
||||
|
||||
|
|
@ -209,7 +209,7 @@ fn upgrade_brew_install_refuses_and_prints_brew_command() {
|
|||
----- stderr -----
|
||||
fabro was installed via Homebrew.
|
||||
Run `brew upgrade fabro` to update.
|
||||
error: refusing to overwrite a Homebrew-managed binary
|
||||
× refusing to overwrite a Homebrew-managed binary
|
||||
");
|
||||
}
|
||||
|
||||
|
|
@ -245,6 +245,6 @@ fn upgrade_brew_install_rejects_version_flag() {
|
|||
exit_code: 1
|
||||
----- stdout -----
|
||||
----- stderr -----
|
||||
error: fabro is managed by Homebrew (formula `fabro`); Homebrew selects the version and channel. Use `brew upgrade fabro` (or reinstall with a different formula) instead of `fabro upgrade --version`/`--prerelease`/`--force`.
|
||||
× fabro is managed by Homebrew (formula `fabro`); Homebrew selects the version and channel. Use `brew upgrade fabro` (or reinstall with a different formula) instead of `fabro upgrade --version`/`--prerelease`/`--force`.
|
||||
");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -146,6 +146,6 @@ fn invalid() {
|
|||
Graph: [FIXTURES]/invalid.fabro
|
||||
error: Pipeline must have exactly one start node (shape=Mdiamond or id start/Start) (start_node)
|
||||
error [node: exit]: Exit node 'exit' has 1 outgoing edge(s) but must have none (exit_no_outgoing)
|
||||
error: Validation failed
|
||||
× Validation failed
|
||||
");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -132,7 +132,7 @@ fn wait_submitted_run_times_out() {
|
|||
exit_code: 1
|
||||
----- stdout -----
|
||||
----- stderr -----
|
||||
error: Timed out after 1s waiting for run '[ULID]'
|
||||
× Timed out after 1s waiting for run '[ULID]'
|
||||
");
|
||||
}
|
||||
|
||||
|
|
@ -189,7 +189,7 @@ fn wait_blocked_run_times_out_without_treating_it_as_terminal() {
|
|||
exit_code: 1
|
||||
----- stdout -----
|
||||
----- stderr -----
|
||||
error: Timed out after 1s waiting for run '[ULID]'
|
||||
× Timed out after 1s waiting for run '[ULID]'
|
||||
");
|
||||
resolve_run.assert();
|
||||
assert!(
|
||||
|
|
|
|||
|
|
@ -140,7 +140,7 @@ fn workflow_create_rejects_existing_workflow() {
|
|||
exit_code: 1
|
||||
----- stdout -----
|
||||
----- stderr -----
|
||||
error: Workflow 'existing' already exists at [TEMP_DIR]/project/.fabro/workflows/existing
|
||||
× Workflow 'existing' already exists at [TEMP_DIR]/project/.fabro/workflows/existing
|
||||
");
|
||||
}
|
||||
|
||||
|
|
@ -155,7 +155,7 @@ fn workflow_create_errors_without_project_config() {
|
|||
exit_code: 1
|
||||
----- stdout -----
|
||||
----- stderr -----
|
||||
error: No .fabro/project.toml found in [TEMP_DIR] or any parent directory
|
||||
× No .fabro/project.toml found in [TEMP_DIR] or any parent directory
|
||||
");
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -37,7 +37,7 @@ fn workflow_list_errors_without_project_config() {
|
|||
exit_code: 1
|
||||
----- stdout -----
|
||||
----- stderr -----
|
||||
error: No .fabro/project.toml found in [TEMP_DIR] or any parent directory
|
||||
× No .fabro/project.toml found in [TEMP_DIR] or any parent directory
|
||||
");
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -153,7 +153,7 @@ fn artifact_commands_share_populated_run_fixture() {
|
|||
exit_code: 1
|
||||
----- stdout -----
|
||||
----- stderr -----
|
||||
error: Path 'assets/retry/report.txt' matches multiple artifacts: create_colliding:retry_1, retry_assets:retry_1, retry_assets:retry_2. Use --node and/or --retry to disambiguate.
|
||||
× Path 'assets/retry/report.txt' matches multiple artifacts: create_colliding:retry_1, retry_assets:retry_1, retry_assets:retry_2. Use --node and/or --retry to disambiguate.
|
||||
");
|
||||
|
||||
let flat_dest = context.temp_dir.join("artifact-flat");
|
||||
|
|
@ -169,6 +169,6 @@ fn artifact_commands_share_populated_run_fixture() {
|
|||
exit_code: 1
|
||||
----- stdout -----
|
||||
----- stderr -----
|
||||
error: Filename collision: 'summary.txt' exists in both create_assets:retry_1 and create_colliding:retry_1. Use --tree to preserve directory structure, or --node and/or --retry to filter.
|
||||
× Filename collision: 'summary.txt' exists in both create_assets:retry_1 and create_colliding:retry_1. Use --tree to preserve directory structure, or --node and/or --retry to filter.
|
||||
");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -26,8 +26,12 @@ pub(crate) fn fatal_error_line(stderr: &[u8]) -> String {
|
|||
console::strip_ansi_codes(&stderr)
|
||||
.lines()
|
||||
.rev()
|
||||
.find_map(|line| line.strip_prefix("error: ").map(ToOwned::to_owned))
|
||||
.expect("stderr should contain a fatal `error:` line")
|
||||
.find_map(|line| {
|
||||
line.strip_prefix("error: ")
|
||||
.or_else(|| line.trim_start().strip_prefix("× "))
|
||||
.map(ToOwned::to_owned)
|
||||
})
|
||||
.expect("stderr should contain a fatal error line")
|
||||
}
|
||||
|
||||
pub(crate) fn unique_run_id() -> String {
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue