mirror of
https://github.com/fabro-sh/fabro.git
synced 2026-09-12 23:02:41 +00:00
## Summary This lays the groundwork for settings-driven LLM providers and models without switching production routing yet. The new schemas and shared vocabulary let later catalog construction treat provider/model identity as data while keeping adapter behavior and control values Rust-owned. ## What changed - Added `[llm.providers]` and `[llm.models]` settings layers with sparse per-entry merging, whole-array replacement for credential/alias/control lists, TOML date support for `knowledge_cutoff`, and typed `credential:` / `env:` references that reject literal secrets. - Added `ProviderId`, `ModelId`, and a shared `ReasoningEffort` enum in `fabro-model`, plus adapter metadata for `anthropic`, `openai`, `gemini`, and `openai_compatible`. - Added a matching `fabro-llm` adapter factory registry with parity tests to keep metadata keys and factory keys in sync. - Added `[run.model.controls]` defaults through config resolution and runtime settings types. - Added a workspace policy test to prevent future `bootstrap_catalog` use outside install/test-support paths. ### Plan Summary - This is the foundation slice of the settings-driven catalog plan. - Production still uses the existing `Provider` enum and `Catalog::builtin()` call paths. - ProviderId routing, OpenAPI regeneration, auth resolver changes, resolved `Arc<Catalog>` injection, typed request speed, and per-speed billing are deferred follow-ups. ⚒️ Generated with [Fabro](https://fabro.sh) --------- Co-authored-by: Fabro <noreply@fabro.sh> Co-authored-by: fabro-bot <fabro-bot@fabro.sh> Co-authored-by: Bryan Helmkamp <bryan@brynary.com>
173 lines
4.6 KiB
Rust
173 lines
4.6 KiB
Rust
use std::path::{Path, PathBuf};
|
|
|
|
mod docker_build;
|
|
#[cfg(unix)]
|
|
mod docker_entrypoint;
|
|
mod docs;
|
|
mod policy;
|
|
mod release;
|
|
mod spa;
|
|
|
|
fn fabro_dev() -> assert_cmd::Command {
|
|
assert_cmd::cargo::cargo_bin_cmd!("fabro-dev")
|
|
}
|
|
|
|
pub(crate) fn workspace_root() -> PathBuf {
|
|
let mut root = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
|
|
root.pop();
|
|
root.pop();
|
|
root.pop();
|
|
root
|
|
}
|
|
|
|
fn output_text(bytes: &[u8]) -> String {
|
|
String::from_utf8(bytes.to_vec()).expect("command output should be valid utf-8")
|
|
}
|
|
|
|
#[expect(
|
|
clippy::disallowed_methods,
|
|
reason = "integration tests stage temporary fixture files with sync std::fs::write"
|
|
)]
|
|
fn write_file(root: &Path, path: &str, contents: impl AsRef<[u8]>) {
|
|
let path = root.join(path);
|
|
std::fs::create_dir_all(path.parent().expect("fixture path should have parent"))
|
|
.expect("creating fixture parent directory");
|
|
std::fs::write(path, contents).expect("writing fixture file");
|
|
}
|
|
|
|
#[expect(
|
|
clippy::disallowed_methods,
|
|
reason = "integration tests inspect fixture files with sync std::fs::read_to_string"
|
|
)]
|
|
fn read_file(root: &Path, path: &str) -> String {
|
|
std::fs::read_to_string(root.join(path)).expect("reading fixture file")
|
|
}
|
|
|
|
#[expect(
|
|
clippy::disallowed_methods,
|
|
reason = "integration tests inspect fixture files with sync std::fs::read"
|
|
)]
|
|
fn read_bytes(root: &Path, path: &str) -> Vec<u8> {
|
|
std::fs::read(root.join(path)).expect("reading fixture file")
|
|
}
|
|
|
|
#[test]
|
|
fn help_lists_scaffolded_commands() {
|
|
let output = fabro_dev()
|
|
.arg("--help")
|
|
.assert()
|
|
.success()
|
|
.get_output()
|
|
.clone();
|
|
let stdout = output_text(&output.stdout);
|
|
|
|
for command in ["build", "docker-build", "docs", "release", "spa"] {
|
|
assert!(
|
|
stdout.contains(command),
|
|
"top-level help should list {command}:\n{stdout}"
|
|
);
|
|
}
|
|
|
|
for removed_command in [
|
|
concat!("generate-cli", "-reference"),
|
|
concat!("generate-options", "-reference"),
|
|
concat!("refresh", "-spa"),
|
|
concat!("check-spa", "-budgets"),
|
|
] {
|
|
assert!(
|
|
!stdout.contains(removed_command),
|
|
"top-level help should not list removed command {removed_command}:\n{stdout}"
|
|
);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn group_only_spa_prints_subcommand_help_successfully() {
|
|
let output = fabro_dev()
|
|
.arg("spa")
|
|
.assert()
|
|
.success()
|
|
.get_output()
|
|
.clone();
|
|
let stdout = output_text(&output.stdout);
|
|
|
|
for command in ["refresh", "check"] {
|
|
assert!(
|
|
stdout.contains(command),
|
|
"spa help should list {command}:\n{stdout}"
|
|
);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn group_only_docs_prints_subcommand_help_successfully() {
|
|
let output = fabro_dev()
|
|
.arg("docs")
|
|
.assert()
|
|
.success()
|
|
.get_output()
|
|
.clone();
|
|
let stdout = output_text(&output.stdout);
|
|
|
|
for command in ["refresh", "check"] {
|
|
assert!(
|
|
stdout.contains(command),
|
|
"docs help should list {command}:\n{stdout}"
|
|
);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn build_help_lists_forwarded_cargo_args() {
|
|
let output = fabro_dev()
|
|
.args(["build", "--help"])
|
|
.assert()
|
|
.success()
|
|
.get_output()
|
|
.clone();
|
|
let stdout = output_text(&output.stdout);
|
|
|
|
assert!(
|
|
stdout.contains("Arguments forwarded to `cargo build`"),
|
|
"build help should explain forwarded cargo args:\n{stdout}"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn cargo_dev_alias_points_at_fabro_dev() {
|
|
let config = read_file(&workspace_root(), ".cargo/config.toml");
|
|
assert!(
|
|
config.contains(r#"dev = "run --package fabro-dev --features dev --""#),
|
|
"cargo dev alias should invoke fabro-dev:\n{config}"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn fabro_dev_does_not_depend_on_fabro_cli() {
|
|
let manifest = read_file(&workspace_root(), "lib/crates/fabro-dev/Cargo.toml");
|
|
assert!(
|
|
!manifest.contains("fabro-cli"),
|
|
"fabro-dev should shell out to fabro-cli instead of depending on it:\n{manifest}"
|
|
);
|
|
assert!(
|
|
manifest.contains("required-features = [\"dev\"]"),
|
|
"fabro-dev binary should require the dev feature:\n{manifest}"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn unknown_subcommand_exits_with_clap_usage_error() {
|
|
let output = fabro_dev()
|
|
.arg("not-a-command")
|
|
.assert()
|
|
.failure()
|
|
.code(2)
|
|
.get_output()
|
|
.clone();
|
|
let stderr = output_text(&output.stderr);
|
|
|
|
assert!(
|
|
stderr.contains("unrecognized subcommand 'not-a-command'"),
|
|
"unknown subcommand should report clap usage error:\n{stderr}"
|
|
);
|
|
}
|