Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
66 changes: 65 additions & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 7 additions & 0 deletions crates/agentkeys-cli/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -32,3 +32,10 @@ axum = { version = "0.7", features = ["json"] }
rusqlite = { version = "0.31", features = ["bundled"] }
serde_json = { workspace = true }
tempfile = "3"
# Serialize tests that mutate process-global env (HOME). See issue #34
# and the comment on `init_test_env` in `tests/cli_tests.rs`.
serial_test = "3"
# Run `AGENTKEYS_SESSION_STORE=file` as a `#[ctor]` pre-main hook so every
# test binary starts with the env var set — no race window where an early
# test hits the real keychain before the OnceLock runs (issue #34).
ctor = "0.2"
27 changes: 17 additions & 10 deletions crates/agentkeys-cli/tests/cli_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,25 @@ use agentkeys_cli::session_store;
use agentkeys_core::backend::CredentialBackend;
use agentkeys_mock_server::test_client::InProcessBackend;
use agentkeys_types::Session;
use serial_test::serial;

fn create_test_backend() -> Arc<InProcessBackend> {
Arc::new(InProcessBackend::new())
}

/// Set `AGENTKEYS_SESSION_STORE=file` BEFORE the test binary's main runs.
/// `#[ctor::ctor]` runs this function during dylib/init, well before any
/// test thread starts, so no race window exists where one test could hit
/// `cmd_init` → `save_session` → real keychain while another test is
/// still trying to set the env var. This replaces an earlier `OnceLock`
/// approach that still had a narrow startup race (issue #34).
#[ctor::ctor]
fn init_test_env() {
unsafe { std::env::set_var("AGENTKEYS_SESSION_STORE", "file"); }
}

/// Initialize a session via the in-process backend and return both wallet and session.
async fn init_session_direct(backend: &Arc<InProcessBackend>) -> (String, Session) {
unsafe { std::env::set_var("AGENTKEYS_SESSION_STORE", "file"); }
let ctx = CommandContext::new("unused", false, false)
.with_backend(backend.clone() as Arc<dyn CredentialBackend>);
let (output, session) = cmd_init(&ctx, Some("test-token-unique".to_string()))
Expand Down Expand Up @@ -115,8 +126,8 @@ async fn cli_revoke_then_read() {

// Test: cmd_revoke_self_clears_local_session
#[tokio::test(flavor = "multi_thread")]
#[serial]
async fn cmd_revoke_self_clears_local_session() {
unsafe { std::env::set_var("AGENTKEYS_SESSION_STORE", "file"); }

let temp_dir = tempfile::tempdir().unwrap();
let temp_home = temp_dir.path().to_str().unwrap().to_string();
Expand Down Expand Up @@ -183,8 +194,8 @@ async fn cmd_revoke_with_agent_calls_revoke_by_wallet() {
// be wiped (same as the no-arg self-revoke form), so subsequent commands
// don't load a stale revoked token.
#[tokio::test(flavor = "multi_thread")]
#[serial]
async fn cmd_revoke_with_own_wallet_clears_local_session() {
unsafe { std::env::set_var("AGENTKEYS_SESSION_STORE", "file"); }

let temp_dir = tempfile::tempdir().unwrap();
let temp_home = temp_dir.path().to_str().unwrap().to_string();
Expand Down Expand Up @@ -229,8 +240,8 @@ async fn cmd_revoke_with_own_wallet_clears_local_session() {
// Counterpart to the above: revoking SOMEONE ELSE's wallet must NOT touch
// the caller's local session file.
#[tokio::test(flavor = "multi_thread")]
#[serial]
async fn cmd_revoke_with_other_wallet_keeps_local_session() {
unsafe { std::env::set_var("AGENTKEYS_SESSION_STORE", "file"); }

let temp_dir = tempfile::tempdir().unwrap();
let temp_home = temp_dir.path().to_str().unwrap().to_string();
Expand Down Expand Up @@ -270,8 +281,8 @@ async fn cmd_revoke_with_other_wallet_keeps_local_session() {

// Test: cmd_revoke_no_session_errors_cleanly
#[tokio::test(flavor = "multi_thread")]
#[serial]
async fn cmd_revoke_no_session_errors_cleanly() {
unsafe { std::env::set_var("AGENTKEYS_SESSION_STORE", "file"); }

let temp_dir = tempfile::tempdir().unwrap();
let temp_home = temp_dir.path().to_str().unwrap().to_string();
Expand Down Expand Up @@ -345,7 +356,6 @@ async fn cli_link_alias() {
});
let base_url = format!("http://127.0.0.1:{}", addr.port());

unsafe { std::env::set_var("AGENTKEYS_SESSION_STORE", "file"); }
let bare_ctx = CommandContext::new(&base_url, false, false);
let (output, session) = cmd_init(&bare_ctx, Some("test-token-unique".to_string()))
.await
Expand Down Expand Up @@ -658,7 +668,6 @@ async fn cmd_store_resolves_alias() {
});
let base_url = format!("http://127.0.0.1:{}", addr.port());

unsafe { std::env::set_var("AGENTKEYS_SESSION_STORE", "file"); }
let bare_ctx = CommandContext::new(&base_url, false, false);
let (output, session) = cmd_init(&bare_ctx, Some("test-token-alias".to_string())).await.unwrap();
let wallet = output.split("Wallet: ").nth(1).unwrap().trim().to_string();
Expand Down Expand Up @@ -693,7 +702,6 @@ async fn cmd_read_unknown_identity_errors_cleanly() {
});
let base_url = format!("http://127.0.0.1:{}", addr.port());

unsafe { std::env::set_var("AGENTKEYS_SESSION_STORE", "file"); }
let bare_ctx = CommandContext::new(&base_url, false, false);
let (_output, session) = cmd_init(&bare_ctx, Some("test-token-unknown".to_string())).await.unwrap();

Expand Down Expand Up @@ -730,12 +738,11 @@ async fn start_scope_test_server() -> (String, String, String) {
});
let base_url = format!("http://127.0.0.1:{}", addr.port());

unsafe { std::env::set_var("AGENTKEYS_SESSION_STORE", "file"); }
let bare_ctx = CommandContext::new(&base_url, false, false);
let (output, _session) = cmd_init(&bare_ctx, Some("scope-test-unique".to_string()))
.await
.unwrap();
let master_wallet = output.split("Wallet: ").nth(1).unwrap().trim().to_string();
let _master_wallet = output.split("Wallet: ").nth(1).unwrap().trim().to_string();

// Create a child session with initial scope [a, b]
let http_client = reqwest::Client::new();
Expand Down
Loading