#![allow(missing_docs, clippy::unwrap_used)]
use std::{fs, process::Command, thread};
use starweaver_session::SessionStore as _;
fn cli(temp: &tempfile::TempDir) -> Command {
let mut command = Command::new(env!("CARGO_BIN_EXE_starweaver-cli"));
command.env("STARWEAVER_PROJECT_DIR", temp.path().join(".starweaver"));
command.env("STARWEAVER_CONFIG_DIR", temp.path().join("global"));
command
}
fn count_json_files(path: &std::path::Path) -> usize {
let Ok(entries) = fs::read_dir(path) else {
return 0;
};
entries
.filter_map(Result::ok)
.map(|entry| entry.path())
.map(|path| {
if path.is_dir() {
count_json_files(&path)
} else {
usize::from(
path.extension()
.is_some_and(|extension| extension == "json"),
)
}
})
.sum()
}
#[test]
fn cli_diagnostics_prints_sdk_and_version() {
let temp = tempfile::tempdir().unwrap();
let output = cli(&temp).arg("diagnostics").output().unwrap();
assert!(
output.status.success(),
"stderr={}",
String::from_utf8_lossy(&output.stderr)
);
let stdout = String::from_utf8(output.stdout).unwrap();
assert!(stdout.contains("sdk=starweaver-agent-sdk"));
assert!(stdout.contains("workspace_version="));
assert!(stdout.contains("wal=true"));
}
#[test]
fn cli_update_accepts_dry_run_and_force_flags() {
let temp = tempfile::tempdir().unwrap();
let output = cli(&temp)
.env("STARWEAVER_INSTALL_DIR", temp.path().join("bin"))
.args(["update", "--dry-run", "--force"])
.output()
.unwrap();
assert!(
output.status.success(),
"stderr={}",
String::from_utf8_lossy(&output.stderr)
);
let stdout = String::from_utf8(output.stdout).unwrap();
assert!(stdout.contains("target=cli"));
assert!(stdout.contains("force=true"));
assert!(stdout.contains("status=dry-run"));
}
#[test]
fn cli_run_prints_display_messages() {
let temp = tempfile::tempdir().unwrap();
let output = cli(&temp)
.args(["run", "hello", "--output", "display-jsonl"])
.output()
.unwrap();
assert!(
output.status.success(),
"stderr={}",
String::from_utf8_lossy(&output.stderr)
);
let stdout = String::from_utf8(output.stdout).unwrap();
let messages = stdout
.lines()
.map(|line| serde_json::from_str::<serde_json::Value>(line).unwrap())
.collect::<Vec<_>>();
assert_eq!(messages[0]["schema"], "starweaver.display.v1");
assert_eq!(messages[0]["type"], "RUN_QUEUED");
for (sequence, message) in messages.iter().enumerate() {
assert_eq!(message["sequence"].as_u64().unwrap(), sequence as u64);
}
let types = messages
.iter()
.map(|message| message["type"].as_str().unwrap())
.collect::<Vec<_>>();
assert!(types.contains(&"CHECKPOINT"));
assert!(types.contains(&"TEXT_MESSAGE_START"));
assert!(types.contains(&"TEXT_MESSAGE_CONTENT"));
assert!(types.contains(&"TEXT_MESSAGE_END"));
assert!(types.contains(&"COMPACTION_STARTED"));
assert!(types.contains(&"COMPACTION_COMPLETED"));
assert!(types.contains(&"RUN_FINISHED"));
let text_messages = messages
.iter()
.filter(|message| message["type"] == "TEXT_MESSAGE_CONTENT")
.collect::<Vec<_>>();
assert_eq!(text_messages.len(), 1);
assert_eq!(text_messages[0]["payload"]["delta"], "local echo: hello");
let session_id = messages[0]["session_id"].as_str().unwrap();
let run_id = messages[0]["run_id"].as_str().unwrap();
assert!(
messages
.iter()
.all(|message| message["run_id"].as_str() == Some(run_id))
);
let raw_stream = temp
.path()
.join(".starweaver/store/sessions")
.join(session_id)
.join("runs")
.join(run_id)
.join("raw.stream.json");
let raw_records = fs::read_to_string(raw_stream).unwrap();
let raw_records: serde_json::Value = serde_json::from_str(&raw_records).unwrap();
let raw_records_array = raw_records.as_array().unwrap();
assert!(
raw_records_array
.iter()
.any(|record| record["event"]["kind"] == "model_response")
);
assert!(raw_records_array.iter().any(|record| {
record["event"]["kind"] == "model_request" && record["event"].get("step").is_some()
}));
let compact = temp
.path()
.join(".starweaver/store/sessions")
.join(session_id)
.join("runs")
.join(run_id)
.join("display.compact.json");
assert!(compact.exists());
let replay = cli(&temp)
.args(["session", "replay", session_id, "--run", run_id])
.output()
.unwrap();
assert!(
replay.status.success(),
"stderr={}",
String::from_utf8_lossy(&replay.stderr)
);
assert_eq!(String::from_utf8(replay.stdout).unwrap(), stdout);
}
#[test]
fn cli_session_list_and_delete_accept_unique_prefix() {
let temp = tempfile::tempdir().unwrap();
let run = cli(&temp)
.args(["-p", "delete me", "--output", "silent"])
.output()
.unwrap();
assert!(run.status.success());
let list = cli(&temp).args(["session", "list"]).output().unwrap();
assert!(list.status.success());
let stdout = String::from_utf8(list.stdout).unwrap();
let session: serde_json::Value = serde_json::from_str(stdout.lines().next().unwrap()).unwrap();
let session_id = session["session_id"].as_str().unwrap();
let prefix = &session_id[..16];
let delete = cli(&temp)
.args(["session", "delete", prefix, "--yes", "--output", "silent"])
.output()
.unwrap();
assert!(
delete.status.success(),
"stderr={}",
String::from_utf8_lossy(&delete.stderr)
);
assert!(
String::from_utf8(delete.stdout)
.unwrap()
.contains("status=deleted")
);
let empty = cli(&temp).args(["session", "list"]).output().unwrap();
assert!(empty.status.success());
assert!(String::from_utf8(empty.stdout).unwrap().trim().is_empty());
assert!(
!temp
.path()
.join(".starweaver/store/sessions")
.join(session_id)
.exists()
);
}
#[test]
fn cli_session_list_and_show_print_local_projection() {
let temp = tempfile::tempdir().unwrap();
let run = cli(&temp)
.args(["-p", "hello", "--output", "silent"])
.output()
.unwrap();
assert!(
run.status.success(),
"stderr={}",
String::from_utf8_lossy(&run.stderr)
);
let list = cli(&temp).args(["session", "list"]).output().unwrap();
assert!(
list.status.success(),
"stderr={}",
String::from_utf8_lossy(&list.stderr)
);
let stdout = String::from_utf8(list.stdout).unwrap();
let session: serde_json::Value = serde_json::from_str(stdout.lines().next().unwrap()).unwrap();
let session_id = session["session_id"].as_str().unwrap();
let show = cli(&temp)
.args(["session", "show", session_id])
.output()
.unwrap();
assert!(
show.status.success(),
"stderr={}",
String::from_utf8_lossy(&show.stderr)
);
let stdout = String::from_utf8(show.stdout).unwrap();
assert!(stdout.contains(session_id));
assert!(stdout.contains("completed"));
}
#[test]
fn cli_config_set_persists_project_config() {
let temp = tempfile::tempdir().unwrap();
let set = cli(&temp)
.args([
"config",
"set",
"trim.current_session_keep_recent_runs",
"3",
])
.output()
.unwrap();
assert!(
set.status.success(),
"stderr={}",
String::from_utf8_lossy(&set.stderr)
);
assert_eq!(
String::from_utf8(set.stdout).unwrap(),
"trim.current_session_keep_recent_runs=3\n"
);
let get = cli(&temp)
.args(["config", "get", "trim.current_session_keep_recent_runs"])
.output()
.unwrap();
assert!(
get.status.success(),
"stderr={}",
String::from_utf8_lossy(&get.stderr)
);
assert_eq!(String::from_utf8(get.stdout).unwrap(), "3\n");
}
#[test]
fn cli_prompt_runs_create_new_session_unless_continue_is_requested() {
let temp = tempfile::tempdir().unwrap();
let first = cli(&temp)
.args(["-p", "first", "--output", "silent"])
.output()
.unwrap();
assert!(
first.status.success(),
"stderr={}",
String::from_utf8_lossy(&first.stderr)
);
let first_stdout = String::from_utf8(first.stdout).unwrap();
let first_session_id = first_stdout
.lines()
.find_map(|line| line.strip_prefix("session_id="))
.unwrap()
.to_string();
let second = cli(&temp)
.args(["-p", "second", "--output", "silent"])
.output()
.unwrap();
assert!(
second.status.success(),
"stderr={}",
String::from_utf8_lossy(&second.stderr)
);
let second_stdout = String::from_utf8(second.stdout).unwrap();
let second_session_id = second_stdout
.lines()
.find_map(|line| line.strip_prefix("session_id="))
.unwrap()
.to_string();
assert_ne!(first_session_id, second_session_id);
let continued = cli(&temp)
.args(["-p", "third", "--continue", "--output", "silent"])
.output()
.unwrap();
assert!(
continued.status.success(),
"stderr={}",
String::from_utf8_lossy(&continued.stderr)
);
let continued_stdout = String::from_utf8(continued.stdout).unwrap();
let continued_session_id = continued_stdout
.lines()
.find_map(|line| line.strip_prefix("session_id="))
.unwrap()
.to_string();
assert_eq!(continued_session_id, second_session_id);
let list = cli(&temp).args(["session", "list"]).output().unwrap();
assert!(
list.status.success(),
"stderr={}",
String::from_utf8_lossy(&list.stderr)
);
let sessions = String::from_utf8(list.stdout)
.unwrap()
.lines()
.map(|line| serde_json::from_str::<serde_json::Value>(line).unwrap())
.collect::<Vec<_>>();
assert_eq!(sessions.len(), 2);
let first_summary = sessions
.iter()
.find(|session| session["session_id"].as_str() == Some(first_session_id.as_str()))
.unwrap();
let second_summary = sessions
.iter()
.find(|session| session["session_id"].as_str() == Some(second_session_id.as_str()))
.unwrap();
assert_eq!(first_summary["run_count"], 1);
assert_eq!(second_summary["run_count"], 2);
}
#[test]
fn concurrent_cli_runs_append_without_sequence_races() {
let temp = tempfile::tempdir().unwrap();
let seed = cli(&temp)
.args(["-p", "seed", "--output", "silent"])
.output()
.unwrap();
assert!(
seed.status.success(),
"stderr={}",
String::from_utf8_lossy(&seed.stderr)
);
let project_dir = temp.path().join(".starweaver");
let global_dir = temp.path().join("global");
let handles = (0..6)
.map(|index| {
let project_dir = project_dir.clone();
let global_dir = global_dir.clone();
thread::spawn(move || {
Command::new(env!("CARGO_BIN_EXE_starweaver-cli"))
.env("STARWEAVER_PROJECT_DIR", project_dir)
.env("STARWEAVER_CONFIG_DIR", global_dir)
.args([
"-p",
&format!("run-{index}"),
"--continue",
"--output",
"silent",
])
.output()
.unwrap()
})
})
.collect::<Vec<_>>();
for handle in handles {
let output = handle.join().unwrap();
assert!(
output.status.success(),
"stdout={} stderr={}",
String::from_utf8_lossy(&output.stdout),
String::from_utf8_lossy(&output.stderr)
);
}
let list = cli(&temp).args(["session", "list"]).output().unwrap();
assert!(
list.status.success(),
"stderr={}",
String::from_utf8_lossy(&list.stderr)
);
let stdout = String::from_utf8(list.stdout).unwrap();
let session: serde_json::Value = serde_json::from_str(stdout.lines().next().unwrap()).unwrap();
assert_eq!(session["run_count"], 7);
let session_id = session["session_id"].as_str().unwrap();
let show = cli(&temp)
.args(["session", "show", session_id, "--runs", "10"])
.output()
.unwrap();
assert!(
show.status.success(),
"stderr={}",
String::from_utf8_lossy(&show.stderr)
);
let sequences = String::from_utf8(show.stdout)
.unwrap()
.lines()
.skip(1)
.map(|line| {
serde_json::from_str::<serde_json::Value>(line).unwrap()["sequence_no"]
.as_u64()
.unwrap()
})
.collect::<Vec<_>>();
assert_eq!(sequences, vec![1, 2, 3, 4, 5, 6, 7]);
}
#[test]
fn cli_config_set_rejects_negative_unsigned_values() {
let temp = tempfile::tempdir().unwrap();
let invalid = cli(&temp)
.args([
"config",
"set",
"trim.current_session_keep_recent_runs",
"-1",
])
.output()
.unwrap();
assert!(!invalid.status.success());
}
#[test]
fn cli_session_replay_orders_runs_by_session_sequence() {
let temp = tempfile::tempdir().unwrap();
for (index, prompt) in ["first", "second", "third", "fourth", "fifth", "sixth"]
.into_iter()
.enumerate()
{
let mut command = cli(&temp);
command.args(["run", prompt]);
if index > 0 {
command.arg("--continue");
}
let output = command.output().unwrap();
assert!(
output.status.success(),
"stderr={}",
String::from_utf8_lossy(&output.stderr)
);
}
let list = cli(&temp).args(["session", "list"]).output().unwrap();
assert!(
list.status.success(),
"stderr={}",
String::from_utf8_lossy(&list.stderr)
);
let stdout = String::from_utf8(list.stdout).unwrap();
let session: serde_json::Value = serde_json::from_str(stdout.lines().next().unwrap()).unwrap();
let session_id = session["session_id"].as_str().unwrap();
let replay = cli(&temp)
.args(["session", "replay", session_id])
.output()
.unwrap();
assert!(
replay.status.success(),
"stderr={}",
String::from_utf8_lossy(&replay.stderr)
);
let replayed_text = String::from_utf8(replay.stdout)
.unwrap()
.lines()
.filter_map(|line| {
let message: serde_json::Value = serde_json::from_str(line).unwrap();
if message["type"] == "TEXT_MESSAGE_CONTENT" {
Some(message["payload"]["delta"].as_str().unwrap().to_string())
} else {
None
}
})
.collect::<Vec<_>>();
assert_eq!(
replayed_text,
vec![
"local echo: first",
"local echo: second",
"local echo: third",
"local echo: fourth",
"local echo: fifth",
"local echo: sixth",
]
);
}
#[test]
fn cli_profile_yaml_config_precedence_and_env_defaults_work() {
let temp = tempfile::tempdir().unwrap();
let project_dir = temp.path().join(".starweaver");
let global_dir = temp.path().join("global");
fs::create_dir_all(project_dir.join("profiles")).unwrap();
fs::create_dir_all(&global_dir).unwrap();
fs::write(
global_dir.join("config.toml"),
r#"
[general]
default_profile = "general"
default_hitl = "deny"
default_output = "display-jsonl"
[environment]
provider = "virtual"
files_policy = "read_write"
[update]
channel = "beta"
"#,
)
.unwrap();
fs::write(
project_dir.join("config.toml"),
r#"
[general]
default_hitl = "defer"
"#,
)
.unwrap();
fs::write(
project_dir.join("profiles/custom.yaml"),
r"
name: custom
instructions:
- Custom deterministic profile.
model:
model_id: local_echo
toolsets:
- environment
",
)
.unwrap();
let run = cli(&temp)
.env("STARWEAVER_OUTPUT", "silent")
.args(["run", "hello", "--profile", "custom"])
.output()
.unwrap();
assert!(
run.status.success(),
"stderr={}",
String::from_utf8_lossy(&run.stderr)
);
let stdout = String::from_utf8(run.stdout).unwrap();
assert!(stdout.contains("status=completed"));
let list = cli(&temp).args(["session", "list"]).output().unwrap();
assert!(list.status.success());
let session: serde_json::Value = serde_json::from_str(
String::from_utf8(list.stdout)
.unwrap()
.lines()
.next()
.unwrap(),
)
.unwrap();
assert_eq!(session["profile"], "custom");
let hitl = cli(&temp)
.args(["config", "get", "general.default_hitl"])
.output()
.unwrap();
assert!(hitl.status.success());
assert_eq!(String::from_utf8(hitl.stdout).unwrap(), "defer\n");
let update = cli(&temp)
.args(["config", "get", "update.channel"])
.output()
.unwrap();
assert!(update.status.success());
assert_eq!(String::from_utf8(update.stdout).unwrap(), "beta\n");
}
#[test]
#[allow(clippy::too_many_lines)]
fn cli_global_config_set_and_env_hitl_override_work() {
let temp = tempfile::tempdir().unwrap();
let set = cli(&temp)
.args([
"config",
"set",
"--global",
"general.default_profile",
"approval_model",
])
.output()
.unwrap();
assert!(
set.status.success(),
"stderr={}",
String::from_utf8_lossy(&set.stderr)
);
let get = cli(&temp)
.args(["config", "get", "general.default_profile"])
.output()
.unwrap();
assert!(get.status.success());
assert_eq!(String::from_utf8(get.stdout).unwrap(), "approval_model\n");
let run = cli(&temp)
.env("STARWEAVER_HITL", "fail")
.args(["run", "needs approval", "--output", "display-jsonl"])
.output()
.unwrap();
assert!(
run.status.success(),
"stderr={}",
String::from_utf8_lossy(&run.stderr)
);
let messages = String::from_utf8(run.stdout)
.unwrap()
.lines()
.map(|line| serde_json::from_str::<serde_json::Value>(line).unwrap())
.collect::<Vec<_>>();
let types = messages
.iter()
.map(|message| message["type"].as_str().unwrap())
.collect::<Vec<_>>();
assert!(types.contains(&"APPROVAL_REQUESTED"));
assert!(types.contains(&"APPROVAL_RESOLVED"));
assert!(
messages.iter().all(|message| {
message["metadata"]["cli_run_policy"]["hitl"].as_str() == Some("fail")
})
);
let prompt_run = cli(&temp)
.args([
"run",
"needs default deferred approval",
"--profile",
"approval_model",
"--output",
"display-jsonl",
])
.output()
.unwrap();
assert!(
prompt_run.status.success(),
"stderr={}",
String::from_utf8_lossy(&prompt_run.stderr)
);
let prompt_messages = String::from_utf8(prompt_run.stdout)
.unwrap()
.lines()
.map(|line| serde_json::from_str::<serde_json::Value>(line).unwrap())
.collect::<Vec<_>>();
let prompt_types = prompt_messages
.iter()
.map(|message| message["type"].as_str().unwrap())
.collect::<Vec<_>>();
assert!(prompt_types.contains(&"APPROVAL_REQUESTED"));
assert!(!prompt_types.contains(&"APPROVAL_RESOLVED"));
assert!(prompt_messages.iter().all(|message| {
message["metadata"]["cli_run_policy"]["hitl"].as_str() == Some("defer")
}));
let prompt_session_id = prompt_messages[0]["session_id"].as_str().unwrap();
let prompt_run_id = prompt_messages[0]["run_id"].as_str().unwrap();
let approvals = cli(&temp)
.args([
"approval",
"list",
"--session",
prompt_session_id,
"--run",
prompt_run_id,
])
.output()
.unwrap();
assert!(
approvals.status.success(),
"stderr={}",
String::from_utf8_lossy(&approvals.stderr)
);
let approval_rows = String::from_utf8(approvals.stdout)
.unwrap()
.lines()
.map(|line| serde_json::from_str::<serde_json::Value>(line).unwrap())
.collect::<Vec<_>>();
assert_eq!(approval_rows.len(), 1);
assert_eq!(approval_rows[0]["status"], "pending");
let list = cli(&temp).args(["session", "list"]).output().unwrap();
let session: serde_json::Value = serde_json::from_str(
String::from_utf8(list.stdout)
.unwrap()
.lines()
.next()
.unwrap(),
)
.unwrap();
assert_eq!(session["head_success_run_id"], serde_json::Value::Null);
}
#[test]
#[allow(clippy::too_many_lines)]
fn cli_persists_restore_environment_control_flow_and_storage_artifacts() {
let temp = tempfile::tempdir().unwrap();
let first = cli(&temp)
.args(["run", "first", "--output", "silent"])
.output()
.unwrap();
assert!(first.status.success());
let first_stdout = String::from_utf8(first.stdout).unwrap();
let session_id = first_stdout
.lines()
.find_map(|line| line.strip_prefix("session_id="))
.unwrap()
.to_string();
let first_run_id = first_stdout
.lines()
.find_map(|line| line.strip_prefix("run_id="))
.unwrap()
.to_string();
let second = cli(&temp)
.args(["run", "second", "--continue", "--output", "silent"])
.output()
.unwrap();
assert!(
second.status.success(),
"stdout={} stderr={}",
String::from_utf8_lossy(&second.stdout),
String::from_utf8_lossy(&second.stderr)
);
let show = cli(&temp)
.args(["session", "show", &session_id])
.output()
.unwrap();
assert!(show.status.success());
let rows = String::from_utf8(show.stdout)
.unwrap()
.lines()
.map(|line| serde_json::from_str::<serde_json::Value>(line).unwrap())
.collect::<Vec<_>>();
assert_eq!(rows[2]["restore_from_run_id"], first_run_id);
let store_root = temp
.path()
.join(".starweaver/store/sessions")
.join(&session_id);
let first_run_root = store_root.join("runs").join(&first_run_id);
assert!(first_run_root.join("context.state.json").exists());
assert!(first_run_root.join("environment.state.json").exists());
let compact: serde_json::Value = serde_json::from_str(
&fs::read_to_string(first_run_root.join("display.compact.json")).unwrap(),
)
.unwrap();
assert!(compact["revision"].as_u64().unwrap() > 0);
let deferred = cli(&temp)
.args([
"run",
"defer me",
"--new-session",
"--profile",
"deferred_model",
"--hitl",
"defer",
"--output",
"silent",
])
.output()
.unwrap();
assert!(
deferred.status.success(),
"stderr={}",
String::from_utf8_lossy(&deferred.stderr)
);
assert!(
String::from_utf8(deferred.stdout)
.unwrap()
.contains("status=waiting")
);
let db = temp.path().join(".starweaver/starweaver.sqlite");
let storage = starweaver_storage::SqliteStorage::open(&db).unwrap();
let sessions = storage.list_sessions().unwrap();
let runs = sessions
.iter()
.flat_map(|session| storage.list_runs(&session.session_id).unwrap())
.collect::<Vec<_>>();
let approvals = storage.list_approvals(None, None).unwrap().len();
let deferred_count = storage.list_deferred_tools(None, None).unwrap().len();
let contexts = runs
.iter()
.filter(|run| {
storage
.load_run_context(&run.session_id, &run.run_id)
.unwrap()
.is_some()
})
.count();
let envs = runs
.iter()
.filter(|run| {
storage
.load_run_environment(&run.session_id, &run.run_id)
.unwrap()
.is_some()
})
.count();
let cursors = runs
.iter()
.map(|run| run.stream_cursors.len())
.sum::<usize>();
let session_store = storage.session_store();
let runtime = tokio::runtime::Runtime::new().unwrap();
let checkpoints = runs
.iter()
.map(|run| {
runtime
.block_on(session_store.load_checkpoints(&run.session_id, &run.run_id))
.unwrap()
.len()
})
.sum::<usize>();
let checkpoint_events = runs
.iter()
.flat_map(|run| {
storage
.load_stream_records(&run.session_id, &run.run_id)
.unwrap()
})
.filter(|record| {
matches!(
record.event,
starweaver_runtime::AgentStreamEvent::Checkpoint { .. }
)
})
.count();
let json_files = count_json_files(&temp.path().join(".starweaver/store"));
assert!(approvals <= runs.len());
assert!(deferred_count >= 1);
assert!(contexts >= 2);
assert!(envs >= 2);
assert!(cursors >= 4);
assert!(checkpoint_events >= 1);
assert!(checkpoints <= checkpoint_events);
assert!(json_files >= 4);
}
#[test]
fn cli_trim_older_than_dry_run_preserves_recent_and_active_runs() {
let temp = tempfile::tempdir().unwrap();
for (index, prompt) in ["one", "two", "three"].into_iter().enumerate() {
let mut command = cli(&temp);
command.args(["run", prompt, "--output", "silent"]);
if index > 0 {
command.arg("--continue");
}
let output = command.output().unwrap();
assert!(output.status.success());
}
let list = cli(&temp).args(["session", "list"]).output().unwrap();
let session: serde_json::Value = serde_json::from_str(
String::from_utf8(list.stdout)
.unwrap()
.lines()
.next()
.unwrap(),
)
.unwrap();
let session_id = session["session_id"].as_str().unwrap();
let dry = cli(&temp)
.args([
"session",
"trim",
"--session",
session_id,
"--keep-runs",
"1",
"--older-than",
"0s",
"--dry-run",
])
.output()
.unwrap();
assert!(dry.status.success());
let report: serde_json::Value = serde_json::from_slice(&dry.stdout).unwrap();
assert_eq!(report["runs_to_trim"], 2);
assert_eq!(report["runs_trimmed"], 0);
let trim = cli(&temp)
.args([
"session",
"trim",
"--session",
session_id,
"--keep-runs",
"1",
"--older-than",
"365d",
])
.output()
.unwrap();
assert!(trim.status.success());
let report: serde_json::Value = serde_json::from_slice(&trim.stdout).unwrap();
assert_eq!(report["runs_to_trim"], 0);
}
#[test]
fn cli_text_output_profiles_config_init_and_provider_config_work() {
let temp = tempfile::tempdir().unwrap();
let text = cli(&temp)
.args(["run", "hello text", "--output", "text"])
.output()
.unwrap();
assert!(
text.status.success(),
"stderr={}",
String::from_utf8_lossy(&text.stderr)
);
assert_eq!(
String::from_utf8(text.stdout).unwrap(),
"local echo: hello text\n"
);
let init = cli(&temp)
.args(["config", "init", "--global", "--force"])
.output()
.unwrap();
assert!(
init.status.success(),
"stderr={}",
String::from_utf8_lossy(&init.stderr)
);
assert!(temp.path().join("global/config.toml").exists());
let set = cli(&temp)
.args([
"config",
"set",
"--global",
"providers.openai.base_url",
"https://gateway.example/v1",
])
.output()
.unwrap();
assert!(
set.status.success(),
"stderr={}",
String::from_utf8_lossy(&set.stderr)
);
let get = cli(&temp)
.args(["config", "get", "providers.openai.base_url"])
.output()
.unwrap();
assert!(get.status.success());
assert_eq!(
String::from_utf8(get.stdout).unwrap(),
"https://gateway.example/v1\n"
);
let profiles = cli(&temp).args(["profile", "list"]).output().unwrap();
assert!(profiles.status.success());
let stdout = String::from_utf8(profiles.stdout).unwrap();
assert!(stdout.contains("\"name\":\"coding\""));
assert!(stdout.contains("openai:gpt-5"));
let show = cli(&temp)
.args(["profile", "show", "coding"])
.output()
.unwrap();
assert!(show.status.success());
let stdout = String::from_utf8(show.stdout).unwrap();
assert!(stdout.contains("name: coding"));
assert!(stdout.contains("model_id: openai:gpt-5"));
let missing_key = cli(&temp)
.args(["run", "hello", "--profile", "coding", "--output", "silent"])
.output()
.unwrap();
assert!(!missing_key.status.success());
assert!(String::from_utf8_lossy(&missing_key.stderr).contains("missing OPENAI_API_KEY"));
}
#[test]
fn cli_google_provider_config_aliases_work() {
let temp = tempfile::tempdir().unwrap();
let init = cli(&temp)
.args(["config", "init", "--global", "--force"])
.output()
.unwrap();
assert!(
init.status.success(),
"stderr={}",
String::from_utf8_lossy(&init.stderr)
);
let set_google_cloud_project = cli(&temp)
.args([
"config",
"set",
"--global",
"providers.google-cloud.project",
"starweaver-project",
])
.output()
.unwrap();
assert!(
set_google_cloud_project.status.success(),
"stderr={}",
String::from_utf8_lossy(&set_google_cloud_project.stderr)
);
let get_google_cloud_project = cli(&temp)
.args(["config", "get", "providers.google-cloud.project"])
.output()
.unwrap();
assert!(get_google_cloud_project.status.success());
assert_eq!(
String::from_utf8(get_google_cloud_project.stdout).unwrap(),
"starweaver-project\n"
);
let set_google_alias = cli(&temp)
.args([
"config",
"set",
"--global",
"providers.google.base_url",
"https://gateway.example/google",
])
.output()
.unwrap();
assert!(
set_google_alias.status.success(),
"stderr={}",
String::from_utf8_lossy(&set_google_alias.stderr)
);
let get_google_alias = cli(&temp)
.args(["config", "get", "providers.gemini.base_url"])
.output()
.unwrap();
assert!(get_google_alias.status.success());
assert_eq!(
String::from_utf8(get_google_alias.stdout).unwrap(),
"https://gateway.example/google\n"
);
}
#[test]
fn cli_provider_missing_or_empty_key_does_not_create_session() {
let temp = tempfile::tempdir().unwrap();
let invalid_env = cli(&temp)
.args(["config", "set", "environment.provider", "remote-for-test"])
.output()
.unwrap();
assert!(!invalid_env.status.success());
fs::create_dir_all(temp.path().join(".starweaver")).unwrap();
fs::write(
temp.path().join(".starweaver/config.toml"),
r#"
[environment]
provider = "remote-for-test"
"#,
)
.unwrap();
let invalid_env_run = cli(&temp)
.args(["run", "hello", "--output", "silent"])
.output()
.unwrap();
assert!(!invalid_env_run.status.success());
assert!(
String::from_utf8_lossy(&invalid_env_run.stderr).contains("unknown environment provider")
);
fs::remove_file(temp.path().join(".starweaver/config.toml")).unwrap();
let missing_key = cli(&temp)
.args(["run", "hello", "--profile", "coding", "--output", "silent"])
.output()
.unwrap();
assert!(!missing_key.status.success());
assert!(String::from_utf8_lossy(&missing_key.stderr).contains("missing OPENAI_API_KEY"));
let list = cli(&temp).args(["session", "list"]).output().unwrap();
assert!(
list.status.success(),
"stderr={}",
String::from_utf8_lossy(&list.stderr)
);
assert!(String::from_utf8(list.stdout).unwrap().is_empty());
let empty_key = cli(&temp)
.env("OPENAI_API_KEY", " ")
.args(["run", "hello", "--profile", "coding", "--output", "silent"])
.output()
.unwrap();
assert!(!empty_key.status.success());
assert!(String::from_utf8_lossy(&empty_key.stderr).contains("missing OPENAI_API_KEY"));
let ready = cli(&temp)
.env("OPENAI_API_KEY", " ")
.args(["config", "get", "providers.openai.ready"])
.output()
.unwrap();
assert!(ready.status.success());
assert_eq!(String::from_utf8(ready.stdout).unwrap(), "false\n");
let invalid_hitl = cli(&temp)
.args(["config", "set", "general.default_hitl", "maybe"])
.output()
.unwrap();
assert!(!invalid_hitl.status.success());
let empty_env = cli(&temp)
.args(["config", "set", "providers.openai.api_key_env", " "])
.output()
.unwrap();
assert!(!empty_env.status.success());
}