a789495a98
FreeBSD Smoke / FreeBSD Smoke (x86_64) (push) Has been cancelled
CI / Quality Guardrails (push) Has been cancelled
CI / Build & Test (macos-latest) (push) Has been cancelled
CI / Build & Test (ubuntu-latest) (push) Has been cancelled
CI / Build & Test (windows-latest) (push) Has been cancelled
CI / Format (push) Has been cancelled
CI / PowerShell Syntax (push) Has been cancelled
CI / Windows Cross-Target Check (Linux) (push) Has been cancelled
309 lines
11 KiB
Rust
309 lines
11 KiB
Rust
use crate::test_support::*;
|
|
|
|
#[tokio::test]
|
|
async fn resume_session_restores_persisted_compaction_for_provider_context() -> Result<()> {
|
|
let _env = setup_test_env()?;
|
|
let runtime_dir = short_runtime_dir(format!(
|
|
"jcode-compaction-resume-test-{}",
|
|
std::time::SystemTime::now()
|
|
.duration_since(std::time::UNIX_EPOCH)
|
|
.unwrap()
|
|
.as_nanos()
|
|
));
|
|
std::fs::create_dir_all(&runtime_dir)?;
|
|
let socket_path = runtime_dir.join("jcode.sock");
|
|
let debug_socket_path = runtime_dir.join("jcode-debug.sock");
|
|
|
|
let provider = CapturingCompactionProvider::new();
|
|
let captured_messages = provider.captured_messages();
|
|
let provider: Arc<dyn Provider> = Arc::new(provider);
|
|
let server_instance =
|
|
server::Server::new_with_paths(provider, socket_path.clone(), debug_socket_path.clone());
|
|
let server_handle = tokio::spawn(async move { server_instance.run().await });
|
|
|
|
let result = async {
|
|
let mut session = Session::create_with_id(
|
|
"session_resume_compaction_restore_test".to_string(),
|
|
None,
|
|
Some("resume compaction restore test".to_string()),
|
|
);
|
|
session.add_message(
|
|
Role::User,
|
|
vec![ContentBlock::Text {
|
|
text: "older user turn".to_string(),
|
|
cache_control: None,
|
|
}],
|
|
);
|
|
session.add_message(
|
|
Role::Assistant,
|
|
vec![ContentBlock::Text {
|
|
text: "older assistant turn".to_string(),
|
|
cache_control: None,
|
|
}],
|
|
);
|
|
session.add_message(
|
|
Role::User,
|
|
vec![ContentBlock::Text {
|
|
text: "recent preserved turn".to_string(),
|
|
cache_control: None,
|
|
}],
|
|
);
|
|
session.compaction = Some(StoredCompactionState {
|
|
summary_text: "Worked on Gemini OAuth reload fixes.".to_string(),
|
|
openai_encrypted_content: None,
|
|
covers_up_to_turn: 2,
|
|
original_turn_count: 2,
|
|
compacted_count: 2,
|
|
});
|
|
session.save()?;
|
|
|
|
wait_for_server_ready(&socket_path, &debug_socket_path).await?;
|
|
let mut client = server::Client::connect_with_path(socket_path.clone()).await?;
|
|
|
|
let subscribe_id = client.subscribe().await?;
|
|
let _ = collect_until_done_unix(&mut client, subscribe_id).await?;
|
|
|
|
let resume_id = client.resume_session(&session.id).await?;
|
|
let _ = collect_until_history_unix(&mut client, resume_id).await?;
|
|
|
|
let message_id = client
|
|
.send_message("continue from the restored session")
|
|
.await?;
|
|
let mut seen_events = Vec::new();
|
|
let deadline = Instant::now() + Duration::from_secs(10);
|
|
while Instant::now() < deadline {
|
|
let event = timeout(Duration::from_secs(1), client.read_event()).await??;
|
|
let is_done = matches!(event, ServerEvent::Done { id } if id == message_id);
|
|
let is_error = matches!(event, ServerEvent::Error { id, .. } if id == message_id);
|
|
seen_events.push(format!("{event:?}"));
|
|
if is_done {
|
|
break;
|
|
}
|
|
if is_error {
|
|
anyhow::bail!(
|
|
"message request failed while validating compaction restore: {}",
|
|
seen_events.join(" | ")
|
|
);
|
|
}
|
|
}
|
|
|
|
let captured = captured_messages.lock().unwrap();
|
|
assert_eq!(
|
|
captured.len(),
|
|
1,
|
|
"expected exactly one provider completion call"
|
|
);
|
|
let provider_messages = &captured[0];
|
|
assert!(
|
|
provider_messages.len() >= 3,
|
|
"expected summary + preserved tail + new user message"
|
|
);
|
|
|
|
let summary_text = flatten_text_blocks(&provider_messages[0]);
|
|
assert!(summary_text.contains("Previous Conversation Summary"));
|
|
assert!(summary_text.contains("Gemini OAuth reload fixes"));
|
|
|
|
let joined = provider_messages
|
|
.iter()
|
|
.map(flatten_text_blocks)
|
|
.collect::<Vec<_>>()
|
|
.join("\n---\n");
|
|
assert!(joined.contains("recent preserved turn"));
|
|
assert!(joined.contains("continue from the restored session"));
|
|
assert!(!joined.contains("older user turn"));
|
|
assert!(!joined.contains("older assistant turn"));
|
|
|
|
Ok::<_, anyhow::Error>(())
|
|
}
|
|
.await;
|
|
|
|
abort_server_and_cleanup(&server_handle, &socket_path, &debug_socket_path);
|
|
result
|
|
}
|
|
|
|
/// Test that a simple text response works
|
|
#[tokio::test]
|
|
async fn test_simple_response() -> Result<()> {
|
|
let _env = setup_test_env()?;
|
|
let provider = MockProvider::new();
|
|
|
|
// Queue a simple response
|
|
provider.queue_response(vec![
|
|
StreamEvent::TextDelta("Hello! ".to_string()),
|
|
StreamEvent::TextDelta("How can I help?".to_string()),
|
|
StreamEvent::MessageEnd {
|
|
stop_reason: Some("end_turn".to_string()),
|
|
},
|
|
StreamEvent::SessionId("test-session-123".to_string()),
|
|
]);
|
|
|
|
let provider: Arc<dyn jcode::provider::Provider> = Arc::new(provider);
|
|
let registry = Registry::new(provider.clone()).await;
|
|
let mut agent = Agent::new(provider, registry);
|
|
|
|
let response = agent.run_once_capture("Say hello").await?;
|
|
let saved = Session::load(agent.session_id())?;
|
|
|
|
assert_eq!(response, "Hello! How can I help?");
|
|
assert!(saved.is_debug, "test sessions should be marked debug");
|
|
Ok(())
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_agent_clear_preserves_debug_flag() -> Result<()> {
|
|
let _env = setup_test_env()?;
|
|
let provider = MockProvider::new();
|
|
let provider: Arc<dyn jcode::provider::Provider> = Arc::new(provider);
|
|
let registry = Registry::new(provider.clone()).await;
|
|
let mut agent = Agent::new(provider, registry);
|
|
agent.set_debug(true);
|
|
let old_session_id = agent.session_id().to_string();
|
|
|
|
agent.clear();
|
|
|
|
assert_ne!(agent.session_id(), old_session_id);
|
|
assert!(agent.is_debug());
|
|
Ok(())
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_debug_create_session_marks_debug() -> Result<()> {
|
|
let _env = setup_test_env()?;
|
|
let runtime_dir = short_runtime_dir(format!(
|
|
"jcode-debug-test-{}",
|
|
std::time::SystemTime::now()
|
|
.duration_since(std::time::UNIX_EPOCH)
|
|
.unwrap()
|
|
.as_nanos()
|
|
));
|
|
std::fs::create_dir_all(&runtime_dir)?;
|
|
let socket_path = runtime_dir.join("jcode.sock");
|
|
let debug_socket_path = runtime_dir.join("jcode-debug.sock");
|
|
|
|
let provider = MockProvider::new();
|
|
let provider: Arc<dyn jcode::provider::Provider> = Arc::new(provider);
|
|
let server_instance =
|
|
server::Server::new_with_paths(provider, socket_path.clone(), debug_socket_path.clone());
|
|
let server_handle = tokio::spawn(async move { server_instance.run().await });
|
|
|
|
wait_for_server_ready(&socket_path, &debug_socket_path).await?;
|
|
|
|
let session_id = debug_create_headless_session(debug_socket_path.clone()).await?;
|
|
let session = Session::load(&session_id)?;
|
|
assert!(session.is_debug);
|
|
|
|
abort_server_and_cleanup(&server_handle, &socket_path, &debug_socket_path);
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_debug_create_selfdev_session_marks_canary() -> Result<()> {
|
|
let _env = setup_test_env()?;
|
|
let runtime_dir = short_runtime_dir(format!(
|
|
"jcode-debug-selfdev-test-{}",
|
|
std::time::SystemTime::now()
|
|
.duration_since(std::time::UNIX_EPOCH)
|
|
.unwrap()
|
|
.as_nanos()
|
|
));
|
|
std::fs::create_dir_all(&runtime_dir)?;
|
|
let socket_path = runtime_dir.join("jcode.sock");
|
|
let debug_socket_path = runtime_dir.join("jcode-debug.sock");
|
|
|
|
let provider = MockProvider::new();
|
|
let provider: Arc<dyn jcode::provider::Provider> = Arc::new(provider);
|
|
let server_instance =
|
|
server::Server::new_with_paths(provider, socket_path.clone(), debug_socket_path.clone());
|
|
let server_handle = tokio::spawn(async move { server_instance.run().await });
|
|
|
|
wait_for_server_ready(&socket_path, &debug_socket_path).await?;
|
|
|
|
let session_id = debug_create_headless_session_with_command(
|
|
debug_socket_path.clone(),
|
|
"create_session:selfdev:/tmp",
|
|
)
|
|
.await?;
|
|
let session = Session::load(&session_id)?;
|
|
assert!(session.is_debug);
|
|
assert!(session.is_canary);
|
|
|
|
abort_server_and_cleanup(&server_handle, &socket_path, &debug_socket_path);
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_clear_preserves_debug_for_resumed_debug_session() -> Result<()> {
|
|
let _env = setup_test_env()?;
|
|
let runtime_dir = short_runtime_dir(format!(
|
|
"jcode-clear-debug-test-{}",
|
|
std::time::SystemTime::now()
|
|
.duration_since(std::time::UNIX_EPOCH)
|
|
.unwrap()
|
|
.as_nanos()
|
|
));
|
|
std::fs::create_dir_all(&runtime_dir)?;
|
|
let socket_path = runtime_dir.join("jcode.sock");
|
|
let debug_socket_path = runtime_dir.join("jcode-debug.sock");
|
|
|
|
let provider = MockProvider::new();
|
|
let provider: Arc<dyn jcode::provider::Provider> = Arc::new(provider);
|
|
let server_instance =
|
|
server::Server::new_with_paths(provider, socket_path.clone(), debug_socket_path.clone());
|
|
let server_handle = tokio::spawn(async move { server_instance.run().await });
|
|
|
|
wait_for_server_ready(&socket_path, &debug_socket_path).await?;
|
|
|
|
let debug_session_id = debug_create_headless_session(debug_socket_path.clone()).await?;
|
|
let mut client = server::Client::connect_with_path(socket_path.clone()).await?;
|
|
let resume_id = client.resume_session(&debug_session_id).await?;
|
|
|
|
// Drain resume completion so clear() events are unambiguous.
|
|
let mut saw_resume_history = false;
|
|
let resume_deadline = Instant::now() + Duration::from_secs(5);
|
|
while Instant::now() < resume_deadline {
|
|
let event = tokio::time::timeout(Duration::from_secs(1), client.read_event()).await??;
|
|
match event {
|
|
ServerEvent::Ack { .. } => continue,
|
|
ServerEvent::History { id, .. } if id == resume_id => {
|
|
saw_resume_history = true;
|
|
break;
|
|
}
|
|
ServerEvent::Error { id, message, .. } if id == resume_id => {
|
|
anyhow::bail!("resume_session failed: {}", message);
|
|
}
|
|
_ => {}
|
|
}
|
|
}
|
|
if !saw_resume_history {
|
|
anyhow::bail!("Timed out waiting for resume history event");
|
|
}
|
|
|
|
client.clear().await?;
|
|
|
|
let mut new_session_id = None;
|
|
let clear_deadline = Instant::now() + Duration::from_secs(5);
|
|
while Instant::now() < clear_deadline {
|
|
let event = tokio::time::timeout(Duration::from_secs(1), client.read_event()).await??;
|
|
match event {
|
|
ServerEvent::Ack { .. } => continue,
|
|
ServerEvent::SessionId { session_id } => {
|
|
new_session_id = Some(session_id);
|
|
}
|
|
ServerEvent::Done { .. } if new_session_id.is_some() => break,
|
|
_ => {}
|
|
}
|
|
}
|
|
|
|
let new_session_id = new_session_id
|
|
.ok_or_else(|| anyhow::anyhow!("Did not receive new session id after clear"))?;
|
|
assert_ne!(new_session_id, debug_session_id);
|
|
let session = Session::load(&new_session_id)?;
|
|
assert!(session.is_debug);
|
|
|
|
abort_server_and_cleanup(&server_handle, &socket_path, &debug_socket_path);
|
|
|
|
Ok(())
|
|
}
|