|
|
|
|
@ -783,6 +783,9 @@ mod tests {
|
|
|
|
|
|
|
|
|
|
use super::*;
|
|
|
|
|
|
|
|
|
|
#[cfg(unix)]
|
|
|
|
|
const LIVE_EVENT_TIMEOUT: Duration = Duration::from_secs(5);
|
|
|
|
|
|
|
|
|
|
fn without_progress(events: Vec<ReplyEvent>) -> Vec<ReplyEvent> {
|
|
|
|
|
events
|
|
|
|
|
.into_iter()
|
|
|
|
|
@ -1034,18 +1037,56 @@ mod tests {
|
|
|
|
|
|
|
|
|
|
#[cfg(unix)]
|
|
|
|
|
fn write_executable_script(path: &Path, body: impl AsRef<str>) {
|
|
|
|
|
fs::write(path, body.as_ref()).unwrap();
|
|
|
|
|
let tmp = path.with_extension(format!("tmp-{}", uuid::Uuid::new_v4()));
|
|
|
|
|
{
|
|
|
|
|
use std::io::Write as _;
|
|
|
|
|
|
|
|
|
|
let mut file = fs::File::create(&tmp).unwrap();
|
|
|
|
|
file.write_all(body.as_ref().as_bytes()).unwrap();
|
|
|
|
|
file.sync_all().unwrap();
|
|
|
|
|
}
|
|
|
|
|
fs::rename(&tmp, path).unwrap();
|
|
|
|
|
let mut permissions = fs::metadata(path).unwrap().permissions();
|
|
|
|
|
permissions.set_mode(0o755);
|
|
|
|
|
fs::set_permissions(path, permissions).unwrap();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[cfg(unix)]
|
|
|
|
|
fn shell_wait_for_file(path: &Path) -> String {
|
|
|
|
|
format!("while [ ! -f '{}' ]; do sleep 0.05; done", path.display())
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[cfg(unix)]
|
|
|
|
|
fn shell_script_command(path: &Path) -> (&'static str, Vec<String>) {
|
|
|
|
|
("sh", vec![path.to_string_lossy().into_owned()])
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[cfg(unix)]
|
|
|
|
|
fn spawn_send_with_tap(
|
|
|
|
|
session: Arc<OpenCodeSession>,
|
|
|
|
|
prompt: &'static str,
|
|
|
|
|
tx: std::sync::mpsc::Sender<ReplyEvent>,
|
|
|
|
|
) -> std::thread::JoinHandle<Result<Vec<ReplyEvent>, AgentSessionError>> {
|
|
|
|
|
std::thread::spawn(move || {
|
|
|
|
|
tokio::runtime::Builder::new_current_thread()
|
|
|
|
|
.enable_all()
|
|
|
|
|
.build()
|
|
|
|
|
.unwrap()
|
|
|
|
|
.block_on(async move {
|
|
|
|
|
session
|
|
|
|
|
.send_with_tap(prompt, tx)
|
|
|
|
|
.await
|
|
|
|
|
.map(|stream| stream.collect::<Vec<_>>())
|
|
|
|
|
})
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[cfg(unix)]
|
|
|
|
|
#[tokio::test]
|
|
|
|
|
async fn send_prefers_parseable_stdout_over_nonzero_exit_status() {
|
|
|
|
|
let tmp = TempDir::new("exit-one");
|
|
|
|
|
let script = tmp.path().join("opencode-fixture.sh");
|
|
|
|
|
fs::write(
|
|
|
|
|
write_executable_script(
|
|
|
|
|
&script,
|
|
|
|
|
r#"#!/bin/sh
|
|
|
|
|
printf '%s\n' '{"type":"step_start"}'
|
|
|
|
|
@ -1054,15 +1095,12 @@ printf '%s\n' '{"type":"step_finish"}'
|
|
|
|
|
printf '%s\n' 'stderr générique' >&2
|
|
|
|
|
exit 1
|
|
|
|
|
"#,
|
|
|
|
|
)
|
|
|
|
|
.unwrap();
|
|
|
|
|
let mut permissions = fs::metadata(&script).unwrap().permissions();
|
|
|
|
|
permissions.set_mode(0o755);
|
|
|
|
|
fs::set_permissions(&script, permissions).unwrap();
|
|
|
|
|
);
|
|
|
|
|
let (command, args) = shell_script_command(&script);
|
|
|
|
|
let session = OpenCodeSession::new(
|
|
|
|
|
SessionId::new_random(),
|
|
|
|
|
script.to_string_lossy(),
|
|
|
|
|
Vec::new(),
|
|
|
|
|
command,
|
|
|
|
|
args,
|
|
|
|
|
tmp.path().to_string_lossy(),
|
|
|
|
|
None,
|
|
|
|
|
Vec::new(),
|
|
|
|
|
@ -1089,25 +1127,28 @@ exit 1
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[cfg(unix)]
|
|
|
|
|
#[tokio::test]
|
|
|
|
|
async fn send_with_tap_emits_progress_live_before_final_and_tags_idea_tool_as_mcp() {
|
|
|
|
|
#[test]
|
|
|
|
|
fn send_with_tap_emits_progress_live_before_final_and_tags_idea_tool_as_mcp() {
|
|
|
|
|
let tmp = TempDir::new("tap-live");
|
|
|
|
|
let script = tmp.path().join("opencode-fixture.sh");
|
|
|
|
|
let release = tmp.path().join("release");
|
|
|
|
|
write_executable_script(
|
|
|
|
|
&script,
|
|
|
|
|
r#"#!/bin/sh
|
|
|
|
|
printf '%s\n' '{"type":"step_start","sessionID":"fresh-engine","part":{"messageID":"m1","type":"step-start"}}'
|
|
|
|
|
sleep 1
|
|
|
|
|
{{WAIT_FOR_RELEASE}}
|
|
|
|
|
printf '%s\n' '{"type":"tool_use","sessionID":"fresh-engine","part":{"messageID":"m1","type":"tool","tool":"idea_ask_agent"}}'
|
|
|
|
|
printf '%s\n' '{"type":"text","sessionID":"fresh-engine","part":{"messageID":"m1","type":"text","text":"réponse live"}}'
|
|
|
|
|
printf '%s\n' '{"type":"step_finish","sessionID":"fresh-engine","part":{"messageID":"m1","type":"step-finish"}}'
|
|
|
|
|
"#,
|
|
|
|
|
"#
|
|
|
|
|
.replace("{{WAIT_FOR_RELEASE}}", &shell_wait_for_file(&release)),
|
|
|
|
|
);
|
|
|
|
|
let (command, args) = shell_script_command(&script);
|
|
|
|
|
let session = Arc::new(
|
|
|
|
|
OpenCodeSession::new(
|
|
|
|
|
SessionId::new_random(),
|
|
|
|
|
script.to_string_lossy(),
|
|
|
|
|
Vec::new(),
|
|
|
|
|
command,
|
|
|
|
|
args,
|
|
|
|
|
tmp.path().to_string_lossy(),
|
|
|
|
|
None,
|
|
|
|
|
Vec::new(),
|
|
|
|
|
@ -1117,24 +1158,12 @@ printf '%s\n' '{"type":"step_finish","sessionID":"fresh-engine","part":{"message
|
|
|
|
|
.unwrap(),
|
|
|
|
|
);
|
|
|
|
|
let (tx, rx) = std::sync::mpsc::channel();
|
|
|
|
|
let worker = {
|
|
|
|
|
let session = Arc::clone(&session);
|
|
|
|
|
tokio::spawn(async move {
|
|
|
|
|
session
|
|
|
|
|
.send_with_tap("prompt", tx)
|
|
|
|
|
.await
|
|
|
|
|
.map(|stream| stream.collect::<Vec<_>>())
|
|
|
|
|
})
|
|
|
|
|
};
|
|
|
|
|
let worker = spawn_send_with_tap(Arc::clone(&session), "prompt", tx);
|
|
|
|
|
|
|
|
|
|
let (first_live, rx) = tokio::task::spawn_blocking(move || {
|
|
|
|
|
let first_live = rx
|
|
|
|
|
.recv_timeout(Duration::from_millis(500))
|
|
|
|
|
.recv_timeout(LIVE_EVENT_TIMEOUT)
|
|
|
|
|
.expect("un événement de progress doit atteindre le tap avant le Final");
|
|
|
|
|
(first_live, rx)
|
|
|
|
|
})
|
|
|
|
|
.await
|
|
|
|
|
.unwrap();
|
|
|
|
|
fs::write(&release, "").unwrap();
|
|
|
|
|
assert!(
|
|
|
|
|
matches!(
|
|
|
|
|
first_live,
|
|
|
|
|
@ -1144,7 +1173,7 @@ printf '%s\n' '{"type":"step_finish","sessionID":"fresh-engine","part":{"message
|
|
|
|
|
"le premier événement live doit être le step_start provider: {first_live:?}"
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
let returned = worker.await.unwrap().unwrap();
|
|
|
|
|
let returned = worker.join().unwrap().unwrap();
|
|
|
|
|
let live: Vec<_> = std::iter::once(first_live).chain(rx.into_iter()).collect();
|
|
|
|
|
|
|
|
|
|
assert!(
|
|
|
|
|
@ -1187,10 +1216,11 @@ printf '%s\n' '{"type":"text","sessionID":"seeded-engine","part":{"messageID":"n
|
|
|
|
|
printf '%s\n' '{"type":"step_finish","sessionID":"seeded-engine","part":{"messageID":"new","type":"step-finish"}}'
|
|
|
|
|
"#,
|
|
|
|
|
);
|
|
|
|
|
let (command, args) = shell_script_command(&script);
|
|
|
|
|
let session = OpenCodeSession::new(
|
|
|
|
|
SessionId::new_random(),
|
|
|
|
|
script.to_string_lossy(),
|
|
|
|
|
Vec::new(),
|
|
|
|
|
command,
|
|
|
|
|
args,
|
|
|
|
|
tmp.path().to_string_lossy(),
|
|
|
|
|
Some("seeded-engine".to_owned()),
|
|
|
|
|
Vec::new(),
|
|
|
|
|
@ -1230,25 +1260,28 @@ printf '%s\n' '{"type":"step_finish","sessionID":"seeded-engine","part":{"messag
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[cfg(unix)]
|
|
|
|
|
#[tokio::test]
|
|
|
|
|
async fn send_with_tap_streams_progress_for_seeded_session_with_single_current_message_id() {
|
|
|
|
|
#[test]
|
|
|
|
|
fn send_with_tap_streams_progress_for_seeded_session_with_single_current_message_id() {
|
|
|
|
|
let tmp = TempDir::new("tap-seeded-single-message");
|
|
|
|
|
let script = tmp.path().join("opencode-fixture.sh");
|
|
|
|
|
let release = tmp.path().join("release");
|
|
|
|
|
write_executable_script(
|
|
|
|
|
&script,
|
|
|
|
|
r#"#!/bin/sh
|
|
|
|
|
printf '%s\n' '{"type":"step_start","sessionID":"seeded-engine","part":{"messageID":"current","type":"step-start"}}'
|
|
|
|
|
sleep 1
|
|
|
|
|
{{WAIT_FOR_RELEASE}}
|
|
|
|
|
printf '%s\n' '{"type":"tool_use","sessionID":"seeded-engine","part":{"messageID":"current","type":"tool","tool":"idea_ask_agent"}}'
|
|
|
|
|
printf '%s\n' '{"type":"text","sessionID":"seeded-engine","part":{"messageID":"current","type":"text","text":"réponse courante"}}'
|
|
|
|
|
printf '%s\n' '{"type":"step_finish","sessionID":"seeded-engine","part":{"messageID":"current","type":"step-finish"}}'
|
|
|
|
|
"#,
|
|
|
|
|
"#
|
|
|
|
|
.replace("{{WAIT_FOR_RELEASE}}", &shell_wait_for_file(&release)),
|
|
|
|
|
);
|
|
|
|
|
let (command, args) = shell_script_command(&script);
|
|
|
|
|
let session = Arc::new(
|
|
|
|
|
OpenCodeSession::new(
|
|
|
|
|
SessionId::new_random(),
|
|
|
|
|
script.to_string_lossy(),
|
|
|
|
|
Vec::new(),
|
|
|
|
|
command,
|
|
|
|
|
args,
|
|
|
|
|
tmp.path().to_string_lossy(),
|
|
|
|
|
Some("seeded-engine".to_owned()),
|
|
|
|
|
Vec::new(),
|
|
|
|
|
@ -1258,24 +1291,12 @@ printf '%s\n' '{"type":"step_finish","sessionID":"seeded-engine","part":{"messag
|
|
|
|
|
.unwrap(),
|
|
|
|
|
);
|
|
|
|
|
let (tx, rx) = std::sync::mpsc::channel();
|
|
|
|
|
let worker = {
|
|
|
|
|
let session = Arc::clone(&session);
|
|
|
|
|
tokio::spawn(async move {
|
|
|
|
|
session
|
|
|
|
|
.send_with_tap("prompt", tx)
|
|
|
|
|
.await
|
|
|
|
|
.map(|stream| stream.collect::<Vec<_>>())
|
|
|
|
|
})
|
|
|
|
|
};
|
|
|
|
|
let worker = spawn_send_with_tap(Arc::clone(&session), "prompt", tx);
|
|
|
|
|
|
|
|
|
|
let (first_live, rx) = tokio::task::spawn_blocking(move || {
|
|
|
|
|
let first_live = rx
|
|
|
|
|
.recv_timeout(Duration::from_millis(500))
|
|
|
|
|
.recv_timeout(LIVE_EVENT_TIMEOUT)
|
|
|
|
|
.expect("la session existante doit streamer le progress du tour courant");
|
|
|
|
|
(first_live, rx)
|
|
|
|
|
})
|
|
|
|
|
.await
|
|
|
|
|
.unwrap();
|
|
|
|
|
fs::write(&release, "").unwrap();
|
|
|
|
|
assert!(
|
|
|
|
|
matches!(
|
|
|
|
|
first_live,
|
|
|
|
|
@ -1285,7 +1306,7 @@ printf '%s\n' '{"type":"step_finish","sessionID":"seeded-engine","part":{"messag
|
|
|
|
|
"le premier événement live doit être le step_start du message courant: {first_live:?}"
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
let returned = worker.await.unwrap().unwrap();
|
|
|
|
|
let returned = worker.join().unwrap().unwrap();
|
|
|
|
|
let live: Vec<_> = std::iter::once(first_live).chain(rx.into_iter()).collect();
|
|
|
|
|
|
|
|
|
|
assert!(
|
|
|
|
|
@ -1332,22 +1353,19 @@ printf '%s\n' '{"type":"step_finish","sessionID":"seeded-engine","part":{"messag
|
|
|
|
|
async fn send_captures_engine_session_id_from_jsonl() {
|
|
|
|
|
let tmp = TempDir::new("capture-session");
|
|
|
|
|
let script = tmp.path().join("opencode-fixture.sh");
|
|
|
|
|
fs::write(
|
|
|
|
|
write_executable_script(
|
|
|
|
|
&script,
|
|
|
|
|
r#"#!/bin/sh
|
|
|
|
|
printf '%s\n' '{"type":"step_start","sessionID":"opencode-engine-1"}'
|
|
|
|
|
printf '%s\n' '{"type":"text","sessionID":"opencode-engine-1","part":{"text":"ok"}}'
|
|
|
|
|
printf '%s\n' '{"type":"step_finish","sessionID":"opencode-engine-1"}'
|
|
|
|
|
"#,
|
|
|
|
|
)
|
|
|
|
|
.unwrap();
|
|
|
|
|
let mut permissions = fs::metadata(&script).unwrap().permissions();
|
|
|
|
|
permissions.set_mode(0o755);
|
|
|
|
|
fs::set_permissions(&script, permissions).unwrap();
|
|
|
|
|
);
|
|
|
|
|
let (command, args) = shell_script_command(&script);
|
|
|
|
|
let session = OpenCodeSession::new(
|
|
|
|
|
SessionId::new_random(),
|
|
|
|
|
script.to_string_lossy(),
|
|
|
|
|
Vec::new(),
|
|
|
|
|
command,
|
|
|
|
|
args,
|
|
|
|
|
tmp.path().to_string_lossy(),
|
|
|
|
|
None,
|
|
|
|
|
Vec::new(),
|
|
|
|
|
@ -1372,7 +1390,7 @@ printf '%s\n' '{"type":"step_finish","sessionID":"opencode-engine-1"}'
|
|
|
|
|
let tmp = TempDir::new("resume-arg");
|
|
|
|
|
let script = tmp.path().join("opencode-fixture.sh");
|
|
|
|
|
let args_file = tmp.path().join("args.txt");
|
|
|
|
|
fs::write(
|
|
|
|
|
write_executable_script(
|
|
|
|
|
&script,
|
|
|
|
|
format!(
|
|
|
|
|
r#"#!/bin/sh
|
|
|
|
|
@ -1383,15 +1401,12 @@ printf '%s\n' '{{"type":"step_finish","sessionID":"seeded-engine"}}'
|
|
|
|
|
"#,
|
|
|
|
|
args_file.display()
|
|
|
|
|
),
|
|
|
|
|
)
|
|
|
|
|
.unwrap();
|
|
|
|
|
let mut permissions = fs::metadata(&script).unwrap().permissions();
|
|
|
|
|
permissions.set_mode(0o755);
|
|
|
|
|
fs::set_permissions(&script, permissions).unwrap();
|
|
|
|
|
);
|
|
|
|
|
let (command, args) = shell_script_command(&script);
|
|
|
|
|
let session = OpenCodeSession::new(
|
|
|
|
|
SessionId::new_random(),
|
|
|
|
|
script.to_string_lossy(),
|
|
|
|
|
Vec::new(),
|
|
|
|
|
command,
|
|
|
|
|
args,
|
|
|
|
|
tmp.path().to_string_lossy(),
|
|
|
|
|
Some("seeded-engine".to_owned()),
|
|
|
|
|
Vec::new(),
|
|
|
|
|
@ -1417,7 +1432,7 @@ printf '%s\n' '{{"type":"step_finish","sessionID":"seeded-engine"}}'
|
|
|
|
|
async fn seeded_session_ignores_and_does_not_capture_other_session_events() {
|
|
|
|
|
let tmp = TempDir::new("ignore-other-session");
|
|
|
|
|
let script = tmp.path().join("opencode-fixture.sh");
|
|
|
|
|
fs::write(
|
|
|
|
|
write_executable_script(
|
|
|
|
|
&script,
|
|
|
|
|
r#"#!/bin/sh
|
|
|
|
|
printf '%s\n' '{"type":"step_start","sessionID":"seeded-engine","part":{"messageID":"m1"}}'
|
|
|
|
|
@ -1425,15 +1440,12 @@ printf '%s\n' '{"type":"text","sessionID":"seeded-engine","part":{"messageID":"m
|
|
|
|
|
printf '%s\n' '{"type":"step_finish","sessionID":"seeded-engine","part":{"messageID":"m1"}}'
|
|
|
|
|
printf '%s\n' '{"type":"text","sessionID":"other-engine","part":{"messageID":"m2","text":"pollution"}}'
|
|
|
|
|
"#,
|
|
|
|
|
)
|
|
|
|
|
.unwrap();
|
|
|
|
|
let mut permissions = fs::metadata(&script).unwrap().permissions();
|
|
|
|
|
permissions.set_mode(0o755);
|
|
|
|
|
fs::set_permissions(&script, permissions).unwrap();
|
|
|
|
|
);
|
|
|
|
|
let (command, args) = shell_script_command(&script);
|
|
|
|
|
let session = OpenCodeSession::new(
|
|
|
|
|
SessionId::new_random(),
|
|
|
|
|
script.to_string_lossy(),
|
|
|
|
|
Vec::new(),
|
|
|
|
|
command,
|
|
|
|
|
args,
|
|
|
|
|
tmp.path().to_string_lossy(),
|
|
|
|
|
Some("seeded-engine".to_owned()),
|
|
|
|
|
Vec::new(),
|
|
|
|
|
|