use std::io::Write as _;
use std::path::PathBuf;
pub const RC_MARKER: &str = "# added by `firstpass onboard`";
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Environment {
pub shell: String,
pub proxy_running: bool,
pub already_routed: bool,
pub has_api_key: bool,
pub has_claude_cli: bool,
pub bind: String,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Provider {
Anthropic,
OpenAi,
Google,
Local,
}
impl Provider {
pub const ALL: [Self; 4] = [Self::Anthropic, Self::OpenAi, Self::Google, Self::Local];
#[must_use]
pub const fn id(self) -> &'static str {
match self {
Self::Anthropic => "anthropic",
Self::OpenAi => "openai",
Self::Google => "google",
Self::Local => "local",
}
}
#[must_use]
pub const fn blurb(self) -> &'static str {
match self {
Self::Anthropic => "Claude — haiku opens, sonnet catches (built in)",
Self::OpenAi => "GPT — 4.1-mini opens, 5.5 catches (built in)",
Self::Google => "Gemini — flash opens, pro catches (needs GEMINI_API_KEY)",
Self::Local => "Ollama on localhost, escalating to Claude sonnet",
}
}
#[must_use]
pub const fn ladder(self) -> [&'static str; 2] {
match self {
Self::Anthropic => ["anthropic/claude-haiku-4-5", "anthropic/claude-sonnet-5"],
Self::OpenAi => ["openai/gpt-4.1-mini", "openai/gpt-5.5"],
Self::Google => ["google/gemini-3.1-flash", "google/gemini-3.1-pro"],
Self::Local => ["ollama/qwen2.5-coder:7b", "anthropic/claude-sonnet-5"],
}
}
#[must_use]
pub const fn judge_model(self) -> &'static str {
match self {
Self::Anthropic | Self::Local => "anthropic/claude-opus-4-8",
Self::OpenAi | Self::Google => "anthropic/claude-haiku-4-5",
}
}
#[must_use]
pub const fn provider_block(self) -> Option<&'static str> {
match self {
Self::Anthropic | Self::OpenAi => None,
Self::Google => Some(
"[[provider]] # only anthropic + openai are built in\n\
id = \"google\"\n\
dialect = \"gemini\"\n\
base_url = \"https://generativelanguage.googleapis.com\"\n\
api_key_env = \"GEMINI_API_KEY\"\n",
),
Self::Local => Some(
"[[provider]] # local rung; escalates to a frontier model\n\
id = \"ollama\"\n\
dialect = \"openai\"\n\
base_url = \"http://localhost:11434\" # keyless\n",
),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Shape {
Json,
Code,
Prose,
Mixed,
}
impl Shape {
pub const ALL: [Self; 4] = [Self::Json, Self::Code, Self::Prose, Self::Mixed];
#[must_use]
pub const fn id(self) -> &'static str {
match self {
Self::Json => "json",
Self::Code => "code",
Self::Prose => "prose",
Self::Mixed => "mixed",
}
}
#[must_use]
pub const fn blurb(self) -> &'static str {
match self {
Self::Json => "JSON / API responses — schema gate, cheapest proof there is",
Self::Code => "Code — your own test suite is the gate",
Self::Prose => "Prose — an LLM judge grades it (maker != checker)",
Self::Mixed => "Mixed — k-sample self-consistency scores agreement",
}
}
#[must_use]
pub const fn gates(self) -> [&'static str; 2] {
match self {
Self::Json => ["json-valid", "extract-shape"],
Self::Code => ["json-valid", "unit-tests"],
Self::Prose => ["non-empty", "judge"],
Self::Mixed => ["non-empty", "uncertainty"],
}
}
#[must_use]
pub fn gate_block(self, provider: Provider) -> String {
match self {
Self::Json => "[[gate]]\n\
id = \"extract-shape\"\n\
schema = { type = \"object\", required = [\"id\", \"total\"] }\n\
on_abstain = \"fail_closed\"\n"
.to_owned(),
Self::Code => {
"[[gate]]\n\
# REPLACE ME. A gate reads the candidate as JSON on stdin and prints\n\
# {\"verdict\":\"pass|fail|abstain\", \"score\"?: 0.0-1.0, \"reason\"?: \"...\"}\n\
# on stdout. Wrap your real test command in that contract — a bare `cargo test`\n\
# or `npm test` will not work, it would abstain on every request.\n\
# `firstpass doctor` fails on this line until you point it at your wrapper.\n\
id = \"unit-tests\"\n\
cmd = [\"your-test-runner\", \"--from-stdin\"]\n"
.to_owned()
}
Self::Prose => format!(
"[[gate]] # judge sits OUTSIDE the ladder: maker != checker\n\
id = \"judge\"\n\
judge = {{ model = \"{}\", threshold = 0.7, rubric = \"The response fully and \
correctly resolves the request, with no errors.\" }}\n",
provider.judge_model()
),
Self::Mixed => format!(
"[[gate]] # k samples; agreement becomes the score\n\
id = \"uncertainty\"\n\
consistency = {{ model = \"{}\", k = 3, threshold = 0.6 }}\n",
provider.ladder()[0]
),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct LadderChoice {
pub provider: Provider,
pub shape: Shape,
pub mode: firstpass_core::Mode,
}
impl Default for LadderChoice {
fn default() -> Self {
Self {
provider: Provider::Anthropic,
shape: Shape::Json,
mode: firstpass_core::Mode::Observe,
}
}
}
#[must_use]
pub fn render_config(choice: &LadderChoice) -> String {
let enforce = choice.mode == firstpass_core::Mode::Enforce;
let mode = if enforce { "enforce" } else { "observe" };
let quoted = |xs: [&str; 2]| -> String { format!("\"{}\", \"{}\"", xs[0], xs[1]) };
let mut out = format!(
"# firstpass.toml — written by `firstpass onboard`. Re-run onboard to regenerate.\n\
# FIRSTPASS_MODE={mode} FIRSTPASS_CONFIG=./firstpass.toml firstpass up\n\n"
);
if let Some(block) = choice.provider.provider_block() {
out.push_str(block);
out.push('\n');
}
out.push_str(&format!(
"[[route]] # routes match top to bottom; first match wins\n\
match = {{}} # everything\n\
mode = \"{mode}\"\n\
ladder = [{}]\n\
gates = [{}]\n\n",
quoted(choice.provider.ladder()),
quoted(choice.shape.gates()),
));
out.push_str(&choice.shape.gate_block(choice.provider));
if enforce {
out.push_str(
"\n[escalation]\nmax_rungs_per_request = 2 # one rung up, never a runaway\n",
);
} else {
out.push_str(
"\n# observe: every request is forwarded unchanged and a receipt is written off\n\
# the hot path. Nothing routes differently until mode = \"enforce\".\n",
);
}
out
}
#[must_use]
pub fn presets_js() -> String {
use firstpass_core::Mode;
let mut out = String::from(
"// GENERATED by `cargo test -p firstpass-proxy presets` — do not edit by hand.\n\
// Source of truth: crates/firstpass-proxy/src/onboard.rs (render_config).\n\
// Regenerate: FIRSTPASS_WRITE_PRESETS=1 cargo test -p firstpass-proxy presets\n\
window.FP_LADDER_PRESETS = {\n",
);
for provider in Provider::ALL {
out.push_str(&format!(" {:?}: {{\n", provider.id()));
for shape in Shape::ALL {
out.push_str(&format!(" {:?}: {{\n", shape.id()));
for (mode, key) in [(Mode::Observe, "observe"), (Mode::Enforce, "enforce")] {
let toml = render_config(&LadderChoice {
provider,
shape,
mode,
});
out.push_str(&format!(" {key:?}: {},\n", js_string(&toml)));
}
out.push_str(" },\n");
}
out.push_str(" },\n");
}
out.push_str("};\n");
out
}
fn js_string(s: &str) -> String {
let mut out = String::with_capacity(s.len() + 16);
out.push('"');
for c in s.chars() {
match c {
'"' => out.push_str("\\\""),
'\\' => out.push_str("\\\\"),
'\n' => out.push_str("\\n"),
'\r' => {}
c => out.push(c),
}
}
out.push('"');
out
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Step {
WriteConfig {
path: PathBuf,
toml: String,
},
StartProxy {
config: Option<PathBuf>,
},
WireShell {
rc: PathBuf,
line: String,
},
SuggestClaudeMcp,
Verify,
AlreadyDone(&'static str),
}
pub fn detect(
env: impl Fn(&str) -> Option<String>,
on_path: impl Fn(&str) -> bool,
healthz: impl Fn() -> bool,
) -> Environment {
let bind = env("FIRSTPASS_BIND").unwrap_or_else(|| "127.0.0.1:8080".to_owned());
let base_url = format!("http://{bind}");
let shell = env("SHELL")
.and_then(|s| s.rsplit('/').next().map(str::to_owned))
.unwrap_or_else(|| "sh".to_owned());
Environment {
shell,
proxy_running: healthz(),
already_routed: env("ANTHROPIC_BASE_URL").is_some_and(|v| v == base_url),
has_api_key: env("ANTHROPIC_API_KEY").is_some(),
has_claude_cli: on_path("claude"),
bind,
}
}
#[must_use]
pub fn shell_wiring(shell: &str, home: &std::path::Path, bind: &str) -> (PathBuf, String) {
let url = format!("http://{bind}");
match shell {
"fish" => (
home.join(".config/fish/config.fish"),
format!("set -gx ANTHROPIC_BASE_URL {url} {RC_MARKER}"),
),
"bash" => (
home.join(".bashrc"),
format!("export ANTHROPIC_BASE_URL={url} {RC_MARKER}"),
),
"zsh" => (
home.join(".zshrc"),
format!("export ANTHROPIC_BASE_URL={url} {RC_MARKER}"),
),
_ => (
home.join(".profile"),
format!("export ANTHROPIC_BASE_URL={url} {RC_MARKER}"),
),
}
}
#[derive(Debug, Clone)]
pub struct ConfigPlan {
pub path: PathBuf,
pub choice: LadderChoice,
}
#[must_use]
pub fn plan(
env: &Environment,
home: &std::path::Path,
rc_already_wired: bool,
config: Option<&ConfigPlan>,
) -> Vec<Step> {
let mut steps = Vec::new();
if let Some(c) = config {
steps.push(Step::WriteConfig {
path: c.path.clone(),
toml: render_config(&c.choice),
});
}
if env.proxy_running {
steps.push(Step::AlreadyDone("proxy already answering /healthz"));
} else {
steps.push(Step::StartProxy {
config: config.map(|c| c.path.clone()),
});
}
if env.already_routed || rc_already_wired {
steps.push(Step::AlreadyDone("ANTHROPIC_BASE_URL already wired"));
} else {
let (rc, line) = shell_wiring(&env.shell, home, &env.bind);
steps.push(Step::WireShell { rc, line });
}
if env.has_claude_cli {
steps.push(Step::SuggestClaudeMcp);
}
steps.push(Step::Verify);
steps
}
#[must_use]
pub fn render(env: &Environment, steps: &[Step], apply: bool) -> String {
let mut out = String::new();
out.push_str(&format!(
"detected: shell={} · proxy_running={} · routed={} · api_key={} · claude_cli={}\n\n",
env.shell, env.proxy_running, env.already_routed, env.has_api_key, env.has_claude_cli
));
for (i, s) in steps.iter().enumerate() {
let n = i + 1;
match s {
Step::WriteConfig { path, toml } => {
out.push_str(&format!("{n}. write {} —\n", path.display()));
for line in toml.lines() {
out.push_str(&format!(" {line}\n"));
}
}
Step::StartProxy { config } => {
out.push_str(&format!(
"{n}. start the proxy — `firstpass up` (observe mode: watches, changes nothing), log → firstpass-proxy.log\n"
));
if let Some(c) = config {
out.push_str(&format!(
" with FIRSTPASS_CONFIG={} — the proxy has no default config path\n",
c.display()
));
}
}
Step::WireShell { rc, line } => out.push_str(&format!(
"{n}. route your agents — append to {}:\n {line}\n",
rc.display()
)),
Step::SuggestClaudeMcp => out.push_str(&format!(
"{n}. (optional) let Claude Code query receipts as tools:\n claude mcp add firstpass -- firstpass mcp\n"
)),
Step::Verify => out.push_str(&format!(
"{n}. verify — probe /healthz and /v1/capabilities, report what's routed\n"
)),
Step::AlreadyDone(why) => out.push_str(&format!("{n}. ✓ {why}\n")),
}
}
if !env.has_api_key {
out.push_str(
"\nnote: ANTHROPIC_API_KEY is not set — observe mode passes your agent's own key \
through (BYOK), so this only matters for enforce mode.\n",
);
}
if !apply {
out.push_str(
"\ndry run — nothing changed. Re-run with `firstpass onboard --apply` to execute.\n",
);
}
out
}
pub fn execute(env: &Environment, steps: &[Step]) -> Result<String, std::io::Error> {
let mut out = String::new();
for s in steps {
match s {
Step::WriteConfig { path, toml } => {
if path.exists() {
out.push_str(&format!(
"✓ {} already present — left untouched\n",
path.display()
));
} else {
std::fs::write(path, toml)?;
out.push_str(&format!("✓ wrote {}\n", path.display()));
}
}
Step::StartProxy { config } => {
let log = std::fs::File::create("firstpass-proxy.log")?;
let exe = std::env::current_exe()?;
let mut cmd = std::process::Command::new(exe);
cmd.arg("up");
if let Some(c) = config {
cmd.env("FIRSTPASS_CONFIG", c);
}
let child = cmd
.stdin(std::process::Stdio::null())
.stdout(std::process::Stdio::from(log.try_clone()?))
.stderr(std::process::Stdio::from(log))
.spawn();
match child {
Ok(c) => {
let _ = std::fs::write("firstpass-proxy.pid", c.id().to_string());
out.push_str(&format!(
"✓ proxy started (pid {}, observe mode) — log: firstpass-proxy.log\n",
c.id()
));
}
Err(e) => out.push_str(&format!("✗ could not start proxy: {e}\n")),
}
}
Step::WireShell { rc, line } => {
if let Some(parent) = rc.parent() {
std::fs::create_dir_all(parent)?;
}
let mut f = std::fs::OpenOptions::new()
.create(true)
.append(true)
.open(rc)?;
writeln!(f, "{line}")?;
out.push_str(&format!(
"✓ wired {} — takes effect in new shells; for this one:\n {}\n",
rc.display(),
line.trim_end_matches(RC_MARKER).trim_end()
));
}
Step::SuggestClaudeMcp => {
out.push_str("→ optional: claude mcp add firstpass -- firstpass mcp\n");
}
Step::Verify => {
let url = format!("http://{}/healthz", env.bind);
let ok = wait_healthz(&url, std::time::Duration::from_secs(6));
if ok {
out.push_str(&format!(
"✓ verified — proxy healthy at http://{} · capabilities: http://{}/v1/capabilities\n",
env.bind, env.bind
));
} else {
out.push_str(&format!(
"✗ proxy not answering http://{} after 6s — check firstpass-proxy.log\n",
env.bind
));
}
}
Step::AlreadyDone(why) => out.push_str(&format!("✓ {why}\n")),
}
}
out.push_str("\noffboard any time: unset ANTHROPIC_BASE_URL (and remove the marked rc line)\n");
Ok(out)
}
fn wait_healthz(url: &str, deadline: std::time::Duration) -> bool {
let Some(addr) = url
.strip_prefix("http://")
.and_then(|r| r.split('/').next())
.map(str::to_owned)
else {
return false;
};
let start = std::time::Instant::now();
while start.elapsed() < deadline {
if let Ok(mut s) = std::net::TcpStream::connect(&addr) {
let _ = s.set_read_timeout(Some(std::time::Duration::from_millis(500)));
let req = format!("GET /healthz HTTP/1.1\r\nHost: {addr}\r\nConnection: close\r\n\r\n");
if s.write_all(req.as_bytes()).is_ok() {
let mut buf = [0u8; 64];
use std::io::Read as _;
if let Ok(n) = s.read(&mut buf)
&& n > 0
&& String::from_utf8_lossy(&buf[..n]).contains("200")
{
return true;
}
}
}
std::thread::sleep(std::time::Duration::from_millis(200));
}
false
}
#[must_use]
pub fn rc_wired(rc: &std::path::Path) -> bool {
std::fs::read_to_string(rc).is_ok_and(|s| s.contains(RC_MARKER))
}
pub fn offboard_rc(rc: &std::path::Path) -> Result<bool, std::io::Error> {
let Ok(content) = std::fs::read_to_string(rc) else {
return Ok(false); };
if !content.contains(RC_MARKER) {
return Ok(false);
}
let kept: Vec<&str> = content.lines().filter(|l| !l.contains(RC_MARKER)).collect();
std::fs::write(rc, kept.join("\n") + "\n")?;
Ok(true)
}
pub fn offboard(home: &std::path::Path) -> Result<String, std::io::Error> {
let mut out = String::new();
for rc in [
home.join(".zshrc"),
home.join(".bashrc"),
home.join(".profile"),
home.join(".config/fish/config.fish"),
] {
if offboard_rc(&rc)? {
out.push_str(&format!("✓ removed firstpass line from {}\n", rc.display()));
}
}
if let Ok(pid) = std::fs::read_to_string("firstpass-proxy.pid") {
let pid = pid.trim().to_owned();
#[cfg(unix)]
{
let killed = std::process::Command::new("kill")
.arg(&pid)
.status()
.is_ok_and(|s| s.success());
if killed {
out.push_str(&format!("✓ stopped proxy (pid {pid})\n"));
} else {
out.push_str(&format!(
"→ proxy pid {pid} not running (already stopped)\n"
));
}
}
let _ = std::fs::remove_file("firstpass-proxy.pid");
}
if out.is_empty() {
out.push_str("nothing to offboard — no marked rc lines, no pidfile.\n");
}
out.push_str("for this shell: unset ANTHROPIC_BASE_URL\n");
Ok(out)
}
#[cfg(test)]
mod tests {
#![allow(clippy::unwrap_used)]
use super::*;
fn env_of<'a>(pairs: &'a [(&'a str, &'a str)]) -> impl Fn(&str) -> Option<String> + 'a {
move |k| {
pairs
.iter()
.find(|(a, _)| *a == k)
.map(|(_, v)| (*v).to_owned())
}
}
#[test]
fn detect_reads_shell_routing_and_tools() {
let e = detect(
env_of(&[
("SHELL", "/bin/zsh"),
("ANTHROPIC_BASE_URL", "http://127.0.0.1:8080"),
("ANTHROPIC_API_KEY", "sk-x"),
]),
|bin| bin == "claude",
|| true,
);
assert_eq!(e.shell, "zsh");
assert!(e.proxy_running && e.already_routed && e.has_api_key && e.has_claude_cli);
assert_eq!(e.bind, "127.0.0.1:8080");
}
#[test]
fn detect_respects_custom_bind_and_mismatched_base_url() {
let e = detect(
env_of(&[
("FIRSTPASS_BIND", "127.0.0.1:9999"),
("ANTHROPIC_BASE_URL", "http://127.0.0.1:8080"), ]),
|_| false,
|| false,
);
assert_eq!(e.bind, "127.0.0.1:9999");
assert!(
!e.already_routed,
"routed to a different port is not routed"
);
}
#[test]
fn shell_wiring_speaks_each_dialect() {
let home = std::path::Path::new("/home/u");
let (rc, line) = shell_wiring("fish", home, "127.0.0.1:8080");
assert!(rc.ends_with(".config/fish/config.fish"));
assert!(line.starts_with("set -gx ANTHROPIC_BASE_URL http://127.0.0.1:8080"));
let (rc, line) = shell_wiring("zsh", home, "127.0.0.1:8080");
assert!(rc.ends_with(".zshrc"));
assert!(line.starts_with("export ANTHROPIC_BASE_URL="));
let (rc, _) = shell_wiring("dash", home, "127.0.0.1:8080");
assert!(
rc.ends_with(".profile"),
"unknown shells fall back to .profile"
);
}
#[test]
fn plan_covers_fresh_machine_and_is_idempotent_when_done() {
let home = std::path::Path::new("/home/u");
let fresh = Environment {
shell: "zsh".into(),
proxy_running: false,
already_routed: false,
has_api_key: false,
has_claude_cli: true,
bind: "127.0.0.1:8080".into(),
};
let steps = plan(&fresh, home, false, None);
assert!(matches!(steps[0], Step::StartProxy { .. }));
assert!(matches!(steps[1], Step::WireShell { .. }));
assert!(matches!(steps[2], Step::SuggestClaudeMcp));
assert!(matches!(steps.last(), Some(Step::Verify)));
let done = Environment {
proxy_running: true,
already_routed: true,
has_claude_cli: false,
..fresh
};
let steps = plan(&done, home, true, None);
assert!(
steps
.iter()
.all(|s| matches!(s, Step::AlreadyDone(_) | Step::Verify))
);
}
#[test]
fn render_dry_run_says_nothing_changed_and_flags_missing_key() {
let home = std::path::Path::new("/home/u");
let e = Environment {
shell: "bash".into(),
proxy_running: false,
already_routed: false,
has_api_key: false,
has_claude_cli: false,
bind: "127.0.0.1:8080".into(),
};
let text = render(&e, &plan(&e, home, false, None), false);
assert!(text.contains("dry run — nothing changed"));
assert!(text.contains("ANTHROPIC_API_KEY is not set"));
assert!(text.contains(".bashrc"));
}
#[test]
fn rc_wired_detects_the_marker_and_execute_appends_it_once() {
let dir = std::env::temp_dir().join(format!("fp-onboard-{}", uuid::Uuid::now_v7()));
std::fs::create_dir_all(&dir).unwrap();
let rc = dir.join(".zshrc");
assert!(!rc_wired(&rc), "missing file is not wired");
let e = Environment {
shell: "zsh".into(),
proxy_running: true, already_routed: false,
has_api_key: true,
has_claude_cli: false,
bind: "127.0.0.1:1".into(), };
let (rc_path, line) = shell_wiring("zsh", &dir, &e.bind);
let steps = vec![Step::WireShell {
rc: rc_path.clone(),
line,
}];
let report = execute(&e, &steps).unwrap();
assert!(report.contains("✓ wired"));
assert!(rc_wired(&rc_path), "marker written");
let steps = plan(&e, &dir, rc_wired(&rc_path), None);
assert!(!steps.iter().any(|s| matches!(s, Step::WireShell { .. })));
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn offboard_removes_only_the_marked_line_and_is_idempotent() {
let dir = std::env::temp_dir().join(format!("fp-offboard-{}", uuid::Uuid::now_v7()));
std::fs::create_dir_all(&dir).unwrap();
let rc = dir.join(".zshrc");
std::fs::write(
&rc,
format!("alias ll='ls -l'\nexport ANTHROPIC_BASE_URL=http://127.0.0.1:8080 {RC_MARKER}\nexport EDITOR=vim\n"),
)
.unwrap();
assert!(offboard_rc(&rc).unwrap(), "marked line removed");
let after = std::fs::read_to_string(&rc).unwrap();
assert!(!after.contains(RC_MARKER));
assert!(
after.contains("alias ll") && after.contains("EDITOR=vim"),
"user lines untouched"
);
assert!(!offboard_rc(&rc).unwrap(), "second offboard is a no-op");
std::fs::write(&rc, format!("x {RC_MARKER}\n")).unwrap();
let report = offboard(&dir).unwrap();
assert!(report.contains("removed firstpass line"));
assert!(report.contains("unset ANTHROPIC_BASE_URL"));
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn every_generated_config_parses_and_resolves() {
use firstpass_core::Mode;
let mut n = 0;
for provider in Provider::ALL {
for shape in Shape::ALL {
for mode in [Mode::Observe, Mode::Enforce] {
n += 1;
let choice = LadderChoice {
provider,
shape,
mode,
};
let toml = render_config(&choice);
let cfg = firstpass_core::Config::parse(&toml).unwrap_or_else(|e| {
panic!(
"{}/{}/{mode:?} did not parse: {e}\n{toml}",
provider.id(),
shape.id()
)
});
let route = &cfg.routes[0];
assert_eq!(
route.mode, mode,
"route mode is what actually gates enforcement"
);
assert_eq!(
route.ladder.len(),
2,
"a ladder needs somewhere to escalate to"
);
for g in &route.gates {
let builtin = g == "non-empty" || g == "json-valid";
assert!(
builtin || cfg.gate_defs.iter().any(|d| &d.id == g),
"{}/{}: gate {g:?} is neither built in nor declared",
provider.id(),
shape.id()
);
}
for rung in &route.ladder {
let pid = rung.split('/').next().unwrap();
let builtin = pid == "anthropic" || pid == "openai";
assert!(
builtin || cfg.providers.iter().any(|d| d.id == pid),
"{}/{}: provider {pid:?} is neither built in nor declared",
provider.id(),
shape.id()
);
}
if let Some(j) = cfg.gate_defs.iter().find_map(|d| d.judge.as_ref()) {
assert!(
!route.ladder.contains(&j.model),
"{}: judge {} is on its own ladder",
provider.id(),
j.model
);
}
assert_eq!(
toml.contains("[escalation]"),
mode == Mode::Enforce,
"escalation block should track enforce only"
);
}
}
}
assert_eq!(n, 32, "4 providers x 4 shapes x 2 modes");
}
#[test]
fn planned_config_is_written_before_the_proxy_starts_and_is_handed_to_it() {
let home = std::path::Path::new("/home/u");
let env = Environment {
shell: "zsh".into(),
proxy_running: false,
already_routed: false,
has_api_key: true,
has_claude_cli: false,
bind: "127.0.0.1:8080".into(),
};
let cfg = ConfigPlan {
path: PathBuf::from("firstpass.toml"),
choice: LadderChoice::default(),
};
let steps = plan(&env, home, false, Some(&cfg));
assert!(
matches!(&steps[0], Step::WriteConfig { path, .. } if path == &cfg.path),
"config is written first, before anything reads it"
);
assert!(
matches!(&steps[1], Step::StartProxy { config: Some(p) } if p == &cfg.path),
"the spawned proxy is handed the config explicitly"
);
let steps = plan(&env, home, false, None);
assert!(matches!(steps[0], Step::StartProxy { config: None }));
assert!(!steps.iter().any(|s| matches!(s, Step::WriteConfig { .. })));
}
#[test]
fn write_config_refuses_to_overwrite_an_existing_file() {
let dir = std::env::temp_dir().join(format!("fp-cfg-{}", uuid::Uuid::now_v7()));
std::fs::create_dir_all(&dir).unwrap();
let path = dir.join("firstpass.toml");
std::fs::write(&path, "# hand-tuned, do not touch\n").unwrap();
let env = Environment {
shell: "zsh".into(),
proxy_running: true, already_routed: true,
has_api_key: true,
has_claude_cli: false,
bind: "127.0.0.1:1".into(),
};
let steps = vec![Step::WriteConfig {
path: path.clone(),
toml: render_config(&LadderChoice::default()),
}];
let report = execute(&env, &steps).unwrap();
assert!(report.contains("already present"));
assert_eq!(
std::fs::read_to_string(&path).unwrap(),
"# hand-tuned, do not touch\n",
"existing config survived untouched"
);
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn dry_run_shows_the_config_it_would_write() {
let home = std::path::Path::new("/home/u");
let env = Environment {
shell: "bash".into(),
proxy_running: false,
already_routed: false,
has_api_key: true,
has_claude_cli: false,
bind: "127.0.0.1:8080".into(),
};
let cfg = ConfigPlan {
path: PathBuf::from("firstpass.toml"),
choice: LadderChoice {
provider: Provider::Local,
shape: Shape::Code,
mode: firstpass_core::Mode::Enforce,
},
};
let text = render(&env, &plan(&env, home, false, Some(&cfg)), false);
assert!(text.contains("write firstpass.toml"));
assert!(text.contains("ollama"), "provider block is shown");
assert!(text.contains("unit-tests"), "gate block is shown");
assert!(
text.contains("FIRSTPASS_CONFIG="),
"explains how the proxy finds it"
);
assert!(text.contains("dry run — nothing changed"));
}
#[test]
fn docs_presets_are_current() {
let path = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
.join("../../docs/assets/ladder-presets.js");
let generated = presets_js();
if std::env::var("FIRSTPASS_WRITE_PRESETS").is_ok() {
std::fs::write(&path, &generated).expect("write presets");
return;
}
let committed = std::fs::read_to_string(&path).unwrap_or_else(|e| {
panic!(
"{}: {e}\nregenerate with:\n \
FIRSTPASS_WRITE_PRESETS=1 cargo test -p firstpass-proxy presets",
path.display()
)
});
assert_eq!(
committed.replace("\r\n", "\n"),
generated,
"docs/assets/ladder-presets.js is stale — the docs builder and `firstpass onboard` \
would emit different config.\nregenerate with:\n \
FIRSTPASS_WRITE_PRESETS=1 cargo test -p firstpass-proxy presets"
);
}
#[test]
fn every_docs_preset_parses() {
let js = presets_js();
let mut n = 0;
for raw in js.split("\": \"").skip(1) {
let lit = &raw[..raw.find("\",\n").unwrap_or(raw.len())];
let toml = lit
.replace("\\n", "\n")
.replace("\\\"", "\"")
.replace("\\\\", "\\");
if !toml.contains("[[route]]") {
continue;
}
n += 1;
firstpass_core::Config::parse(&toml)
.unwrap_or_else(|e| panic!("preset #{n} does not parse: {e}\n{toml}"));
}
assert_eq!(n, 32, "expected all 32 presets to be checked, saw {n}");
}
}