use super::support::{PI_ENV_MUTEX, continue_session_with, resolved_for_repo};
use crate::commands::run::supervision::resume_continue_session;
use crate::contracts::Runner;
use crate::testsupport::runner::create_fake_runner;
use crate::testsupport::{INTERRUPT_TEST_MUTEX, reset_ctrlc_interrupt_flag};
use std::sync::{Mutex, MutexGuard};
use tempfile::TempDir;
fn interrupt_guard() -> MutexGuard<'static, ()> {
let interrupt_mutex = INTERRUPT_TEST_MUTEX.get_or_init(|| Mutex::new(()));
let guard = interrupt_mutex.lock().expect("interrupt mutex poisoned");
reset_ctrlc_interrupt_flag();
guard
}
#[test]
fn resume_continue_session_falls_back_to_fresh_invocation_without_session_id() -> anyhow::Result<()>
{
let _interrupt_guard = interrupt_guard();
let temp_dir = TempDir::new()?;
let args_path = temp_dir.path().join("runner-args.txt");
let runner_script = format!(
r#"#!/bin/sh
set -e
echo "$@" > "{args_path}"
echo '{{"type":"text","part":{{"text":"fresh"}}}}'
echo '{{"sessionID":"sess-fresh"}}'
"#,
args_path = args_path.display()
);
let runner_path = create_fake_runner(temp_dir.path(), "opencode", &runner_script)?;
let mut resolved = resolved_for_repo(temp_dir.path());
resolved.config.agent.opencode_bin = Some(runner_path.to_string_lossy().to_string());
let mut session = continue_session_with(
Runner::Opencode,
None,
crate::commands::run::PhaseType::Implementation,
);
let resumed = resume_continue_session(&resolved, &mut session, "hello", None)?;
let args = std::fs::read_to_string(&args_path)?;
assert!(
!args.split_whitespace().any(|arg| arg == "-s"),
"fresh invocation should not include resume session args, got: {args}"
);
assert_eq!(
resumed.decision.reason,
crate::session::ResumeReason::MissingRunnerSessionId
);
assert_eq!(session.session_id.as_deref(), Some("sess-fresh"));
Ok(())
}
#[test]
fn resume_continue_session_pi_falls_back_to_fresh_when_resume_lookup_fails() -> anyhow::Result<()> {
let _interrupt_guard = interrupt_guard();
let _env_guard = PI_ENV_MUTEX.lock().expect("pi env mutex poisoned");
let temp_dir = TempDir::new()?;
let args_path = temp_dir.path().join("pi-runner-args.txt");
let runner_script = format!(
r#"#!/bin/sh
set -e
echo "$@" > "{args_path}"
echo '{{"type":"result","result":"fresh"}}'
echo '{{"sessionID":"sess-pi-fresh"}}'
"#,
args_path = args_path.display()
);
let runner_path = create_fake_runner(temp_dir.path(), "pi", &runner_script)?;
let previous_pi_root = std::env::var_os("PI_CODING_AGENT_DIR");
let pi_root = temp_dir.path().join("pi-root");
std::fs::create_dir_all(&pi_root)?;
unsafe { std::env::set_var("PI_CODING_AGENT_DIR", &pi_root) };
let mut resolved = resolved_for_repo(temp_dir.path());
resolved.config.agent.pi_bin = Some(runner_path.to_string_lossy().to_string());
let mut session = continue_session_with(
Runner::Pi,
Some("missing-session-id"),
crate::commands::run::PhaseType::Implementation,
);
let result = resume_continue_session(&resolved, &mut session, "hello", None);
match previous_pi_root {
Some(value) => unsafe { std::env::set_var("PI_CODING_AGENT_DIR", value) },
None => unsafe { std::env::remove_var("PI_CODING_AGENT_DIR") },
}
let resumed = result?;
let args = std::fs::read_to_string(&args_path)?;
assert!(
!args.split_whitespace().any(|arg| arg == "--session"),
"fresh invocation should not include --session args, got: {args}"
);
assert_eq!(
resumed.decision.reason,
crate::session::ResumeReason::RunnerSessionInvalid
);
assert_eq!(session.session_id.as_deref(), Some("sess-pi-fresh"));
Ok(())
}
#[test]
fn resume_continue_session_gemini_falls_back_to_fresh_on_invalid_resume() -> anyhow::Result<()> {
let _interrupt_guard = interrupt_guard();
let temp_dir = TempDir::new()?;
let args_path = temp_dir.path().join("gemini-runner-args.txt");
let runner_script = format!(
r#"#!/bin/sh
set -e
if printf '%s' "$*" | grep -q -- '--resume'; then
echo 'Error resuming session: Invalid session identifier "does-not-exist".' >&2
echo ' Use --list-sessions to see available sessions.' >&2
exit 42
fi
echo "$*" > "{args_path}"
echo '{{"type":"message","role":"assistant","content":"fresh"}}'
echo '{{"session_id":"sess-gemini-fresh"}}'
"#,
args_path = args_path.display()
);
let runner_path = create_fake_runner(temp_dir.path(), "gemini", &runner_script)?;
let mut resolved = resolved_for_repo(temp_dir.path());
resolved.config.agent.gemini_bin = Some(runner_path.to_string_lossy().to_string());
let mut session = continue_session_with(
Runner::Gemini,
Some("does-not-exist"),
crate::commands::run::PhaseType::Implementation,
);
let resumed = resume_continue_session(&resolved, &mut session, "hello", None)?;
let args = std::fs::read_to_string(&args_path)?;
assert!(
!args.split_whitespace().any(|arg| arg == "--resume"),
"fresh invocation should not include --resume args, got: {args}"
);
assert_eq!(
resumed.decision.reason,
crate::session::ResumeReason::RunnerSessionInvalid
);
assert_eq!(session.session_id.as_deref(), Some("sess-gemini-fresh"));
Ok(())
}
#[test]
fn resume_continue_session_claude_falls_back_to_fresh_on_invalid_uuid() -> anyhow::Result<()> {
let _interrupt_guard = interrupt_guard();
let temp_dir = TempDir::new()?;
let args_path = temp_dir.path().join("claude-runner-args.txt");
let runner_script = format!(
r#"#!/bin/sh
set -e
if printf '%s' "$*" | grep -q -- '--resume'; then
echo '{{"type":"result","is_error":true,"errors":["--resume requires a valid session ID"]}}'
exit 1
fi
echo "$*" > "{args_path}"
echo '{{"type":"assistant","session_id":"sess-claude-fresh","message":{{"content":[{{"type":"text","text":"fresh"}}]}}}}'
"#,
args_path = args_path.display()
);
let runner_path = create_fake_runner(temp_dir.path(), "claude", &runner_script)?;
let mut resolved = resolved_for_repo(temp_dir.path());
resolved.config.agent.claude_bin = Some(runner_path.to_string_lossy().to_string());
let mut session = continue_session_with(
Runner::Claude,
Some("not-a-uuid"),
crate::commands::run::PhaseType::Implementation,
);
let resumed = resume_continue_session(&resolved, &mut session, "hello", None)?;
let args = std::fs::read_to_string(&args_path)?;
assert!(
!args.split_whitespace().any(|arg| arg == "--resume"),
"fresh invocation should not include --resume args, got: {args}"
);
assert_eq!(
resumed.decision.reason,
crate::session::ResumeReason::RunnerSessionInvalid
);
assert_eq!(session.session_id.as_deref(), Some("sess-claude-fresh"));
Ok(())
}
#[test]
fn resume_continue_session_opencode_falls_back_when_resume_errors_with_exit_zero()
-> anyhow::Result<()> {
let _interrupt_guard = interrupt_guard();
let temp_dir = TempDir::new()?;
let args_path = temp_dir.path().join("opencode-runner-args.txt");
let runner_script = format!(
r#"#!/bin/sh
set -e
for arg in "$@"; do
if [ "$arg" = "-s" ] || [ "$arg" = "--session" ]; then
echo 'ZodError: invalid_format sessionID' >&2
echo 'Invalid string: must start with "ses"' >&2
exit 0
fi
done
echo "$*" > "{args_path}"
echo '{{"type":"text","part":{{"text":"fresh"}}}}'
echo '{{"sessionID":"sess-opencode-fresh"}}'
"#,
args_path = args_path.display()
);
let runner_path = create_fake_runner(temp_dir.path(), "opencode", &runner_script)?;
let mut resolved = resolved_for_repo(temp_dir.path());
resolved.config.agent.opencode_bin = Some(runner_path.to_string_lossy().to_string());
let mut session = continue_session_with(
Runner::Opencode,
Some("bad-session"),
crate::commands::run::PhaseType::Implementation,
);
let resumed = resume_continue_session(&resolved, &mut session, "hello", None)?;
let args = std::fs::read_to_string(&args_path)?;
assert!(
!args.split_whitespace().any(|arg| arg == "-s"),
"fresh invocation should not include -s args, got: {args}"
);
assert_eq!(
resumed.decision.reason,
crate::session::ResumeReason::RunnerSessionInvalid
);
assert_eq!(session.session_id.as_deref(), Some("sess-opencode-fresh"));
Ok(())
}