feat(inter-agent): backend des annonces live d'agent (B0-B3)
Redémarre le ticket #4 sur la base develop. Émission d'annonces live d'un agent vers l'UI, distinctes du Final de délégation : - domain: ReplyEvent::Announcement/Final et DomainEvent::AgentAnnouncement (events.rs), port d'émission (ports.rs), gating de readiness (readiness.rs). - application: mapping des événements structurés en annonces (agent/structured.rs, agent/mod.rs, lib.rs) et relais côté orchestrateur (orchestrator/service.rs). - infrastructure/session: parse des annonces + fix du Final pour Claude et Codex, propagé aux adaptateurs et à la conformance (claude.rs, codex.rs, conformance.rs, mod.rs, process.rs, sandbox_e2e.rs). - app-tauri: relais Tauri des annonces vers le front (events.rs, chat.rs). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@ -303,8 +303,51 @@ impl AgentSession for ClaudeSdkSession {
|
||||
}
|
||||
|
||||
async fn send(&self, prompt: &str) -> Result<ReplyStream, AgentSessionError> {
|
||||
self.send_inner(prompt, None).await
|
||||
}
|
||||
|
||||
async fn send_with_tap(
|
||||
&self,
|
||||
prompt: &str,
|
||||
tap: std::sync::mpsc::Sender<ReplyEvent>,
|
||||
) -> Result<ReplyStream, AgentSessionError> {
|
||||
self.send_inner(prompt, Some(tap)).await
|
||||
}
|
||||
|
||||
async fn shutdown(&self) -> Result<(), AgentSessionError> {
|
||||
// Incarnation « un run par tour » : aucun process long ne survit entre les
|
||||
// tours, donc `shutdown` est intrinsèquement idempotent et sans effet.
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl ClaudeSdkSession {
|
||||
async fn send_inner(
|
||||
&self,
|
||||
prompt: &str,
|
||||
tap: Option<std::sync::mpsc::Sender<ReplyEvent>>,
|
||||
) -> Result<ReplyStream, AgentSessionError> {
|
||||
let spec = self.build_spawn_line(prompt);
|
||||
let raw_lines = run_turn(&spec, None, self.sandbox_enforcer.as_ref()).await?;
|
||||
let (line_tap, parser_thread) = tap.map_or((None, None), |event_tap| {
|
||||
let (line_tx, line_rx) = std::sync::mpsc::channel::<String>();
|
||||
let parser = std::thread::spawn(move || {
|
||||
for line in line_rx {
|
||||
if let Ok(parsed) = parse_event(&line) {
|
||||
for event in parsed.events {
|
||||
if let ReplyEvent::TextDelta { text } = event {
|
||||
let _ = event_tap.send(ReplyEvent::Announcement { text });
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
(Some(line_tx), Some(parser))
|
||||
});
|
||||
let raw_result = run_turn(&spec, None, self.sandbox_enforcer.as_ref(), line_tap).await;
|
||||
if let Some(parser) = parser_thread {
|
||||
let _ = parser.join();
|
||||
}
|
||||
let raw_lines = raw_result?;
|
||||
|
||||
let mut events = Vec::new();
|
||||
let mut captured_id = None;
|
||||
@ -334,10 +377,4 @@ impl AgentSession for ClaudeSdkSession {
|
||||
|
||||
Ok(Box::new(events.into_iter()))
|
||||
}
|
||||
|
||||
async fn shutdown(&self) -> Result<(), AgentSessionError> {
|
||||
// Incarnation « un run par tour » : aucun process long ne survit entre les
|
||||
// tours, donc `shutdown` est intrinsèquement idempotent et sans effet.
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@ -214,8 +214,51 @@ impl AgentSession for CodexExecSession {
|
||||
}
|
||||
|
||||
async fn send(&self, prompt: &str) -> Result<ReplyStream, AgentSessionError> {
|
||||
self.send_inner(prompt, None, true).await
|
||||
}
|
||||
|
||||
async fn send_with_tap(
|
||||
&self,
|
||||
prompt: &str,
|
||||
tap: std::sync::mpsc::Sender<ReplyEvent>,
|
||||
) -> Result<ReplyStream, AgentSessionError> {
|
||||
self.send_inner(prompt, Some(tap), false).await
|
||||
}
|
||||
|
||||
async fn shutdown(&self) -> Result<(), AgentSessionError> {
|
||||
// « un run par tour » ⇒ pas de process long survivant : idempotent, no-op.
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl CodexExecSession {
|
||||
async fn send_inner(
|
||||
&self,
|
||||
prompt: &str,
|
||||
tap: Option<std::sync::mpsc::Sender<ReplyEvent>>,
|
||||
include_stream_announcements: bool,
|
||||
) -> Result<ReplyStream, AgentSessionError> {
|
||||
let spec = self.build_spawn_line(prompt);
|
||||
let raw_lines = run_turn(&spec, None, self.sandbox_enforcer.as_ref()).await?;
|
||||
let (line_tap, parser_thread) = tap.map_or((None, None), |event_tap| {
|
||||
let (line_tx, line_rx) = std::sync::mpsc::channel::<String>();
|
||||
let parser = std::thread::spawn(move || {
|
||||
for line in line_rx {
|
||||
if let Ok(parsed) = parse_event(&line) {
|
||||
for event in parsed.events {
|
||||
if let ReplyEvent::Final { content } = event {
|
||||
let _ = event_tap.send(ReplyEvent::Announcement { text: content });
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
(Some(line_tx), Some(parser))
|
||||
});
|
||||
let raw_result = run_turn(&spec, None, self.sandbox_enforcer.as_ref(), line_tap).await;
|
||||
if let Some(parser) = parser_thread {
|
||||
let _ = parser.join();
|
||||
}
|
||||
let raw_lines = raw_result?;
|
||||
|
||||
let mut events = Vec::new();
|
||||
let mut captured_id = None;
|
||||
@ -236,11 +279,13 @@ impl AgentSession for CodexExecSession {
|
||||
match event {
|
||||
ReplyEvent::Final { content } => {
|
||||
// Un `agent_message` précédemment retenu est supersédé : il devient
|
||||
// une activité non terminale, on garde le plus récent en conclusion.
|
||||
// une annonce non terminale, on garde le plus récent en conclusion.
|
||||
if last_final.is_some() {
|
||||
events.push(ReplyEvent::ToolActivity {
|
||||
label: "agent_message".to_owned(),
|
||||
});
|
||||
if include_stream_announcements {
|
||||
events.push(ReplyEvent::Announcement {
|
||||
text: last_final.take().expect("présent"),
|
||||
});
|
||||
}
|
||||
}
|
||||
last_final = Some(content);
|
||||
}
|
||||
@ -259,9 +304,4 @@ impl AgentSession for CodexExecSession {
|
||||
|
||||
Ok(Box::new(events.into_iter()))
|
||||
}
|
||||
|
||||
async fn shutdown(&self) -> Result<(), AgentSessionError> {
|
||||
// « un run par tour » ⇒ pas de process long survivant : idempotent, no-op.
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@ -218,6 +218,7 @@ pub(crate) mod harness {
|
||||
e,
|
||||
ReplyEvent::TextDelta { .. }
|
||||
| ReplyEvent::ToolActivity { .. }
|
||||
| ReplyEvent::Announcement { .. }
|
||||
| ReplyEvent::Heartbeat
|
||||
| ReplyEvent::RateLimited { .. }
|
||||
),
|
||||
|
||||
@ -90,7 +90,7 @@ mod tests {
|
||||
#[tokio::test]
|
||||
async fn run_turn_drains_every_line_in_order() {
|
||||
let fake = FakeCli::printing(&["ligne-1", "ligne-2", "ligne-3"]);
|
||||
let lines = run_turn(&fake.spawn_line(), None, None)
|
||||
let lines = run_turn(&fake.spawn_line(), None, None, None)
|
||||
.await
|
||||
.expect("run_turn réussit");
|
||||
assert_eq!(lines, vec!["ligne-1", "ligne-2", "ligne-3"]);
|
||||
@ -106,7 +106,9 @@ mod tests {
|
||||
stdin: None,
|
||||
sandbox: None,
|
||||
};
|
||||
let err = run_turn(&spec, None, None).await.expect_err("doit échouer");
|
||||
let err = run_turn(&spec, None, None, None)
|
||||
.await
|
||||
.expect_err("doit échouer");
|
||||
assert!(matches!(err, AgentSessionError::Start(_)), "vu: {err:?}");
|
||||
}
|
||||
|
||||
@ -595,7 +597,7 @@ mod tests {
|
||||
stdin: None,
|
||||
sandbox: None,
|
||||
};
|
||||
let err = run_turn(&spec, Some(Duration::from_millis(50)), None)
|
||||
let err = run_turn(&spec, Some(Duration::from_millis(50)), None, None)
|
||||
.await
|
||||
.expect_err("doit expirer");
|
||||
assert!(matches!(err, AgentSessionError::Timeout), "vu: {err:?}");
|
||||
@ -858,6 +860,129 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn codex_two_agent_messages_preserve_first_as_announcement_and_last_as_final() {
|
||||
let fake = FakeCli::printing(&[
|
||||
r#"{"type":"thread.started","thread_id":"c"}"#,
|
||||
r#"{"type":"item.completed","item":{"id":"i0","type":"agent_message","text":"je regarde"}}"#,
|
||||
r#"{"type":"item.completed","item":{"id":"i1","type":"agent_message","text":"résultat"}}"#,
|
||||
]);
|
||||
let s = CodexExecSession::new(
|
||||
SessionId::new_random(),
|
||||
fake.command(),
|
||||
"/",
|
||||
None,
|
||||
Vec::new(),
|
||||
None,
|
||||
None,
|
||||
);
|
||||
|
||||
let events: Vec<_> = s.send("x").await.expect("send").collect();
|
||||
assert_eq!(
|
||||
events,
|
||||
vec![
|
||||
ReplyEvent::Announcement {
|
||||
text: "je regarde".into()
|
||||
},
|
||||
ReplyEvent::Final {
|
||||
content: "résultat".into()
|
||||
}
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn codex_single_agent_message_is_final_without_announcement() {
|
||||
let fake = FakeCli::printing(&[
|
||||
r#"{"type":"thread.started","thread_id":"c"}"#,
|
||||
r#"{"type":"item.completed","item":{"id":"i0","type":"agent_message","text":"résultat"}}"#,
|
||||
]);
|
||||
let s = CodexExecSession::new(
|
||||
SessionId::new_random(),
|
||||
fake.command(),
|
||||
"/",
|
||||
None,
|
||||
Vec::new(),
|
||||
None,
|
||||
None,
|
||||
);
|
||||
|
||||
let events: Vec<_> = s.send("x").await.expect("send").collect();
|
||||
assert_eq!(
|
||||
events,
|
||||
vec![ReplyEvent::Final {
|
||||
content: "résultat".into()
|
||||
}]
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn codex_zero_agent_message_has_no_final() {
|
||||
let fake = FakeCli::printing(&[
|
||||
r#"{"type":"thread.started","thread_id":"c"}"#,
|
||||
r#"{"type":"item.completed","item":{"id":"i0","type":"reasoning","text":"analyse"}}"#,
|
||||
]);
|
||||
let s = CodexExecSession::new(
|
||||
SessionId::new_random(),
|
||||
fake.command(),
|
||||
"/",
|
||||
None,
|
||||
Vec::new(),
|
||||
None,
|
||||
None,
|
||||
);
|
||||
|
||||
let events: Vec<_> = s.send("x").await.expect("send").collect();
|
||||
assert!(
|
||||
events
|
||||
.iter()
|
||||
.all(|e| !matches!(e, ReplyEvent::Final { .. })),
|
||||
"aucun agent_message => aucun Final"
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn codex_send_with_tap_emits_each_agent_message_live_as_announcement() {
|
||||
let fake = FakeCli::printing(&[
|
||||
r#"{"type":"thread.started","thread_id":"c"}"#,
|
||||
r#"{"type":"item.completed","item":{"id":"i0","type":"agent_message","text":"je regarde"}}"#,
|
||||
r#"{"type":"item.completed","item":{"id":"i1","type":"agent_message","text":"résultat"}}"#,
|
||||
]);
|
||||
let s = CodexExecSession::new(
|
||||
SessionId::new_random(),
|
||||
fake.command(),
|
||||
"/",
|
||||
None,
|
||||
Vec::new(),
|
||||
None,
|
||||
None,
|
||||
);
|
||||
let (tx, rx) = std::sync::mpsc::channel();
|
||||
|
||||
let events: Vec<_> = s.send_with_tap("x", tx).await.expect("send").collect();
|
||||
let live: Vec<_> = rx.into_iter().collect();
|
||||
|
||||
assert_eq!(
|
||||
live,
|
||||
vec![
|
||||
ReplyEvent::Announcement {
|
||||
text: "je regarde".into()
|
||||
},
|
||||
ReplyEvent::Announcement {
|
||||
text: "résultat".into()
|
||||
},
|
||||
],
|
||||
"le tap live publie chaque agent_message, y compris celui qui deviendra Final"
|
||||
);
|
||||
assert_eq!(
|
||||
events,
|
||||
vec![ReplyEvent::Final {
|
||||
content: "résultat".into()
|
||||
}],
|
||||
"le flux final du chemin tap garde seulement le dernier Final pour le demandeur"
|
||||
);
|
||||
}
|
||||
|
||||
/// LIMITE/ÉCART (à arbitrer) : un flux SANS `Final` ne provoque PAS d'erreur au
|
||||
/// niveau de l'adapter — `send()` renvoie Ok avec uniquement des deltas et AUCUN
|
||||
/// `Final`. Le §17.9 D2 mentionne « flux sans Final ⇒ Io » ; l'adapter actuel ne
|
||||
@ -892,7 +1017,9 @@ mod tests {
|
||||
#[tokio::test]
|
||||
async fn run_turn_empty_output_is_ok() {
|
||||
let fake = FakeCli::printing(&[]);
|
||||
let lines = run_turn(&fake.spawn_line(), None, None).await.expect("ok");
|
||||
let lines = run_turn(&fake.spawn_line(), None, None, None)
|
||||
.await
|
||||
.expect("ok");
|
||||
assert!(lines.is_empty());
|
||||
}
|
||||
|
||||
@ -902,7 +1029,7 @@ mod tests {
|
||||
let fake = FakeCli::printing(&["pong"]);
|
||||
let mut spec = fake.spawn_line();
|
||||
spec.stdin = Some("ping".to_owned());
|
||||
let lines = run_turn(&spec, None, None).await.expect("ok");
|
||||
let lines = run_turn(&spec, None, None, None).await.expect("ok");
|
||||
assert_eq!(lines, vec!["pong"]);
|
||||
}
|
||||
|
||||
@ -965,7 +1092,7 @@ mod tests {
|
||||
stdin: None,
|
||||
sandbox: None,
|
||||
};
|
||||
let err = run_turn(&spec, Some(Duration::from_millis(50)), None)
|
||||
let err = run_turn(&spec, Some(Duration::from_millis(50)), None, None)
|
||||
.await
|
||||
.expect_err("doit expirer");
|
||||
assert!(matches!(err, AgentSessionError::Timeout), "vu: {err:?}");
|
||||
|
||||
@ -28,6 +28,7 @@
|
||||
|
||||
use std::io;
|
||||
use std::process::Stdio;
|
||||
use std::sync::mpsc::Sender;
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
|
||||
@ -82,13 +83,15 @@ pub async fn run_turn(
|
||||
spec: &SpawnLine,
|
||||
timeout: Option<Duration>,
|
||||
enforcer: Option<&Arc<dyn SandboxEnforcer>>,
|
||||
line_tap: Option<Sender<String>>,
|
||||
) -> Result<Vec<String>, AgentSessionError> {
|
||||
// Chemin SANDBOXÉ (Linux + plan posé + enforcer câblé) : transpose la technique
|
||||
// du PTY (`spawn_command_sandboxed`) — enforce sur un thread jetable AVANT le fork,
|
||||
// l'enfant hérite le domaine via fork+exec.
|
||||
#[cfg(target_os = "linux")]
|
||||
if let (Some(plan), Some(enforcer)) = (spec.sandbox.as_ref(), enforcer) {
|
||||
return run_turn_sandboxed(spec, plan.clone(), Arc::clone(enforcer), timeout).await;
|
||||
return run_turn_sandboxed(spec, plan.clone(), Arc::clone(enforcer), timeout, line_tap)
|
||||
.await;
|
||||
}
|
||||
// Hors Linux : aucun sandboxing OS ⇒ on ignore l'enforcer (Noop de toute façon) et
|
||||
// on garde le drain async historique. `let _` évite l'avertissement « unused ».
|
||||
@ -96,11 +99,11 @@ pub async fn run_turn(
|
||||
let _ = enforcer;
|
||||
|
||||
match timeout {
|
||||
Some(dur) => match tokio::time::timeout(dur, drain(spec)).await {
|
||||
Some(dur) => match tokio::time::timeout(dur, drain(spec, line_tap)).await {
|
||||
Ok(result) => result,
|
||||
Err(_elapsed) => Err(AgentSessionError::Timeout),
|
||||
},
|
||||
None => drain(spec).await,
|
||||
None => drain(spec, line_tap).await,
|
||||
}
|
||||
}
|
||||
|
||||
@ -141,6 +144,7 @@ async fn run_turn_sandboxed(
|
||||
plan: SandboxPlan,
|
||||
enforcer: Arc<dyn SandboxEnforcer>,
|
||||
timeout: Option<Duration>,
|
||||
line_tap: Option<Sender<String>>,
|
||||
) -> Result<Vec<String>, AgentSessionError> {
|
||||
use std::sync::Mutex as StdMutex;
|
||||
|
||||
@ -158,7 +162,9 @@ async fn run_turn_sandboxed(
|
||||
|
||||
// Thread JETABLE : sa restriction Landlock meurt avec lui.
|
||||
std::thread::spawn(move || {
|
||||
let result = drain_sandboxed(command, args, cwd, env, stdin, &enforcer, &plan, killer_tx);
|
||||
let result = drain_sandboxed(
|
||||
command, args, cwd, env, stdin, &enforcer, &plan, killer_tx, line_tap,
|
||||
);
|
||||
// Le récepteur peut avoir abandonné (timeout) : on ignore l'erreur d'envoi.
|
||||
let _ = done_tx.send(result);
|
||||
});
|
||||
@ -211,6 +217,7 @@ fn drain_sandboxed(
|
||||
enforcer: &Arc<dyn SandboxEnforcer>,
|
||||
plan: &SandboxPlan,
|
||||
killer_tx: tokio::sync::oneshot::Sender<Arc<std::sync::Mutex<std::process::Child>>>,
|
||||
line_tap: Option<Sender<String>>,
|
||||
) -> Result<Vec<String>, AgentSessionError> {
|
||||
use std::io::{BufRead, BufReader as StdBufReader, Write as _};
|
||||
use std::process::{Command as StdCommand, Stdio};
|
||||
@ -267,7 +274,12 @@ fn drain_sandboxed(
|
||||
let mut collected = Vec::new();
|
||||
for line in StdBufReader::new(stdout).lines() {
|
||||
match line {
|
||||
Ok(l) => collected.push(l),
|
||||
Ok(l) => {
|
||||
if let Some(tap) = &line_tap {
|
||||
let _ = tap.send(l.clone());
|
||||
}
|
||||
collected.push(l);
|
||||
}
|
||||
Err(e) => return Err(AgentSessionError::Io(e.to_string())),
|
||||
}
|
||||
}
|
||||
@ -280,7 +292,10 @@ fn drain_sandboxed(
|
||||
}
|
||||
|
||||
/// Cœur du drain : spawn → écriture stdin → lecture ligne-à-ligne → wait.
|
||||
async fn drain(spec: &SpawnLine) -> Result<Vec<String>, AgentSessionError> {
|
||||
async fn drain(
|
||||
spec: &SpawnLine,
|
||||
line_tap: Option<Sender<String>>,
|
||||
) -> Result<Vec<String>, AgentSessionError> {
|
||||
let mut cmd = Command::new(&spec.command);
|
||||
cmd.args(&spec.args)
|
||||
.stdin(Stdio::piped())
|
||||
@ -323,7 +338,12 @@ async fn drain(spec: &SpawnLine) -> Result<Vec<String>, AgentSessionError> {
|
||||
let mut collected = Vec::new();
|
||||
loop {
|
||||
match lines.next_line().await {
|
||||
Ok(Some(line)) => collected.push(line),
|
||||
Ok(Some(line)) => {
|
||||
if let Some(tap) = &line_tap {
|
||||
let _ = tap.send(line.clone());
|
||||
}
|
||||
collected.push(line);
|
||||
}
|
||||
Ok(None) => break,
|
||||
Err(e) => return Err(map_io(e)),
|
||||
}
|
||||
|
||||
@ -139,7 +139,7 @@ async fn pty_structured_run_turn_enforces_plan_end_to_end() {
|
||||
);
|
||||
let enforcer = crate::sandbox::default_enforcer();
|
||||
|
||||
let lines = run_turn(&spec, None, Some(&enforcer))
|
||||
let lines = run_turn(&spec, None, Some(&enforcer), None)
|
||||
.await
|
||||
.expect("run_turn sandboxé réussit sous posture Ask");
|
||||
|
||||
@ -182,7 +182,7 @@ async fn structured_run_turn_without_plan_does_not_sandbox() {
|
||||
let spec = writing_spawn_line(RESULT_LINE, &denied_marker, &allowed_marker, None);
|
||||
let enforcer = crate::sandbox::default_enforcer();
|
||||
|
||||
let lines = run_turn(&spec, None, Some(&enforcer))
|
||||
let lines = run_turn(&spec, None, Some(&enforcer), None)
|
||||
.await
|
||||
.expect("run_turn natif réussit");
|
||||
|
||||
@ -253,7 +253,7 @@ async fn structured_run_turn_fail_closed_no_child_on_enforce_err() {
|
||||
};
|
||||
let enforcer: Arc<dyn SandboxEnforcer> = Arc::new(AlwaysFailEnforcer);
|
||||
|
||||
let err = run_turn(&spec, None, Some(&enforcer))
|
||||
let err = run_turn(&spec, None, Some(&enforcer), None)
|
||||
.await
|
||||
.expect_err("enforce Err ⇒ run_turn doit échouer (fail-closed)");
|
||||
assert!(
|
||||
@ -296,7 +296,7 @@ async fn structured_run_turn_none_plan_is_native_path() {
|
||||
let enforcer = crate::sandbox::default_enforcer();
|
||||
|
||||
// Enforcer fourni MAIS plan None ⇒ la branche sandboxée n'est pas prise.
|
||||
let lines = run_turn(&spec, None, Some(&enforcer))
|
||||
let lines = run_turn(&spec, None, Some(&enforcer), None)
|
||||
.await
|
||||
.expect("chemin natif réussit");
|
||||
assert_eq!(lines, vec![RESULT_LINE.to_owned()]);
|
||||
@ -330,7 +330,7 @@ async fn structured_two_turns_disjoint_grants_are_confined() {
|
||||
let a_in = dir_a.join("in.txt");
|
||||
let a_into_b = dir_b.join("from-a.txt");
|
||||
let spec_a = writing_spawn_line(RESULT_LINE, &a_into_b, &a_in, Some(rw_plan(&dir_a)));
|
||||
run_turn(&spec_a, None, Some(&enforcer))
|
||||
run_turn(&spec_a, None, Some(&enforcer), None)
|
||||
.await
|
||||
.expect("tour A ok");
|
||||
assert!(
|
||||
@ -347,7 +347,7 @@ async fn structured_two_turns_disjoint_grants_are_confined() {
|
||||
let b_in = dir_b.join("in.txt");
|
||||
let b_into_a = dir_a.join("from-b.txt");
|
||||
let spec_b = writing_spawn_line(RESULT_LINE, &b_into_a, &b_in, Some(rw_plan(&dir_b)));
|
||||
run_turn(&spec_b, None, Some(&enforcer))
|
||||
run_turn(&spec_b, None, Some(&enforcer), None)
|
||||
.await
|
||||
.expect("tour B ok");
|
||||
assert!(
|
||||
@ -388,9 +388,14 @@ async fn structured_run_turn_timeout_under_sandbox() {
|
||||
let enforcer = crate::sandbox::default_enforcer();
|
||||
|
||||
let started = Instant::now();
|
||||
let err = run_turn(&spec, Some(Duration::from_millis(250)), Some(&enforcer))
|
||||
.await
|
||||
.expect_err("doit expirer");
|
||||
let err = run_turn(
|
||||
&spec,
|
||||
Some(Duration::from_millis(250)),
|
||||
Some(&enforcer),
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.expect_err("doit expirer");
|
||||
let elapsed = started.elapsed();
|
||||
|
||||
assert!(
|
||||
|
||||
Reference in New Issue
Block a user