use std::io::Cursor;
use clap::Parser;
use serial_test::serial;
use trusty_common::credentials::{KeyStore, MemoryKeyStore};
use trusty_common::inference::config::ops::{KeyTier, ProbeOutcome};
use trusty_common::inference::config::{ConfigCommand, ops};
use trusty_common::inference::providers::openrouter;
use trusty_common::inference::test_support::MockInferenceServer;
use trusty_common::inference::{Configurator, ProviderId, ResolvedProvider};
const FAKE_KEY: &str = "sk-or-supersecretvalue-must-never-print-9999";
#[derive(Parser)]
#[command(name = "harness")]
struct Harness {
#[command(subcommand)]
command: HarnessCmd,
}
#[derive(clap::Subcommand)]
enum HarnessCmd {
Config(ConfigCommand),
}
fn clear_provider_env() {
for var in [
"OPENROUTER_API_KEY",
"ANTHROPIC_API_KEY",
"OPENAI_API_KEY",
"FIREWORKS_API_KEY",
"TOGETHER_API_KEY",
] {
unsafe { std::env::remove_var(var) };
}
}
fn out_string(buf: &[u8]) -> String {
String::from_utf8(buf.to_vec()).expect("output is valid UTF-8")
}
#[test]
fn config_keys_argv_grammar_parses() {
let canonical: &[&[&str]] = &[
&["harness", "config", "keys", "set", "openrouter"],
&["harness", "config", "keys", "set", "openrouter", "sk-value"], &["harness", "config", "keys", "list"],
&["harness", "config", "keys", "test", "anthropic"],
&["harness", "config", "keys", "unset", "fireworks"],
&["harness", "config", "keys", "test", "together"],
];
for argv in canonical {
assert!(
Harness::try_parse_from(*argv).is_ok(),
"canonical grammar should parse: {argv:?}"
);
}
}
#[test]
fn config_keys_rejects_get() {
assert!(
Harness::try_parse_from(["harness", "config", "keys", "get", "openrouter"]).is_err(),
"`config keys get` must not exist"
);
}
#[test]
fn read_key_line_trims_piped_value() {
let mut reader = Cursor::new(format!("{FAKE_KEY}\n").into_bytes());
assert_eq!(ops::read_key_line(&mut reader).unwrap(), FAKE_KEY);
}
#[test]
#[serial(dotenv_credential_env)]
fn set_then_list_reports_store_tier_without_value() {
clear_provider_env();
let store = MemoryKeyStore::new();
let mut set_out = Vec::new();
ops::set(&store, "openrouter", FAKE_KEY, &mut set_out).expect("set ok");
let set_str = out_string(&set_out);
assert!(set_str.contains("openrouter"), "{set_str}");
assert!(!set_str.contains(FAKE_KEY), "set leaked the key: {set_str}");
assert_eq!(store.get("openrouter").as_deref(), Some(FAKE_KEY));
let mut list_out = Vec::new();
ops::list(&store, &mut list_out).expect("list ok");
let list_str = out_string(&list_out);
assert!(
list_str.contains("openrouter") && list_str.contains(KeyTier::Store.label()),
"list should show openrouter via the secure store: {list_str}"
);
assert!(list_str.contains("not configured"), "{list_str}");
assert!(
!list_str.contains(FAKE_KEY),
"list leaked the key: {list_str}"
);
let mut unset_out = Vec::new();
ops::unset(&store, "openrouter", &mut unset_out).expect("unset ok");
assert!(store.get("openrouter").is_none(), "key should be gone");
let unset_str = out_string(&unset_out);
assert!(unset_str.contains("Removed"), "{unset_str}");
assert!(!unset_str.contains(FAKE_KEY));
let mut again = Vec::new();
ops::unset(&store, "openrouter", &mut again).expect("idempotent unset");
assert!(out_string(&again).contains("nothing to remove"));
}
#[test]
#[serial(dotenv_credential_env)]
fn list_reports_env_tier() {
clear_provider_env();
let store = MemoryKeyStore::new();
store.set("openai", FAKE_KEY).unwrap();
unsafe { std::env::set_var("OPENAI_API_KEY", "env-value") };
let mut out = Vec::new();
ops::list(&store, &mut out).expect("list ok");
let s = out_string(&out);
assert!(
s.contains("openai") && s.contains(KeyTier::Env.label()),
"openai should report the env tier: {s}"
);
assert!(
!s.contains(FAKE_KEY) && !s.contains("env-value"),
"list leaked a value: {s}"
);
unsafe { std::env::remove_var("OPENAI_API_KEY") };
}
#[test]
#[serial(dotenv_credential_env)]
fn list_reports_ambiguous_tier_when_env_and_env_local_both_present() {
clear_provider_env();
let tmp = tempfile::TempDir::new().expect("tempdir");
std::fs::write(
tmp.path().join(".env.local"),
"ANTHROPIC_API_KEY=file-value\n",
)
.expect("write .env.local");
let prior_cwd = std::env::current_dir().expect("cwd");
std::env::set_current_dir(tmp.path()).expect("chdir into tempdir");
unsafe { std::env::set_var("ANTHROPIC_API_KEY", "env-value") };
let store = MemoryKeyStore::new();
let mut out = Vec::new();
let list_result = ops::list(&store, &mut out);
unsafe { std::env::remove_var("ANTHROPIC_API_KEY") };
std::env::set_current_dir(&prior_cwd).expect("restore cwd");
list_result.expect("list ok");
let s = out_string(&out);
assert!(
s.contains("anthropic") && s.contains(KeyTier::EnvOrEnvLocal.label()),
"anthropic should report the ambiguous env/.env.local tier: {s}"
);
assert!(
!s.contains("env-value") && !s.contains("file-value"),
"list leaked a value: {s}"
);
}
#[test]
#[serial(dotenv_credential_env)]
fn set_rejects_unknown_and_keyless_providers() {
clear_provider_env();
let store = MemoryKeyStore::new();
let mut out = Vec::new();
assert!(ops::set(&store, "bedrock", "x", &mut out).is_err());
assert!(ops::set(&store, "cohere", "x", &mut out).is_err());
assert!(store.list().is_empty());
}
fn mock_configurator(base_url: String) -> Configurator {
let mut cfg = Configurator::new();
cfg.register(
ProviderId::OpenRouter,
Box::new(move |r: &ResolvedProvider| openrouter::build(r, &base_url)),
);
cfg
}
#[tokio::test]
#[serial(dotenv_credential_env)]
async fn test_probe_ok_against_mock() {
clear_provider_env();
let body = serde_json::json!({
"id": "gen-mock",
"choices": [{"message": {"role": "assistant", "content": "pong"},
"finish_reason": "stop"}],
"usage": {"prompt_tokens": 1, "completion_tokens": 1}
});
let server = MockInferenceServer::spawn(200, body).await.expect("spawn");
let cfg = mock_configurator(server.url().to_string());
let store = MemoryKeyStore::new();
store.set("openrouter", FAKE_KEY).unwrap();
let outcome = ops::probe(&store, &cfg, "openrouter").await.expect("probe");
assert_eq!(outcome, ProbeOutcome::Ok);
let mut out = Vec::new();
ops::report_probe("openrouter", &outcome, &mut out).expect("report");
let s = out_string(&out);
assert!(s.contains("OK"), "{s}");
assert!(!s.contains(FAKE_KEY), "probe report leaked the key: {s}");
}
#[tokio::test]
#[serial(dotenv_credential_env)]
async fn test_probe_401_is_unauthorized() {
clear_provider_env();
let server = MockInferenceServer::spawn(401, serde_json::json!({"error": "invalid key"}))
.await
.expect("spawn");
let cfg = mock_configurator(server.url().to_string());
let store = MemoryKeyStore::new();
store.set("openrouter", FAKE_KEY).unwrap();
let outcome = ops::probe(&store, &cfg, "openrouter").await.expect("probe");
assert_eq!(outcome, ProbeOutcome::Unauthorized);
assert!(
outcome.clone().into_result().is_err(),
"401 must be a failure exit"
);
let mut out = Vec::new();
ops::report_probe("openrouter", &outcome, &mut out).expect("report");
assert!(!out_string(&out).contains(FAKE_KEY));
}
#[tokio::test]
#[serial(dotenv_credential_env)]
async fn test_probe_404_is_model_not_found() {
clear_provider_env();
let server = MockInferenceServer::spawn(
404,
serde_json::json!({"error": "Model not found, inaccessible, and/or not deployed"}),
)
.await
.expect("spawn");
let cfg = mock_configurator(server.url().to_string());
let store = MemoryKeyStore::new();
store.set("openrouter", FAKE_KEY).unwrap();
let outcome = ops::probe(&store, &cfg, "openrouter").await.expect("probe");
let ProbeOutcome::ModelNotFound(reason) = &outcome else {
panic!("expected ModelNotFound for a 404 response, got {outcome:?}");
};
assert!(!reason.contains(FAKE_KEY), "leaked the key: {reason}");
assert!(
outcome.clone().into_result().is_err(),
"404 must be a failure exit"
);
let mut out = Vec::new();
ops::report_probe("openrouter", &outcome, &mut out).expect("report");
let s = out_string(&out);
assert!(!s.contains(FAKE_KEY), "report_probe leaked the key: {s}");
assert!(s.contains("MODEL NOT FOUND"), "{s}");
assert!(
s.contains("set an explicit"),
"label should be actionable: {s}"
);
}
#[tokio::test]
#[serial(dotenv_credential_env)]
async fn probe_error_body_never_leaks_the_resolved_key() {
clear_provider_env();
let server = MockInferenceServer::spawn(
400,
serde_json::json!({"error": {"message": format!("invalid credential: {FAKE_KEY}")}}),
)
.await
.expect("spawn");
let cfg = mock_configurator(server.url().to_string());
let store = MemoryKeyStore::new();
store.set("openrouter", FAKE_KEY).unwrap();
let outcome = ops::probe(&store, &cfg, "openrouter").await.expect("probe");
let ProbeOutcome::Failed(reason) = &outcome else {
panic!("expected a Failed outcome for a 400 response, got {outcome:?}");
};
assert!(
!reason.contains(FAKE_KEY),
"ProbeOutcome::Failed leaked the key: {reason}"
);
let mut out = Vec::new();
ops::report_probe("openrouter", &outcome, &mut out).expect("report");
let s = out_string(&out);
assert!(!s.contains(FAKE_KEY), "report_probe leaked the key: {s}");
assert!(s.contains("ERROR"), "{s}");
}
#[tokio::test]
#[serial(dotenv_credential_env)]
async fn test_probe_unconfigured_when_no_key() {
clear_provider_env();
let cfg = mock_configurator("http://127.0.0.1:1".to_string());
let store = MemoryKeyStore::new();
let outcome = ops::probe(&store, &cfg, "openrouter").await.expect("probe");
assert_eq!(outcome, ProbeOutcome::Unconfigured);
assert!(outcome.into_result().is_ok());
}
#[tokio::test]
#[serial(dotenv_credential_env)]
async fn test_probe_bedrock_is_unsupported() {
clear_provider_env();
let cfg = mock_configurator("http://127.0.0.1:1".to_string());
let store = MemoryKeyStore::new();
let outcome = ops::probe(&store, &cfg, "bedrock").await.expect("probe");
assert!(
matches!(outcome, ProbeOutcome::Unsupported(_)),
"{outcome:?}"
);
}