#![allow(clippy::expect_used, clippy::panic, clippy::unwrap_used)]
use std::path::PathBuf;
use std::process::{Command, Stdio};
fn seed_managed_openai_realm_config(root: &std::path::Path) {
seed_managed_openai_realm_config_with_ids(root, "default_openai", "default_openai");
}
fn seed_managed_openai_realm_config_with_ids(
root: &std::path::Path,
auth_profile_id: &str,
binding_id: &str,
) {
seed_openai_realm_config_with_ids_and_method(
root,
auth_profile_id,
binding_id,
"openai_api",
"api_key",
);
}
fn seed_openai_realm_config_with_ids_and_method(
root: &std::path::Path,
auth_profile_id: &str,
binding_id: &str,
backend_kind: &str,
auth_method: &str,
) {
let config = format!(
r#"
[realm.dev]
default_binding = "{binding_id}"
[realm.dev.backend.openai_backend]
provider = "openai"
backend_kind = "{backend_kind}"
[realm.dev.auth.{auth_profile_id}]
provider = "openai"
auth_method = "{auth_method}"
source = {{ kind = "managed_store" }}
[realm.dev.binding.{binding_id}]
backend_profile = "openai_backend"
auth_profile = "{auth_profile_id}"
"#
);
let mut realm_ids = std::collections::BTreeSet::from(["dev".to_string()]);
let cwd = std::env::current_dir().unwrap_or_else(|_| PathBuf::from("."));
realm_ids.insert(meerkat_core::derive_workspace_realm_id(&cwd));
let manifest_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
realm_ids.insert(meerkat_core::derive_workspace_realm_id(&manifest_dir));
if let Some(workspace_root) = manifest_dir.parent().and_then(std::path::Path::parent) {
realm_ids.insert(meerkat_core::derive_workspace_realm_id(workspace_root));
}
for realm_id in realm_ids {
let realm_dir = root.join("realms").join(realm_id);
std::fs::create_dir_all(&realm_dir).expect("mkdir realm config dir");
std::fs::write(realm_dir.join("config.toml"), &config).expect("write realm config");
}
}
fn token_file_path(root: &std::path::Path) -> PathBuf {
token_file_path_for_binding(root, "default_openai")
}
fn token_file_path_in_realm(root: &std::path::Path, realm: &str, binding_id: &str) -> PathBuf {
#[cfg(target_os = "macos")]
let config_root = root.join("Library").join("Application Support");
#[cfg(not(target_os = "macos"))]
let config_root = root.join("config");
config_root
.join("meerkat")
.join("credentials")
.join(realm)
.join(format!("{binding_id}.json"))
}
fn token_file_path_for_binding(root: &std::path::Path, binding_id: &str) -> PathBuf {
token_file_path_in_realm(root, "dev", binding_id)
}
fn seed_token_file(root: &std::path::Path, body: &str) {
seed_token_file_for_binding(root, "default_openai", body);
}
fn seed_token_file_for_binding(root: &std::path::Path, binding_id: &str, body: &str) {
let token_file = token_file_path_for_binding(root, binding_id);
let token_dir = token_file.parent().expect("token file has parent");
std::fs::create_dir_all(token_dir).expect("mkdir token dir");
std::fs::write(token_file, body).expect("write token file");
}
fn token_file_exists(root: &std::path::Path) -> bool {
token_file_path(root).exists()
}
fn rkat_binary() -> Option<PathBuf> {
if let Some(path) = std::env::var_os("CARGO_BIN_EXE_rkat") {
let p = PathBuf::from(path);
if p.exists() {
return Some(p.canonicalize().unwrap_or(p));
}
}
if let Some(target_dir) = std::env::var_os("CARGO_TARGET_DIR") {
let target_dir = PathBuf::from(target_dir);
for profile in ["debug", "release"] {
let candidate = target_dir.join(profile).join("rkat");
if candidate.exists() {
return Some(candidate);
}
}
}
let manifest_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
let workspace_root = manifest_dir.parent()?.parent()?;
let codex_debug = workspace_root.join("target-codex/debug/rkat");
if codex_debug.exists() {
return Some(codex_debug);
}
let codex_release = workspace_root.join("target-codex/release/rkat");
if codex_release.exists() {
return Some(codex_release);
}
None
}
#[test]
fn rkat_auth_help_lists_all_subcommands() {
let Some(rkat) = rkat_binary() else {
eprintln!("SKIP: rkat binary unavailable");
return;
};
let out = Command::new(&rkat)
.args(["auth", "--help"])
.stdin(Stdio::null())
.output()
.expect("rkat auth --help must spawn");
assert!(
out.status.success(),
"rkat auth --help must exit 0; stderr={}",
String::from_utf8_lossy(&out.stderr)
);
let stdout = String::from_utf8_lossy(&out.stdout);
for subcommand in ["login", "logout", "profile", "status", "binding"] {
assert!(
stdout.contains(subcommand),
"rkat auth --help must mention `{subcommand}` subcommand; stdout:\n{stdout}"
);
}
}
#[test]
fn rkat_auth_logout_clears_malformed_token_file() {
let Some(rkat) = rkat_binary() else {
eprintln!("SKIP: rkat binary unavailable");
return;
};
let tmp = tempfile::TempDir::new().expect("tempdir");
let token = token_file_path_in_realm(tmp.path(), "global", "default_openai");
std::fs::create_dir_all(token.parent().expect("token parent")).expect("mkdir token dir");
std::fs::write(&token, "{ malformed token json").expect("write token");
let logout = Command::new(&rkat)
.args(["auth", "logout", "default_openai"])
.env("HOME", tmp.path())
.env("XDG_CONFIG_HOME", tmp.path().join("config"))
.stdin(Stdio::null())
.output()
.expect("rkat auth logout must spawn");
if !logout.status.success() {
let stderr = String::from_utf8_lossy(&logout.stderr);
if stderr.contains("requires the `anthropic`, `openai`, and `gemini`") {
eprintln!("SKIP: auth provider features unavailable");
return;
}
panic!("logout must clear malformed token file; stderr={stderr}");
}
assert!(
!token.exists(),
"logout must remove malformed persisted credentials"
);
}
#[test]
fn rkat_auth_profile_delete_clears_malformed_token_file() {
let Some(rkat) = rkat_binary() else {
eprintln!("SKIP: rkat binary unavailable");
return;
};
let tmp = tempfile::TempDir::new().expect("tempdir");
seed_managed_openai_realm_config(tmp.path());
seed_token_file(tmp.path(), "{ malformed token json");
let delete = Command::new(&rkat)
.args([
"--state-root",
tmp.path().join("realms").to_str().expect("utf8 path"),
"--realm",
"dev",
"auth",
"profile-delete",
"--realm",
"dev",
"default_openai",
"--yes",
])
.env("HOME", tmp.path())
.env("XDG_CONFIG_HOME", tmp.path().join("config"))
.stdin(Stdio::null())
.output()
.expect("rkat auth profile-delete must spawn");
if !delete.status.success() {
let stderr = String::from_utf8_lossy(&delete.stderr);
if stderr.contains("requires the `anthropic`, `openai`, and `gemini`") {
eprintln!("SKIP: auth provider features unavailable");
return;
}
panic!("profile delete must clear malformed token file; stderr={stderr}");
}
assert!(
!token_file_exists(tmp.path()),
"profile delete must remove malformed persisted credentials"
);
}
#[test]
fn rkat_auth_profile_delete_clears_binding_scoped_token_when_profile_id_differs() {
let Some(rkat) = rkat_binary() else {
eprintln!("SKIP: rkat binary unavailable");
return;
};
let tmp = tempfile::TempDir::new().expect("tempdir");
seed_managed_openai_realm_config_with_ids(tmp.path(), "openai_managed", "default_openai");
seed_token_file_for_binding(tmp.path(), "default_openai", "{ malformed token json");
let delete = Command::new(&rkat)
.args([
"--state-root",
tmp.path().join("realms").to_str().expect("utf8 path"),
"--realm",
"dev",
"auth",
"profile-delete",
"--realm",
"dev",
"openai_managed",
"--yes",
])
.env("HOME", tmp.path())
.env("XDG_CONFIG_HOME", tmp.path().join("config"))
.stdin(Stdio::null())
.output()
.expect("rkat auth profile-delete must spawn");
if !delete.status.success() {
let stderr = String::from_utf8_lossy(&delete.stderr);
if stderr.contains("requires the `anthropic`, `openai`, and `gemini`") {
eprintln!("SKIP: auth provider features unavailable");
return;
}
panic!("profile delete must clear binding-scoped token file; stderr={stderr}");
}
let stdout = String::from_utf8_lossy(&delete.stdout);
assert!(
stdout.contains("deleted: dev:default_openai"),
"profile delete should report the binding-scoped token key; stdout:\n{stdout}"
);
assert!(
!token_file_path_for_binding(tmp.path(), "default_openai").exists(),
"profile delete must remove the token file keyed by the owning binding"
);
}
#[test]
fn rkat_auth_status_hides_token_metadata_without_lifecycle() {
let Some(rkat) = rkat_binary() else {
eprintln!("SKIP: rkat binary unavailable");
return;
};
let tmp = tempfile::TempDir::new().expect("tempdir");
seed_managed_openai_realm_config(tmp.path());
seed_token_file(
tmp.path(),
r#"{
"auth_mode": "api_key",
"primary_secret": "sk-stale",
"refresh_token": "refresh-stale",
"expires_at": 1800000000,
"last_refresh": 1700000000,
"scopes": ["email"],
"account_id": "acct-stale"
}"#,
);
let status = Command::new(&rkat)
.args([
"--state-root",
tmp.path().join("realms").to_str().expect("utf8 path"),
"--realm",
"dev",
"auth",
"status",
"--realm",
"dev",
"default_openai",
])
.env("HOME", tmp.path())
.env("XDG_CONFIG_HOME", tmp.path().join("config"))
.stdin(Stdio::null())
.output()
.expect("rkat auth status must spawn");
if !status.status.success() {
let stderr = String::from_utf8_lossy(&status.stderr);
if stderr.contains("requires the `anthropic`, `openai`, and `gemini`") {
eprintln!("SKIP: auth provider features unavailable");
return;
}
panic!("status must succeed with seeded config; stderr={stderr}");
}
let stdout = String::from_utf8_lossy(&status.stdout);
assert!(
stdout.contains("state: missing_credential"),
"status should report missing_credential without a live AuthMachine lifecycle; stdout:\n{stdout}"
);
for forbidden in [
"auth_mode:",
"has_secret:",
"has_refresh:",
"expires_at:",
"last_refresh:",
"account_id:",
"scopes:",
"acct-stale",
"email",
] {
assert!(
!stdout.contains(forbidden),
"status must not expose token-derived `{forbidden}` without lifecycle; stdout:\n{stdout}"
);
}
}
#[test]
fn rkat_auth_status_fails_closed_on_malformed_token_storage() {
let Some(rkat) = rkat_binary() else {
eprintln!("SKIP: rkat binary unavailable");
return;
};
let tmp = tempfile::TempDir::new().expect("tempdir");
seed_managed_openai_realm_config(tmp.path());
seed_token_file(tmp.path(), "{ malformed token json");
let status = Command::new(&rkat)
.args([
"--state-root",
tmp.path().join("realms").to_str().expect("utf8 path"),
"--realm",
"dev",
"auth",
"status",
"--realm",
"dev",
"default_openai",
])
.env("HOME", tmp.path())
.env("XDG_CONFIG_HOME", tmp.path().join("config"))
.stdin(Stdio::null())
.output()
.expect("rkat auth status must spawn");
let stderr = String::from_utf8_lossy(&status.stderr);
if stderr.contains("requires the `anthropic`, `openai`, and `gemini`") {
eprintln!("SKIP: auth provider features unavailable");
return;
}
assert!(
!status.status.success(),
"status must fail closed on malformed token storage; stdout:\n{}",
String::from_utf8_lossy(&status.stdout)
);
assert!(
stderr.contains("TokenStore rehydration failed"),
"failure must surface the typed token-store fault; stderr:\n{stderr}"
);
}
#[test]
fn rkat_auth_refresh_uses_binding_scoped_token_when_profile_id_differs() {
let Some(rkat) = rkat_binary() else {
eprintln!("SKIP: rkat binary unavailable");
return;
};
let tmp = tempfile::TempDir::new().expect("tempdir");
seed_openai_realm_config_with_ids_and_method(
tmp.path(),
"openai_managed",
"default_openai",
"chatgpt_backend",
"managed_chatgpt_oauth",
);
seed_token_file(
tmp.path(),
r#"{
"auth_mode": "chatgpt_oauth",
"primary_secret": "fresh-chatgpt-access",
"refresh_token": "refresh-token",
"expires_at": 1893456000,
"last_refresh": 1700000000,
"scopes": ["openid", "email"],
"account_id": "acct-fresh",
"metadata": {
"meerkat_auth_lifecycle": {
"published": true,
"version": 2,
"expires_at": 1893456000
}
}
}"#,
);
let refresh = Command::new(&rkat)
.args([
"--state-root",
tmp.path().join("realms").to_str().expect("utf8 path"),
"--realm",
"dev",
"auth",
"refresh",
"--realm",
"dev",
"openai_managed",
])
.env("HOME", tmp.path())
.env("XDG_CONFIG_HOME", tmp.path().join("config"))
.stdin(Stdio::null())
.output()
.expect("rkat auth refresh must spawn");
if !refresh.status.success() {
let stderr = String::from_utf8_lossy(&refresh.stderr);
if stderr.contains("requires the `anthropic`, `openai`, and `gemini`") {
eprintln!("SKIP: auth provider features unavailable");
return;
}
if stderr.contains("token endpoint error") || stderr.contains("stale credential material") {
assert!(
!stderr.contains("missing secret for auth resolution"),
"refresh must resolve the binding-scoped token before reaching the provider; stderr={stderr}"
);
eprintln!("SKIP: fake refresh token was rejected after binding-scoped resolution");
return;
}
panic!("refresh must use the binding-scoped token key; stderr={stderr}");
}
let stdout = String::from_utf8_lossy(&refresh.stdout);
assert!(
stdout.contains("profile: dev:openai_managed"),
"refresh should still report the requested auth profile; stdout:\n{stdout}"
);
assert!(
stdout.contains("refresh: ok"),
"refresh must not skip when credentials exist under the owning binding; stdout:\n{stdout}"
);
assert!(
!stdout.contains("reason: no persisted credential"),
"refresh must not preflight token storage by auth profile id; stdout:\n{stdout}"
);
}
#[test]
fn rkat_auth_status_resolves_binding_that_references_auth_profile() {
let Some(rkat) = rkat_binary() else {
eprintln!("SKIP: rkat binary unavailable");
return;
};
let tmp = tempfile::TempDir::new().expect("tempdir");
seed_managed_openai_realm_config_with_ids(tmp.path(), "openai_managed", "default_openai");
let status = Command::new(&rkat)
.args([
"--state-root",
tmp.path().join("realms").to_str().expect("utf8 path"),
"--realm",
"dev",
"auth",
"status",
"--realm",
"dev",
"openai_managed",
])
.env("HOME", tmp.path())
.env("XDG_CONFIG_HOME", tmp.path().join("config"))
.stdin(Stdio::null())
.output()
.expect("rkat auth status must spawn");
assert!(
status.status.success(),
"status must resolve binding-scoped lease identity from auth profile; stderr={}",
String::from_utf8_lossy(&status.stderr)
);
let stdout = String::from_utf8_lossy(&status.stdout);
assert!(
stdout.contains("binding_id: default_openai"),
"status must report the binding that owns the auth lease key; stdout:\n{stdout}"
);
assert!(
stdout.contains("state: missing_credential"),
"status should still be missing_credential without a live AuthMachine lifecycle; stdout:\n{stdout}"
);
}
#[test]
fn rkat_auth_profile_list_returns_cleanly_for_empty_realm() {
let Some(rkat) = rkat_binary() else {
eprintln!("SKIP: rkat binary unavailable");
return;
};
let tmp = tempfile::TempDir::new().expect("tempdir");
let out = Command::new(&rkat)
.args(["auth", "profile", "list", "--realm", "nonexistent-realm"])
.env("HOME", tmp.path())
.env("XDG_CONFIG_HOME", tmp.path())
.stdin(Stdio::null())
.output()
.expect("rkat auth profile list must spawn");
if !out.status.success() {
let stderr = String::from_utf8_lossy(&out.stderr);
assert!(
!stderr.is_empty(),
"non-zero exit must include stderr diagnostic"
);
assert!(
!stderr.contains("thread 'main' panicked"),
"CLI must not panic; stderr:\n{stderr}"
);
}
}
#[test]
fn rkat_run_auth_binding_flag_is_registered_and_routes() {
let Some(rkat) = rkat_binary() else {
eprintln!("SKIP: rkat binary unavailable");
return;
};
let tmp = tempfile::TempDir::new().expect("tempdir");
std::fs::create_dir_all(tmp.path().join(".rkat")).expect("mkdir .rkat");
let out = Command::new(&rkat)
.args(["run", "--auth-binding", "nonexistent:binding", "hi"])
.current_dir(tmp.path())
.env("HOME", tmp.path())
.env("XDG_CONFIG_HOME", tmp.path())
.env("RKAT_TEST_CLIENT", "1") .stdin(Stdio::null())
.output()
.expect("rkat run must spawn");
let stderr = String::from_utf8_lossy(&out.stderr);
assert!(
!stderr.contains("unrecognized argument")
&& !stderr.contains("unexpected argument")
&& !stderr.contains("found argument '--auth-binding'"),
"--auth-binding must be a registered CLI flag; stderr:\n{stderr}"
);
assert!(
!stderr.contains("thread 'main' panicked"),
"rkat run must not panic on unknown realm; stderr:\n{stderr}"
);
}
#[test]
fn rkat_auth_logout_clears_scripted_login_token_key() {
let Some(rkat) = rkat_binary() else {
eprintln!("SKIP: rkat binary unavailable");
return;
};
let tmp = tempfile::TempDir::new().expect("tempdir");
let login = Command::new(&rkat)
.args([
"auth",
"login",
"openai",
"--non-interactive",
"--secret",
"sk-test",
])
.env("HOME", tmp.path())
.env("XDG_CONFIG_HOME", tmp.path())
.stdin(Stdio::null())
.output()
.expect("rkat auth login must spawn");
if !login.status.success() {
let stderr = String::from_utf8_lossy(&login.stderr);
if stderr.contains("requires the `anthropic`, `openai`, and `gemini`") {
eprintln!("SKIP: auth provider features unavailable");
return;
}
panic!("scripted login failed: {stderr}");
}
let logout = Command::new(&rkat)
.args(["auth", "logout", "default_openai"])
.env("HOME", tmp.path())
.env("XDG_CONFIG_HOME", tmp.path())
.stdin(Stdio::null())
.output()
.expect("rkat auth logout must spawn");
assert!(
logout.status.success(),
"logout must clear scripted login token; stderr={}",
String::from_utf8_lossy(&logout.stderr)
);
let repeat = Command::new(&rkat)
.args(["auth", "logout", "default_openai"])
.env("HOME", tmp.path())
.env("XDG_CONFIG_HOME", tmp.path())
.stdin(Stdio::null())
.output()
.expect("repeat rkat auth logout must spawn");
assert!(
!repeat.status.success(),
"repeat logout must not report success for already-cleared credentials"
);
let stderr = String::from_utf8_lossy(&repeat.stderr);
assert!(
stderr.contains("No credentials found"),
"repeat logout should explain that no token remains; stderr={stderr}"
);
assert!(
!stderr.contains("thread 'main' panicked"),
"repeat logout must not panic; stderr={stderr}"
);
}
#[test]
fn rkat_prompt_loads_legacy_model_fallback_default_with_warning() {
let Some(rkat) = rkat_binary() else {
eprintln!("SKIP: rkat binary unavailable");
return;
};
let tmp = tempfile::TempDir::new().expect("tempdir");
let home = tmp.path().join("home");
let workspace = tmp.path().join("workspace");
std::fs::create_dir_all(home.join(".rkat")).expect("mkdir ~/.rkat");
std::fs::create_dir_all(&workspace).expect("mkdir workspace");
let legacy_doc = "[model_fallback]\nenabled = true\n\n[realm.global]\n";
let global_doc = home.join(".rkat").join("config.toml");
std::fs::write(&global_doc, legacy_doc).expect("write legacy global config");
let mut command = Command::new(&rkat);
command
.arg("Hello")
.current_dir(&workspace)
.env("HOME", &home)
.env("XDG_CONFIG_HOME", home.join(".config"))
.env("XDG_DATA_HOME", home.join(".local/share"))
.env("XDG_STATE_HOME", home.join(".local/state"))
.env_remove("RKAT_TEST_CLIENT")
.stdin(Stdio::null());
for key in [
"ANTHROPIC_API_KEY",
"RKAT_ANTHROPIC_API_KEY",
"OPENAI_API_KEY",
"RKAT_OPENAI_API_KEY",
"GEMINI_API_KEY",
"GOOGLE_API_KEY",
"RKAT_GEMINI_API_KEY",
] {
command.env_remove(key);
}
let out = command.output().expect("rkat must spawn");
let stderr = String::from_utf8_lossy(&out.stderr);
assert!(
!stderr.contains("requires a nonempty explicit chain"),
"a pre-0.8.37 config must not brick rkat; stderr:\n{stderr}"
);
assert!(
!stderr.contains("Invalid runtime config"),
"config must load; stderr:\n{stderr}"
);
assert!(
!stderr.contains("panicked"),
"rkat must not panic; stderr:\n{stderr}"
);
assert!(
stderr.contains("missing_secret"),
"rkat must proceed past config loading to credential resolution; stderr:\n{stderr}"
);
let warning_lines: Vec<&str> = stderr
.lines()
.filter(|line| line.contains("model_fallback.enabled = true"))
.collect();
assert_eq!(
warning_lines.len(),
1,
"the legacy-default warning prints exactly once; stderr:\n{stderr}"
);
let warning = warning_lines[0];
for needle in [
"0.8.37",
"ignored",
"inherits",
"delete the [model_fallback] table",
"[[model_fallback.chain]]",
] {
assert!(
warning.contains(needle),
"warning must mention {needle}; line: {warning}"
);
}
assert!(
!warning.contains("enabled = false"),
"an explicit `enabled = false` would block inheritance; line: {warning}"
);
assert!(
warning.contains(&global_doc.display().to_string()),
"warning names the document to fix; line: {warning}"
);
assert_eq!(
std::fs::read_to_string(&global_doc).expect("read global config"),
legacy_doc,
"loading must not rewrite the operator's document"
);
}
const LEGACY_FALLBACK_WARNING_NEEDLE: &str = "model_fallback.enabled = true with no";
fn legacy_fallback_warning_lines(stderr: &str) -> Vec<&str> {
stderr
.lines()
.filter(|line| line.contains(LEGACY_FALLBACK_WARNING_NEEDLE))
.collect()
}
fn isolated_rkat_command(rkat: &std::path::Path, root: &std::path::Path) -> Command {
let mut command = Command::new(rkat);
command
.env("HOME", root)
.env("XDG_CONFIG_HOME", root.join("config"))
.env("XDG_DATA_HOME", root.join("data"))
.env("XDG_STATE_HOME", root.join("state"))
.env_remove("RKAT_TEST_CLIENT")
.stdin(Stdio::null());
for key in [
"ANTHROPIC_API_KEY",
"RKAT_ANTHROPIC_API_KEY",
"OPENAI_API_KEY",
"RKAT_OPENAI_API_KEY",
"GEMINI_API_KEY",
"GOOGLE_API_KEY",
"RKAT_GEMINI_API_KEY",
] {
command.env_remove(key);
}
command
}
#[test]
fn rkat_config_get_and_patch_print_legacy_model_fallback_warning() {
let Some(rkat) = rkat_binary() else {
eprintln!("SKIP: rkat binary unavailable");
return;
};
let tmp = tempfile::TempDir::new().expect("tempdir");
let state_root = tmp.path().join("realms");
let realm_doc = state_root.join("dev").join("config.toml");
std::fs::create_dir_all(realm_doc.parent().expect("realm dir")).expect("mkdir realm");
let legacy_doc = "[model_fallback]\nenabled = true\n";
std::fs::write(&realm_doc, legacy_doc).expect("write legacy realm doc");
let state_root_arg = state_root.to_str().expect("utf8 path");
let get = isolated_rkat_command(&rkat, tmp.path())
.args([
"--state-root",
state_root_arg,
"--realm",
"dev",
"config",
"get",
])
.output()
.expect("rkat config get must spawn");
let stderr = String::from_utf8_lossy(&get.stderr);
assert!(
get.status.success(),
"config get must succeed; stderr:\n{stderr}"
);
let lines = legacy_fallback_warning_lines(&stderr);
assert_eq!(lines.len(), 1, "warning prints once; stderr:\n{stderr}");
assert!(
lines[0].contains(&realm_doc.display().to_string()),
"warning names the document; line: {}",
lines[0]
);
assert_eq!(
std::fs::read_to_string(&realm_doc).expect("reread"),
legacy_doc,
"config get never rewrites the document"
);
let patch = isolated_rkat_command(&rkat, tmp.path())
.args([
"--state-root",
state_root_arg,
"--realm",
"dev",
"config",
"patch",
"--json",
r#"{"max_tokens":1234}"#,
])
.output()
.expect("rkat config patch must spawn");
let stderr = String::from_utf8_lossy(&patch.stderr);
assert!(
patch.status.success(),
"config patch must succeed; stderr:\n{stderr}"
);
assert_eq!(
legacy_fallback_warning_lines(&stderr).len(),
1,
"an unrelated patch over a legacy doc warns once; stderr:\n{stderr}"
);
let persisted: toml::Value =
toml::from_str(&std::fs::read_to_string(&realm_doc).expect("reread")).expect("toml");
assert_eq!(
persisted["max_tokens"].as_integer(),
Some(1234),
"{persisted}"
);
assert!(
persisted
.get("model_fallback")
.and_then(|table| table.get("enabled"))
.is_none(),
"an unrelated patch persists the normalized no-policy form; {persisted}"
);
std::fs::write(&realm_doc, legacy_doc).expect("restore legacy realm doc");
let add_chain = isolated_rkat_command(&rkat, tmp.path())
.args([
"--state-root",
state_root_arg,
"--realm",
"dev",
"config",
"patch",
"--json",
r#"{"model_fallback":{"chain":[{"model":"gpt-5.5","provider":"openai"}]}}"#,
])
.output()
.expect("rkat config patch must spawn");
let stderr = String::from_utf8_lossy(&add_chain.stderr);
assert!(
add_chain.status.success(),
"adding a chain must succeed; stderr:\n{stderr}"
);
assert!(
legacy_fallback_warning_lines(&stderr).is_empty(),
"the merged result is no longer legacy; stderr:\n{stderr}"
);
let persisted: toml::Value =
toml::from_str(&std::fs::read_to_string(&realm_doc).expect("reread")).expect("toml");
assert_eq!(
persisted["model_fallback"]["enabled"].as_bool(),
Some(true),
"{persisted}"
);
assert_eq!(
persisted["model_fallback"]["chain"]
.as_array()
.map(Vec::len),
Some(1),
"{persisted}"
);
}
#[test]
fn rkat_prompt_prints_legacy_warning_when_credential_bootstrap_rewrites_global_doc() {
let Some(rkat) = rkat_binary() else {
eprintln!("SKIP: rkat binary unavailable");
return;
};
let tmp = tempfile::TempDir::new().expect("tempdir");
let workspace = tmp.path().join("workspace");
std::fs::create_dir_all(&workspace).expect("mkdir workspace");
let global_doc = tmp.path().join(".rkat").join("config.toml");
std::fs::create_dir_all(global_doc.parent().expect("rkat dir")).expect("mkdir ~/.rkat");
std::fs::write(
&global_doc,
"[model_fallback]\nenabled = true\n\n[realm.global]\n",
)
.expect("write legacy global doc");
let token_file = token_file_path_in_realm(tmp.path(), "global", "openai_oauth");
std::fs::create_dir_all(token_file.parent().expect("token dir")).expect("mkdir token dir");
std::fs::write(
&token_file,
r#"{
"auth_mode": "chatgpt_oauth",
"primary_secret": "access-token",
"refresh_token": "refresh-token",
"expires_at": 1800000000,
"last_refresh": 1700000000,
"scopes": ["openid"]
}"#,
)
.expect("seed global oauth token");
let out = isolated_rkat_command(&rkat, tmp.path())
.arg("Hello")
.current_dir(&workspace)
.env("RKAT_TEST_CLIENT", "1")
.output()
.expect("rkat must spawn");
let stderr = String::from_utf8_lossy(&out.stderr);
if stderr.contains("requires the `anthropic`, `openai`, and `gemini`") {
eprintln!("SKIP: auth provider features unavailable");
return;
}
assert!(
!stderr.contains("panicked"),
"rkat must not panic; stderr:\n{stderr}"
);
let rewritten = std::fs::read_to_string(&global_doc).expect("read global doc");
let persisted: toml::Value = toml::from_str(&rewritten).expect("toml");
assert!(
persisted
.get("model_fallback")
.and_then(|table| table.get("enabled"))
.is_none(),
"the bootstrap provisioning write must have run and persisted the \
normalized no-policy form; doc:\n{rewritten}\nstderr:\n{stderr}"
);
let lines = legacy_fallback_warning_lines(&stderr);
assert_eq!(
lines.len(),
1,
"the bootstrap prints the warning once; stderr:\n{stderr}"
);
assert!(
lines[0].contains(&global_doc.display().to_string()),
"warning names the global doc; line: {}",
lines[0]
);
}
#[test]
fn rkat_prompt_skips_legacy_warning_for_global_doc_outside_chain() {
let Some(rkat) = rkat_binary() else {
eprintln!("SKIP: rkat binary unavailable");
return;
};
let tmp = tempfile::TempDir::new().expect("tempdir");
let workspace = tmp.path().join("workspace");
std::fs::create_dir_all(&workspace).expect("mkdir workspace");
let global_doc = tmp.path().join(".rkat").join("config.toml");
std::fs::create_dir_all(global_doc.parent().expect("rkat dir")).expect("mkdir ~/.rkat");
std::fs::write(&global_doc, "[model_fallback]\nenabled = true\n")
.expect("write legacy global doc");
let out = isolated_rkat_command(&rkat, tmp.path())
.arg("Hello")
.current_dir(&workspace)
.output()
.expect("rkat must spawn");
let stderr = String::from_utf8_lossy(&out.stderr);
assert!(
stderr.contains("missing_secret"),
"rkat reaches credential resolution; stderr:\n{stderr}"
);
assert!(
legacy_fallback_warning_lines(&stderr).is_empty(),
"a global doc outside the chain is not reported; stderr:\n{stderr}"
);
}