#![cfg(feature = "integration-tests")]
use muse_codes::{ExecRun, MuseExecBuilder, MusePayload, Provider};
#[tokio::test]
async fn echo_run_streams_to_terminal() {
let run = ExecRun::spawn(
&MuseExecBuilder::new("muse-codes integration probe")
.provider(Provider::Echo)
.working_directory(std::env::temp_dir()),
)
.await
.expect("muse installed and spawnable");
let mut saw_prompt = false;
let mut saw_delta = false;
let mut records = 0usize;
let terminal = run
.wait_terminal(|record| {
records += 1;
match record.typed_payload().expect("every payload types") {
MusePayload::TurnInputUser(t) => {
saw_prompt = t.prompt.contains("integration probe");
}
MusePayload::RunOutputDelta(d) => {
saw_delta = saw_delta || !d.text.is_empty();
}
_ => {}
}
})
.await
.expect("run reaches terminal");
assert_eq!(terminal.terminal, "completed");
assert!(saw_prompt, "turn.input.user should carry the prompt");
assert!(saw_delta, "run.output.delta should stream text");
assert!(
records >= 20,
"expected a full journal, saw {records} records"
);
}
#[tokio::test]
async fn device_login_yields_url_and_code_live() {
use muse_codes::auth::DeviceLoginFlow;
use std::time::Duration;
let mut flow = DeviceLoginFlow::start().await.expect("muse spawns");
let dc = flow
.device_code(Duration::from_secs(20))
.await
.expect("device code appears");
assert!(dc.verification_url.starts_with("https://"));
assert!(dc.code.contains('-'), "code shape: {}", dc.code);
assert!(
dc.verification_url.contains(&dc.code),
"URL should embed the code"
);
flow.cancel().await.expect("cancel");
}
#[tokio::test]
async fn auth_set_and_logout_roundtrip_in_sandbox_home() {
let sandbox = std::env::temp_dir().join(format!("muse-auth-sandbox-{}", std::process::id()));
std::fs::create_dir_all(&sandbox).unwrap();
let orig_home = std::env::var_os("HOME");
let muse = which::which("muse").expect("muse on PATH");
let child = tokio::process::Command::new(&muse)
.args(["auth", "set", "--provider", "meta", "--api-key-stdin"])
.env("HOME", &sandbox)
.env_remove("XDG_CONFIG_HOME")
.stdin(std::process::Stdio::piped())
.output_stdin(b"meta-dummy-key-not-real\n")
.await;
drop(orig_home);
let out = child.expect("auth set runs");
assert!(
out.status.success(),
"auth set failed: {}",
String::from_utf8_lossy(&out.stderr)
);
let auth_file = sandbox.join(".config/muse/auth.json");
assert!(auth_file.exists(), "credential file should appear");
let saved: muse_codes::auth::AuthFile =
serde_json::from_str(&std::fs::read_to_string(&auth_file).unwrap()).unwrap();
assert_eq!(
saved.providers["meta"].api_key.as_deref(),
Some("meta-dummy-key-not-real")
);
let out = tokio::process::Command::new(&muse)
.arg("logout")
.env("HOME", &sandbox)
.env_remove("XDG_CONFIG_HOME")
.output()
.await
.expect("logout runs");
assert!(out.status.success());
let post: serde_json::Value =
serde_json::from_str(&std::fs::read_to_string(&auth_file).unwrap()).unwrap();
assert_eq!(post["providers"], serde_json::json!({}));
std::fs::remove_dir_all(&sandbox).ok();
}
trait StdinExt {
async fn output_stdin(&mut self, input: &[u8]) -> std::io::Result<std::process::Output>;
}
impl StdinExt for tokio::process::Command {
async fn output_stdin(&mut self, input: &[u8]) -> std::io::Result<std::process::Output> {
use tokio::io::AsyncWriteExt;
let mut child = self
.stdout(std::process::Stdio::piped())
.stderr(std::process::Stdio::piped())
.spawn()?;
if let Some(mut stdin) = child.stdin.take() {
stdin.write_all(input).await?;
}
child.wait_with_output().await
}
}
#[tokio::test]
async fn session_id_continuity_and_sequence_reuse() {
use muse_codes::{ExecRun, MuseExecBuilder, Provider};
use std::time::Duration;
let session = uuid_v4_like();
let mut streams = Vec::new();
let mut first_seqs = Vec::new();
let mut ids = Vec::new();
for turn in ["first turn", "second turn"] {
let run = ExecRun::spawn(
&MuseExecBuilder::new(turn)
.provider(Provider::Echo)
.session_id(&session)
.working_directory(std::env::temp_dir()),
)
.await
.expect("spawn");
let mut seqs = Vec::new();
let mut turn_ids = Vec::new();
let terminal = run
.wait_terminal(|r| {
seqs.push(r.sequence);
turn_ids.push(r.id.clone());
streams.push(r.stream.id.clone());
})
.await
.expect("terminal");
assert_eq!(terminal.terminal, "completed");
first_seqs.push(*seqs.first().expect("records"));
ids.push(turn_ids);
}
assert!(
streams.iter().all(|s| *s == session),
"the caller-supplied session id must be adopted verbatim as stream.id"
);
assert!(
first_seqs[1] <= 3,
"sequence restarts per turn (got {first_seqs:?}); never key across turns on it"
);
let within_session_overlap = ids[0].iter().filter(|i| ids[1].contains(i)).count();
assert_eq!(
within_session_overlap, 0,
"record ids should not repeat across turns of the SAME session"
);
let _ = Duration::from_secs(0);
}
#[tokio::test]
async fn record_ids_repeat_across_sessions() {
use muse_codes::{ExecRun, MuseExecBuilder, Provider};
let mut runs: Vec<Vec<String>> = Vec::new();
for _ in 0..2 {
let run = ExecRun::spawn(
&MuseExecBuilder::new("same prompt, different session")
.provider(Provider::Echo)
.session_id(uuid_v4_like())
.working_directory(std::env::temp_dir()),
)
.await
.expect("spawn");
let mut ids = Vec::new();
run.wait_terminal(|r| ids.push(r.id.clone()))
.await
.expect("terminal");
runs.push(ids);
}
let overlap = runs[0].iter().filter(|i| runs[1].contains(i)).count();
assert!(
overlap > 0,
"record ids are expected to REPEAT across sessions (UUID-shaped counters). \
If this ever fails they may have become globally unique — re-check before \
relaxing any (stream_id, id) composite key."
);
assert!(
runs[0][0].starts_with("018f0000-"),
"the counter shape is load-bearing context for the composite-key rule: {}",
runs[0][0]
);
}
#[tokio::test]
async fn kill_midflight_then_resume_same_session() {
use muse_codes::{ExecRun, MuseExecBuilder, Provider};
use std::time::Duration;
let session = uuid_v4_like();
let mut victim = ExecRun::spawn(
&MuseExecBuilder::new("this run gets killed")
.provider(Provider::Echo)
.session_id(&session)
.working_directory(std::env::temp_dir()),
)
.await
.expect("spawn victim");
tokio::time::sleep(Duration::from_millis(60)).await;
victim.kill().await.expect("kill");
drop(victim);
let run = ExecRun::spawn(
&MuseExecBuilder::new("this run must still work")
.provider(Provider::Echo)
.session_id(&session)
.working_directory(std::env::temp_dir()),
)
.await
.expect("spawn after kill");
let terminal = run
.wait_terminal(|_| {})
.await
.expect("session store survives a mid-flight kill");
assert_eq!(terminal.terminal, "completed");
}
fn uuid_v4_like() -> String {
use std::time::{SystemTime, UNIX_EPOCH};
let n = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_nanos();
format!(
"{:08x}-{:04x}-4{:03x}-8{:03x}-{:012x}",
(n & 0xffff_ffff) as u32,
((n >> 32) & 0xffff) as u16,
((n >> 48) & 0xfff) as u16,
((n >> 60) & 0xfff) as u16,
(n & 0xffff_ffff_ffff) as u64
)
}