mirror of
https://github.com/fabro-sh/fabro.git
synced 2026-10-09 03:20:56 +00:00
Phase 2/3 of the std::fs lint initiative (Phase 1 refactors landed in
commit 9d1c0d98c).
clippy.toml additions (appended to disallowed-methods):
std::fs::read, read_to_string, write, read_dir, copy, canonicalize
std::fs::File::open, File::create, File::create_new
std::fs::OpenOptions::open
File::options was deliberately excluded — it returns an OpenOptions
builder with no syscall. OpenOptions::open is where the block happens.
Non-blocking std::fs items (metadata, exists, create_dir_all, remove_*,
rename, and all std::fs types) remain legal.
Annotation policy (per updated plan):
- Mixed async/sync production source: function- or statement-scoped
#[expect(...)] so future accidental Tokio-path regressions in the
same file still fire.
- Fully-sync production source, test modules, integration tests,
build.rs: file-level #![expect(...)].
- Every #[expect] has a specific reason identifying the sync context.
Annotations added in ~90 files across the workspace. Notable narrow
placements: fabro-server server.rs current_server_target,
build_disk_usage_response, create_test_app_state_with_session_key;
fabro-server install.rs read_to_string rollback snapshot;
fabro-sandbox local.rs list_recursive; fabro-agent cli.rs FOLLOW-UP on
the JSON-stdout writer; fabro-llm providers/common.rs FOLLOW-UP for
load_file_as_base64 (7 translator call sites; revisit if file:// URL
usage grows).
build.rs blanket allows: fabro-api/build.rs, fabro-util/build.rs.
Pre-existing unrelated nightly-clippy warnings fixed under scope:
fabro-sandbox sandbox_spec.rs (unused_imports, unused_async),
reconnect.rs (unused_variables, unused_async).
Verified: cargo +nightly-2026-04-14 clippy --workspace --all-targets
-- -D warnings passes; fmt clean; 4129/4131 tests pass (two known
flakes under parallel nextest load, both pass individually and are
unrelated to this change).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
246 lines
7.1 KiB
Rust
246 lines
7.1 KiB
Rust
#![expect(
|
|
clippy::disallowed_methods,
|
|
reason = "integration tests stage fixtures with sync std::fs; test infrastructure, not Tokio-hot path"
|
|
)]
|
|
|
|
use assert_cmd::Command;
|
|
use fabro_test::{TestContext, fabro_snapshot, test_context};
|
|
|
|
fn hard_link_or_copy(src: &std::path::Path, dest: &std::path::Path) {
|
|
if std::fs::hard_link(src, dest).is_ok() {
|
|
return;
|
|
}
|
|
|
|
std::fs::copy(src, dest).expect("copy test binary into fake Cellar");
|
|
#[cfg(unix)]
|
|
{
|
|
use std::os::unix::fs::PermissionsExt;
|
|
|
|
let perms = std::fs::metadata(src)
|
|
.expect("read source binary metadata")
|
|
.permissions()
|
|
.mode();
|
|
std::fs::set_permissions(dest, std::fs::Permissions::from_mode(perms))
|
|
.expect("preserve executable permissions");
|
|
}
|
|
}
|
|
|
|
fn brew_command(context: &TestContext, formula: &str, version: &str) -> Command {
|
|
let bin_dir = context
|
|
.temp_dir
|
|
.join("Cellar")
|
|
.join(formula)
|
|
.join(version)
|
|
.join("bin");
|
|
std::fs::create_dir_all(&bin_dir).expect("create fake Cellar bin dir");
|
|
let brew_fabro = bin_dir.join("fabro");
|
|
hard_link_or_copy(
|
|
std::path::Path::new(env!("CARGO_BIN_EXE_fabro")),
|
|
&brew_fabro,
|
|
);
|
|
|
|
let mut cmd = Command::new(&brew_fabro);
|
|
cmd.current_dir(&context.temp_dir);
|
|
for (key, _) in std::env::vars_os() {
|
|
if let Some(s) = key.to_str() {
|
|
if s.starts_with("FABRO_") {
|
|
cmd.env_remove(&key);
|
|
}
|
|
}
|
|
}
|
|
cmd.env("NO_COLOR", "1");
|
|
cmd.env("HOME", &context.home_dir);
|
|
cmd.env("FABRO_NO_UPGRADE_CHECK", "true")
|
|
.env("FABRO_HTTP_PROXY_POLICY", "disabled")
|
|
.env("FABRO_TELEMETRY", "off")
|
|
.env("FABRO_SERVER_MAX_CONCURRENT_RUNS", "64")
|
|
.env("FABRO_TEST_IN_MEMORY_STORE", "1");
|
|
cmd
|
|
}
|
|
|
|
#[test]
|
|
fn help() {
|
|
let context = test_context!();
|
|
let mut cmd = context.command();
|
|
cmd.args(["upgrade", "--help"]);
|
|
fabro_snapshot!(context.filters(), cmd, @r#"
|
|
success: true
|
|
exit_code: 0
|
|
----- stdout -----
|
|
Upgrade fabro to the latest version
|
|
|
|
Usage: fabro upgrade [OPTIONS]
|
|
|
|
Options:
|
|
--json Output as JSON [env: FABRO_JSON=]
|
|
--version <VERSION> Target version (e.g. "0.5.0", "v0.5.0", or "v0.177.0-alpha.1")
|
|
--debug Enable DEBUG-level logging (default is INFO) [env: FABRO_DEBUG=]
|
|
--prerelease Include prereleases (alpha, beta, rc) when selecting the latest version
|
|
--force Upgrade even if already on the target version
|
|
--no-upgrade-check Disable automatic upgrade check [env: FABRO_NO_UPGRADE_CHECK=true]
|
|
--dry-run Preview what would happen without making changes
|
|
--quiet Suppress non-essential output [env: FABRO_QUIET=]
|
|
--verbose Enable verbose output [env: FABRO_VERBOSE=]
|
|
-h, --help Print help
|
|
----- stderr -----
|
|
"#);
|
|
}
|
|
|
|
#[test]
|
|
fn upgrade_invalid_version_errors() {
|
|
let context = test_context!();
|
|
let mut cmd = context.command();
|
|
cmd.args(["upgrade", "--version", "not-a-semver"]);
|
|
|
|
fabro_snapshot!(context.filters(), cmd, @"
|
|
success: false
|
|
exit_code: 1
|
|
----- stdout -----
|
|
----- stderr -----
|
|
error: invalid version: not-a-semver
|
|
> unexpected character 'n' while parsing major version number
|
|
");
|
|
}
|
|
|
|
#[test]
|
|
fn upgrade_already_on_current_version_short_circuits() {
|
|
let context = test_context!();
|
|
let mut filters = context.filters();
|
|
filters.push((
|
|
regex::escape(env!("CARGO_PKG_VERSION")),
|
|
"[VERSION]".to_string(),
|
|
));
|
|
let mut cmd = context.command();
|
|
cmd.args(["upgrade", "--version", env!("CARGO_PKG_VERSION")]);
|
|
|
|
fabro_snapshot!(filters, cmd, @"
|
|
success: true
|
|
exit_code: 0
|
|
----- stdout -----
|
|
----- stderr -----
|
|
Already on version [VERSION]
|
|
");
|
|
}
|
|
|
|
#[test]
|
|
fn upgrade_dry_run_prefers_latest_stable_release_for_gh_backend() {
|
|
let context = test_context!();
|
|
let fake_bin = context.temp_dir.join("fake-bin");
|
|
std::fs::create_dir_all(&fake_bin).unwrap();
|
|
|
|
let fake_gh = fake_bin.join("gh");
|
|
std::fs::write(
|
|
&fake_gh,
|
|
r#"#!/bin/sh
|
|
set -eu
|
|
|
|
case "$1" in
|
|
--version)
|
|
echo "gh version 2.89.0"
|
|
;;
|
|
auth)
|
|
test "$2" = "status"
|
|
;;
|
|
api)
|
|
test "$2" = "repos/fabro-sh/fabro/releases/latest"
|
|
test "$3" = "--jq"
|
|
test "$4" = ".tag_name"
|
|
echo "v999.0.0"
|
|
;;
|
|
release)
|
|
test "$2" = "view"
|
|
echo "v999.0.1-alpha.1"
|
|
;;
|
|
*)
|
|
echo "unexpected gh invocation: $*" >&2
|
|
exit 1
|
|
;;
|
|
esac
|
|
"#,
|
|
)
|
|
.unwrap();
|
|
|
|
#[cfg(unix)]
|
|
{
|
|
use std::os::unix::fs::PermissionsExt;
|
|
|
|
std::fs::set_permissions(&fake_gh, std::fs::Permissions::from_mode(0o755)).unwrap();
|
|
}
|
|
|
|
let mut filters = context.filters();
|
|
filters.push((
|
|
regex::escape(env!("CARGO_PKG_VERSION")),
|
|
"[VERSION]".to_string(),
|
|
));
|
|
filters.push((
|
|
"(aarch64-apple-darwin|x86_64-unknown-linux-gnu|aarch64-unknown-linux-gnu|x86_64-unknown-linux-musl|aarch64-unknown-linux-musl)".to_string(),
|
|
"[TARGET]".to_string(),
|
|
));
|
|
|
|
let path = format!("{}:{}", fake_bin.display(), std::env::var("PATH").unwrap());
|
|
let mut cmd = context.command();
|
|
cmd.env("PATH", path).args(["upgrade", "--dry-run"]);
|
|
|
|
fabro_snapshot!(filters, cmd, @"
|
|
success: true
|
|
exit_code: 0
|
|
----- stdout -----
|
|
----- stderr -----
|
|
Would upgrade fabro from [VERSION] to 999.0.0
|
|
tag: v999.0.0
|
|
target: [TARGET]
|
|
");
|
|
}
|
|
|
|
#[test]
|
|
fn upgrade_brew_install_refuses_and_prints_brew_command() {
|
|
let context = test_context!();
|
|
let mut cmd = brew_command(&context, "fabro", "0.176.2");
|
|
cmd.args(["upgrade"]);
|
|
|
|
fabro_snapshot!(context.filters(), cmd, @"
|
|
success: false
|
|
exit_code: 1
|
|
----- stdout -----
|
|
----- stderr -----
|
|
fabro was installed via Homebrew.
|
|
Run `brew upgrade fabro` to update.
|
|
error: refusing to overwrite a Homebrew-managed binary
|
|
");
|
|
}
|
|
|
|
#[test]
|
|
fn upgrade_brew_install_dry_run_json_reports_brew_command() {
|
|
let context = test_context!();
|
|
let mut cmd = brew_command(&context, "fabro-nightly", "0.205.0-nightly.0");
|
|
cmd.args(["--json", "upgrade", "--dry-run"]);
|
|
|
|
fabro_snapshot!(context.filters(), cmd, @r#"
|
|
success: true
|
|
exit_code: 0
|
|
----- stdout -----
|
|
{
|
|
"install_source": "homebrew",
|
|
"formula": "fabro-nightly",
|
|
"brew_command": "brew upgrade fabro-nightly",
|
|
"dry_run": true,
|
|
"note": "fabro is Homebrew-managed; no in-place upgrade attempted"
|
|
}
|
|
----- stderr -----
|
|
"#);
|
|
}
|
|
|
|
#[test]
|
|
fn upgrade_brew_install_rejects_version_flag() {
|
|
let context = test_context!();
|
|
let mut cmd = brew_command(&context, "fabro", "0.176.2");
|
|
cmd.args(["upgrade", "--version", "0.1.0"]);
|
|
|
|
fabro_snapshot!(context.filters(), cmd, @"
|
|
success: false
|
|
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`.
|
|
");
|
|
}
|