fabro/lib/crates/fabro-dev/tests/it/main.rs
fabro-sh-0530[bot] 10de9fd16c
Add foundation for settings-driven LLM catalog (#207)
## 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>
2026-05-12 10:01:57 -04:00

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}"
);
}